Hold mobile pushes for to-dos outside working hours
What does this MR do and why?
Outside a user's working hours, no push is sent for a new to-do.
Notifications::MobilePush::SendTodoNotificationsServiceasks the recipient's schedule before the rate limiter. Outside the window it does not push, countsskipped_outside_working_hours, and records the user onGitlab::MobilePush::WorkingHoursHold, one Redis sorted set (Gitlab::Redis::SharedState) scored with the next window start. Recording again moves the user to the new time.- The check sits before the limiter on purpose: a skipped push must not spend the user's hourly budget, and the limiter's own "You have new to-dos" summary must stay quiet outside working hours too.
UserPreference#mobile_push_deliver_now?and#mobile_push_next_window_starthand the stored schedule toGitlab::MobilePush::WorkingHours; off means always deliver. A schedule the server cannot evaluate delivers, and one with no computable next start is only counted.Gitlab::MobilePush::Recipientgainsworking_hours_enabled?,deliver_now?andnext_window_start, answered from the preference.Todo.with_preloaded_user_and_push_subscriptionsalso preloads the user preference, so the check adds no query per recipient (covered by a query-count example).
Nothing sends the held summaries yet; the next two MRs do. Until D merges nobody can turn the switch on, so this is dark.
References
Split from !260440 (closed), as agreed with the reviewer there: seven merge requests of 200 to 300 lines each, cut where the code does not reference the other parts. Each one targets its parent branch and is retargeted to master when the parent merges.
Merge order is the chain order: A, B1, B2, C1, C2a, C2b, then D. D is the switch: it targets C2b, so the API cannot reach master before the server can hold a push and send its summary. Any other order would open a window on GitLab.com where users who enabled the schedule have their pushes held but not summarized until C2b deploys.
Screenshots or screen recordings
No UI changes.
How to set up and validate locally
- With a registered device (see
doc/api/mobile_push_subscriptions.md) and themobile_push_notifications_dispatchandmobile_push_notificationsfeature flags on for your user, set a window that is closed right now, in a Rails console:
user = User.find_by!(username: 'root')
user.user_preference.update!(
mobile_push_working_hours_enabled: true,
mobile_push_working_hours_start: 3 * 60, # 03:00
mobile_push_working_hours_end: 4 * 60, # 04:00
mobile_push_working_days: %w[monday tuesday wednesday thursday friday],
mobile_push_working_hours_time_zone: 'Europe/Berlin'
)-
From a second user, assign yourself an issue. No push arrives;
log/sidekiq.logshowsTodos::PushNotificationWorkerwithresults.skipped_outside_working_hours: 1. -
The user is on the wake-up list, scored with the next 03:00 in Berlin:
Gitlab::Redis::SharedState.with { |redis| redis.zrange('mobile_push:working_hours:resume', 0, -1, with_scores: true) } -
Widen the window to cover the current time and assign another issue: the push arrives.
Database
A skipped push writes to Redis only; nothing new is written to PostgreSQL. The one new read is the preference in the existing per-batch preload. Measured with EXPLAIN ANALYZE on a production clone (postgres.ai, gitlab-production-main) for the original merge request; the statements are unchanged. The id lists are synthetic.
-
Per to-do batch, the preload
Todo.with_preloaded_user_and_push_subscriptionsgains the preference:SELECT "user_preferences".* FROM "user_preferences" WHERE "user_preferences"."user_id" IN (...)over the uniqueindex_user_preferences_on_user_id. 10 ids: 16.312 ms, shared buffers 18 hit / 13 read.Plan
Index Scan using index_user_preferences_on_user_id on public.user_preferences (cost=0.44..27.22 rows=10 width=254) (actual time=9.326..14.750 rows=1 loops=1)
Index Cond: (user_preferences.user_id = ANY ('{...}'::integer[]))
Buffers: shared hit=18 read=13
I/O Timings: read=14.652 write=0.000
Settings: seq_page_cost = '4', effective_cache_size = '472585MB', work_mem = '230MB', jit = 'off', random_page_cost = '1.5'
Query ID: -7106013930098408959
Time: 16.312 ms
- planning: 1.492 ms
- execution: 14.820 ms
- I/O read: 14.652 ms
- I/O write: 0.000 ms
Shared buffers:
- hits: 18 (~144.00 KiB) from the buffer pool
- reads: 13 (~104.00 KiB) from the OS file cache, including disk I/O
- dirtied: 0
- writes: 0MR acceptance checklist
Evaluated against the MR acceptance checklist.