Skip to content

fix(user_status): one heartbeat per interval, and refresh before invalidation - #63746

Open
solracsf wants to merge 2 commits into
masterfrom
fix/user-status-heartbeat-rate
Open

fix(user_status): one heartbeat per interval, and refresh before invalidation#63746
solracsf wants to merge 2 commits into
masterfrom
fix/user-status-heartbeat-rate

Conversation

@solracsf

@solracsf solracsf commented Aug 27, 2026

Copy link
Copy Markdown
Member

Summary

Two fixes that need to land together, one in the browser and one on the server.

The heartbeat fires on every mouse movement

The away countdown was cancelled with clearTimeout(this.setAwayTimeout), which passes the callback instead of the id setTimeout returned. Nothing was ever cancelled and the id was thrown away, so every burst of mouse movement left another two minute timer running. Each one that expired marked an active user as away, and the next movement flipped them back and sent a heartbeat. An ordinary session ends up sending around 593 heartbeats an hour instead of the 13 the code intends, and holding a timer for each one.

The scheduling moves into src/services/heartbeatScheduler.ts so it can be tested without mounting the component. That also fixes the teardown: the listener was added as mousemove with capture and removed as mouseMove without it, so it was never actually removed and every unmounted component leaked one.

A live status is invalidated before it gets refreshed

UserLiveStatusListener only refreshed status_timestamp once it was already older than INVALIDATE_STATUS_THRESHOLD, which is the same 15 minutes at which ClearOldStatusesBackgroundJob sweeps a status to offline. The job runs every minute and the client heartbeats every five, so an active user can show up as offline for up to five minutes until their next heartbeat lands.

The refresh now happens at a new REFRESH_STATUS_THRESHOLD of 7 minutes, which caps the age of a live status at 12 minutes (the refresh threshold plus one heartbeat interval), well under the 15 at which it gets swept.

Why they go together

The first fix is what makes the second bug visible. Before it, an active user flapped between away and online every couple of seconds, and every flip hits the listener's priority branch and refreshes the timestamp. That is what was hiding the stale refresh.

Going from 593 heartbeats an hour down to 13 would open the offline window on its own, so these two should not be split.

Trade-offs

An active user's oc_user_status row is now written roughly every 10 minutes instead of every 20, so twice the writes on that table, against 45x fewer heartbeat requests.

REFRESH_STATUS_THRESHOLD assumes no client heartbeats slower than 8 minutes. Web, desktop, mobile and Talk all call the same OCS endpoint, so if any of them ever uses a slower interval the constant needs a second look.

Testing

heartbeatScheduler.spec.ts reports 593 calls against the old inline logic where it expects 13, and its teardown test catches the leaked listener. UserLiveStatusListenerIntegrationTest steps time at the cleanup job's one minute cadence and asserts after every sweep, and it fails at minute 15 without the server fix. Two rows were added to the UserLiveStatusListenerTest provider for the new threshold. The full apps/user_status suite (205 tests) is green on MariaDB 11.8 and PostgreSQL 16.

Checklist

  • Tests (unit, integration) are included
  • Screenshots before/after for front-end changes (no visual change)
  • Documentation has been updated or is not required
  • Backports requested where applicable
  • Labels added where applicable
  • Milestone added for target branch/version

AI (if applicable)

  • The content of this PR was partly or fully generated using AI

@solracsf
solracsf requested review from a team as code owners August 27, 2026 19:10
@solracsf
solracsf requested review from Altahrim, icewind1991, kristian-zendato, leftybournes, skjnldsv and sorbaugh and removed request for a team August 27, 2026 19:10
@solracsf solracsf self-assigned this Aug 27, 2026
@solracsf solracsf added the 3. to review Waiting for reviews label Aug 27, 2026
@solracsf solracsf added this to the Nextcloud 36 milestone Aug 27, 2026
@solracsf
solracsf force-pushed the fix/user-status-heartbeat-rate branch from fa89c13 to fc9c32f Compare August 27, 2026 19:23
The away countdown was cancelled by passing the callback function to
clearTimeout() instead of the id returned by setTimeout(), so the clear
did nothing and the id was thrown away. Every burst of mouse movement
scheduled another two minute timer that nothing could cancel, and each
one that expired marked an active user as away. The next movement
flipped them back and sent a heartbeat, so an ordinary browsing session
sent around 600 heartbeats an hour instead of the 13 the code intends.

The scheduling now lives in its own module, which makes it testable
without mounting the component and gives the timers a single owner. That
also fixes the teardown: the listener was registered as "mousemove" with
capture and removed as "mouseMove" without it, so it was never actually
removed and every unmounted component leaked one.

Signed-off-by: Git'Fellow <12234510+solracsf@users.noreply.github.com>
UserLiveStatusListener only refreshed status_timestamp once it was
already older than INVALIDATE_STATUS_THRESHOLD, which is the same 15
minutes at which ClearOldStatusesBackgroundJob sweeps a status to
offline. With a five minute client interval that leaves a window, up to
one interval wide, in which a user who never stopped working is shown as
offline until their next heartbeat arrives.

The refresh now happens at REFRESH_STATUS_THRESHOLD, far enough below
the invalidation threshold to leave room for a full heartbeat interval.
Until now the sheer number of heartbeats hid the problem, because one
always landed within seconds of the cleanup job.

Signed-off-by: Git'Fellow <12234510+solracsf@users.noreply.github.com>
@solracsf
solracsf force-pushed the fix/user-status-heartbeat-rate branch from fc9c32f to 0d47a25 Compare August 27, 2026 19:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant