-
Notifications
You must be signed in to change notification settings - Fork 224
Notification events resolving and rendering in batches #4722
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
📱 Scan the QR code below to install the build (arm64 only) for this PR. |
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #4722 +/- ##
===========================================
+ Coverage 80.40% 80.41% +0.01%
===========================================
Files 2143 2144 +1
Lines 56678 56751 +73
Branches 7096 7112 +16
===========================================
+ Hits 45570 45635 +65
- Misses 8681 8684 +3
- Partials 2427 2432 +5 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for improving this part!
Some early remarks.
|
||
interface NotificationService { | ||
suspend fun getNotification(roomId: RoomId, eventId: EventId): Result<NotificationData?> | ||
suspend fun getNotification(sessionId: SessionId, roomId: RoomId, eventId: EventId): Result<NotificationData?> | ||
suspend fun getNotifications(sessionId: SessionId, ids: Map<RoomId, List<EventId>>): Result<Map<EventId, NotificationData?>> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At first sight, it's strange that the return type is not
Result<Map<RoomId, List<NotificationData>>>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was like that originally, but where it's immediately used later (here) having the notifications grouped by room id doesn't really help, we'd have to first search for the room, then use notifications.find { it.eventId == eventId }
. Using having the id -> data mapping is more optimised.
suspend fun resolveEvents( | ||
sessionId: SessionId, | ||
notificationEventRequests: List<NotificationEventRequest> | ||
): Result<Map<EventId, ResolvedPushEvent?>> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same type of remark, why don't we have a return type like:
Map<RoomId, List<ResolvedPushEvent>>
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here it might actually make sense to do that for the pushHistoryService.onSuccess
and failure callbacks, searching might be a bit faster even if later we need to either iterate by room then event id or just flatten everything.
Maybe it would even make sense to use the NotificationEventRequest
as the key itself? It has the session id, room and event ids to make fetching a single operation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
More comments. I will test the code on a real device.
@@ -30,8 +30,9 @@ class DefaultOnMissedCallNotificationHandler @Inject constructor( | |||
// Resolve the event and add a notification for it, at this point it should no longer be a ringing one | |||
val notificationData = matrixClientProvider.getOrRestore(sessionId).getOrNull() | |||
?.notificationService() | |||
?.getNotification(roomId, eventId) | |||
?.getNotifications(sessionId, mapOf(roomId to listOf(eventId))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks weird to have to provide a sessionId
here, since we are using a method from a service of a matrix client.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True, but RustNotificationService
doesn't have a reference to the matrix client. Maybe I should just add a SessionId
as a constructor param for it. WDYT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe I should just add a
SessionId
as a constructor param for it. WDYT?
Yes please. We're doing it for RustRoomFactory
already for instance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RustRoomFactory
doesn't use injection though. Adding it here would mean creating a factory abstraction.
Never mind, I looked at the wrong component. I have a severe case of the Mondays, it seems.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
coroutineScope.launch { | ||
while (coroutineScope.isActive) { | ||
// Wait for a batch of requests to be received in a specified time window | ||
delay(BATCH_WINDOW_MS.milliseconds) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you sure this code will behave as a first throttler?
If we get a Push just before the delay is ended, it will trigger the request without waiting for any other potential push. I have not checked in practice how much time we have between mutliple pending push received from Firebase though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It doesn't have to act as a throttle operation, but as a window one, grouping events that happen in an interval in batches. Those batches won't always be optimal, since as you say, if you receive one after one window closes it'll be processed on the next one.
If you think it's worth it implementing it more in a throttle/debounce way (without discarding items as those operators usually do) I can try to do that. There is the possibility that this means we chain lots of 'received item, wait for X ms' operations and end up delaying the processing for longer, but it's not really likely to happen in the real world except maybe for when you have several pushes in firebase that couldn't be delivered, the device reconnects and it starts receiving the pending pushes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in bd27b50, it seems to work quite well.
val notificationManager = NotificationManagerCompat.from(context) | ||
val previousNotification = notificationManager.activeNotifications.find { it.tag == roomInfo.roomId.value } | ||
val elapsed = System.currentTimeMillis() - (previousNotification?.postTime ?: 0L) | ||
Timber.d("Creating noisy notification for room ${roomInfo.roomId.value} with elapsed time $elapsed") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move this log into the if block (not sure)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ooops, this should actually be gone. It was done for an experiment and I removed part of it, but apparently I kept this by mistake.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few remarks, else I think we can merge this PR to make is live in the next nightlies for a few days
|
||
delay(BATCH_WINDOW_MS.milliseconds) | ||
|
||
if (!isActive) return@launch |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this line mandatory? I mean if the job is cancelled during the delay, I do not think this line and the following will be executed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, it's not needed. delay
will check if the context is alive for the whole duration of the function being suspended.
/** | ||
* Enqueues a notification event request to be resolved. | ||
* The request will be processed in batches, so it may not be resolved immediately. | ||
* | ||
* @param request The notification event request to enqueue. | ||
*/ | ||
suspend fun enqueue(request: NotificationEventRequest) { | ||
// Cancel previous processing job if it exists, acting as a debounce operation | ||
Timber.d("Cancelling job: $currentProcessingJob") | ||
currentProcessingJob?.cancel() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should cancel only a delay
operation, just reading the code I am afraid that it can cancel a started treatment, but I may be wrong
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's what I'm trying to do: the 'enqueue' job is saved and will be cancelled when a new one arrives. Internally, it waits for the time window and launches a new coroutine in the parent one, which should be independent AFAICT. Maybe we could use a SupervisorJob
to ensure the 'enqueue' job being cancelled won't affect any other child coroutine of the injected scope.
…cation is disabled for the time being).
…s inside the same time window
…ions` to its constructor
… cancelling it doesn't affect their siblings
bd27b50
to
a4f1595
Compare
|
Content
NotificationResolverQueue
type that enqueues notification fetching requests received in a certain time window, and resolves them in batches, emitting the results when done.DefaultPushHandler
now subscribes to these results and also processes them in batches, categorising the items and redirecting them to their callbacks (onRedactedEventReceived
,handleRingingCallEvent
, etc.).DefaultNotificationDrawerManager
now has aonNotifiableEventsReceived
to render several notifications at the same time.Motivation and context
The latest SDK version allows clients to fetch several notifications as a group so we need to run a single sliding sync request for them. However, since the items were returned at the same time and immediately sent to the
NotificationManager
so they can be displayed, the OS thought we were spamming the notifications and 'muted' (or discarded) some of them. Instead, we had to rework how we process the resolved notifications.Tests
Tested devices
Checklist