proxy server: collapse low-priority push notifications

When a device is unreachable (deep doze, offline), FCM queues
non-collapsible messages individually, with a hard limit of 100
pending messages per device. A Jami device that is a member of a few
active swarms receives a sustained stream of connection-churn and
expiration notifications (observed: 27-44/min, ~12k/day), so a phone
suspended for a few tens of minutes is enough to overflow the queue.
Once full, FCM silently drops messages, including the high-priority
notification announcing an incoming call: the callee never wakes up
and the caller gets a timeout after the ICE window (observed in
production, with the call notification never delivered while pushes
sent after the device woke up arrived in under a second).

Mark Android notifications as collapsible (collapse_key) when they
are neither high-priority (calls, messages, invitations - as set by
the client through Value::priority) nor resubscribe requests. While
collapsed messages are pending, FCM only keeps the last one per key,
so routine sync noise can no longer evict actionable notifications.

A single constant key is used instead of one key per DHT listen key:
FCM only honors 4 distinct collapse keys per device, and the Android
client reacts to any low-priority notification the same way (wake up
and resynchronize against the proxy), so finer granularity would not
change client behavior.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Cyrille BéraudandCopilot committed 2026-06-12 08:22:02 -04:00
1 parent 732da0e478
commit e0fcee240a
1 file changed
+15
+15
View File
@@ -1267,6 +1267,21 @@ DhtProxyServer::sendPushNotification(
Json::Value androidConfig(Json::objectValue);
androidConfig["priority"] = priority;
androidConfig["ttl"] = "86400s"; // time to live = 24 hours
// Make routine sync notifications (connection churn, value
// expirations) collapsible: FCM keeps only the last pending
// message per collapse key when the device is unreachable
// (dozing/offline), while non-collapsible messages pile up in
// a 100-message queue. When that queue overflows, FCM silently
// drops messages - including the high-priority ones that
// announce incoming calls. A single constant key is used
// because FCM only honors 4 distinct collapse keys per device
// and the Android client reacts to any of these notifications
// the same way: wake up and resynchronize against the proxy.
// High-priority notifications (calls, messages, invites - see
// Value::priority set by the client) and resubscribe requests
// remain non-collapsible so none of them can be overwritten.
if (!highPriority && !isResubscribe)
androidConfig["collapse_key"] = "sync";
notification["android"] = std::move(androidConfig);
} else {
notification["priority"] = priority;