Make the presence state-machine timers configurable (#19942)

Adds `last_active_granularity`, `sync_online_timeout` and `idle_timeout`
options to the `presence` config section, controlling the previously
hard-coded `LAST_ACTIVE_GRANULARITY`, `SYNC_ONLINE_TIMEOUT` and
`IDLE_TIMER` constants (which remain as the defaults).

This is mainly useful on deployments that ratelimit how often syncs can
affect presence (`rc_presence`): the sync timeout must exceed the
ratelimit interval or users flap offline between syncs, so tuning down
presence traffic currently requires squeezing under the fixed 30s limit.

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik Johnston
2026-07-09 17:21:50 +01:00
committed by GitHub
parent 365df86536
commit 3f3edaf029
6 changed files with 401 additions and 63 deletions
+1
View File
@@ -0,0 +1 @@
Add `last_active_granularity`, `sync_online_timeout` and `idle_timeout` options to the `presence` config section to allow tuning the presence state machine timers.
@@ -284,6 +284,24 @@ This setting has the following sub-options:
* `include_offline_users_on_sync` (boolean): When clients perform an initial or `full_state` sync, presence results for offline users are not included by default. Setting `include_offline_users_on_sync` to `true` will always include offline users in the results. Defaults to `false`.
* `last_active_granularity` (duration): How long after a user was last active that they are still shown as "currently active" to other users. Larger values reduce the rate of presence updates sent to other users and servers.
*Added in Synapse 1.156.0.*
Defaults to `"1m"`.
* `sync_online_timeout` (duration): How long after a client's last sync request their presence is switched to offline. Clients are expected to keep a sync request open at (almost) all times while online, so this only needs to cover the gap between two consecutive sync requests. Note that if `rc_presence` is set to ratelimit how often syncs can affect presence, this must be greater than the ratelimit's interval or users will incorrectly be marked as offline in between syncs.
*Added in Synapse 1.156.0.*
Defaults to `"30s"`.
* `idle_timeout` (duration): How long after a user was last active that their presence is switched to "unavailable" (idle) while they remain connected. Must be greater than `last_active_granularity`.
*Added in Synapse 1.156.0.*
Defaults to `"5m"`.
Example configuration:
```yaml
presence:
+40
View File
@@ -202,6 +202,46 @@ properties:
`include_offline_users_on_sync` to `true` will always include offline
users in the results.
default: false
last_active_granularity:
$ref: "#/$defs/duration"
description: >-
How long after a user was last active that they are still shown as
"currently active" to other users. Larger values reduce the rate of
presence updates sent to other users and servers.
*Added in Synapse 1.156.0.*
default: 1m
examples:
- 5m
sync_online_timeout:
$ref: "#/$defs/duration"
description: >-
How long after a client's last sync request their presence is
switched to offline. Clients are expected to keep a sync request
open at (almost) all times while online, so this only needs to
cover the gap between two consecutive sync requests. Note that if
`rc_presence` is set to ratelimit how often syncs can affect
presence, this must be greater than the ratelimit's interval or
users will incorrectly be marked as offline in between syncs.
*Added in Synapse 1.156.0.*
default: 30s
examples:
- 1m
idle_timeout:
$ref: "#/$defs/duration"
description: >-
How long after a user was last active that their presence is
switched to "unavailable" (idle) while they remain connected. Must
be greater than `last_active_granularity`.
*Added in Synapse 1.156.0.*
default: 5m
examples:
- 10m
examples:
- enabled: false
include_offline_users_on_sync: false
+39
View File
@@ -177,6 +177,19 @@ DEFAULT_IP_RANGE_BLOCKLIST = [
DEFAULT_ROOM_VERSION = "10"
# Defaults for the presence state machine timers, in milliseconds. Overridden
# by the corresponding options in the `presence` config section.
#
# How long after a user was last active that they are still considered
# "currently_active".
DEFAULT_LAST_ACTIVE_GRANULARITY = 60 * 1000
# How long to wait until a new /events or /sync request before assuming the
# client has gone.
DEFAULT_SYNC_ONLINE_TIMEOUT = 30 * 1000
# How long to wait before marking the user as idle. Compared against last
# active.
DEFAULT_IDLE_TIMER = 5 * 60 * 1000
ROOM_COMPLEXITY_TOO_GREAT = (
"Your homeserver is unable to join rooms this large or complex. "
"Please speak to your server administrator, or upgrade your instance "
@@ -505,6 +518,32 @@ class ServerConfig(Config):
"include_offline_users_on_sync", False
)
# Timers controlling the presence state machine.
self.presence_last_active_granularity = self.parse_duration(
presence_config.get(
"last_active_granularity", DEFAULT_LAST_ACTIVE_GRANULARITY
)
)
self.presence_sync_online_timeout = self.parse_duration(
presence_config.get("sync_online_timeout", DEFAULT_SYNC_ONLINE_TIMEOUT)
)
self.presence_idle_timeout = self.parse_duration(
presence_config.get("idle_timeout", DEFAULT_IDLE_TIMER)
)
if self.presence_last_active_granularity <= 0:
raise ConfigError(
"'presence.last_active_granularity' must be a positive duration"
)
if self.presence_sync_online_timeout <= 0:
raise ConfigError(
"'presence.sync_online_timeout' must be a positive duration"
)
if self.presence_idle_timeout <= self.presence_last_active_granularity:
raise ConfigError(
"'presence.idle_timeout' must be greater than "
"'presence.last_active_granularity'"
)
# Custom presence router module
# This is the legacy way of configuring it (the config should now be put in the modules section)
self.presence_router_module_class = None
+81 -26
View File
@@ -179,19 +179,18 @@ presence_wheel_timer_size_gauge = LaterGauge(
labelnames=[SERVER_NAME_LABEL],
)
# If a user was last active in the last LAST_ACTIVE_GRANULARITY, consider them
# "currently_active"
LAST_ACTIVE_GRANULARITY = 60 * 1000
# Note: the timers deciding when a user goes idle or offline and how long
# they count as "currently_active" are configurable, via the
# `last_active_granularity`, `sync_online_timeout` and `idle_timeout` options
# in the `presence` config section (see
# `synapse.config.server.DEFAULT_LAST_ACTIVE_GRANULARITY` and friends for the
# defaults).
# How long to wait until a new /events or /sync request before assuming
# the client has gone.
SYNC_ONLINE_TIMEOUT = 30 * 1000
# Busy status waits longer, but does eventually go offline.
# How long to wait until a device with busy status stops syncing before it
# goes offline. Busy status waits longer than the (configurable) sync online
# timeout, but does eventually go offline.
BUSY_ONLINE_TIMEOUT = 60 * 60 * 1000
# How long to wait before marking the user as idle. Compared against last active
IDLE_TIMER = 5 * 60 * 1000
# How often we expect remote servers to resend us presence.
FEDERATION_TIMEOUT = 30 * 60 * 1000
@@ -206,8 +205,6 @@ EXTERNAL_PROCESS_EXPIRY = 5 * 60 * 1000
# syncing.
UPDATE_SYNCING_USERS = Duration(seconds=10)
assert LAST_ACTIVE_GRANULARITY < IDLE_TIMER
class BasePresenceHandler(abc.ABC):
"""Parts of the PresenceHandler that are shared between workers and presence
@@ -225,6 +222,13 @@ class BasePresenceHandler(abc.ABC):
self._presence_enabled = hs.config.server.presence_enabled
self._track_presence = hs.config.server.track_presence
# The (configurable) presence state machine timers.
self._last_active_granularity = (
hs.config.server.presence_last_active_granularity
)
self._sync_online_timeout = hs.config.server.presence_sync_online_timeout
self._idle_timer = hs.config.server.presence_idle_timeout
self._federation = None
if hs.should_send_federation():
self._federation = hs.get_federation_sender()
@@ -685,7 +689,11 @@ class WorkerPresenceHandler(BasePresenceHandler):
self.user_to_current_state[new_state.user_id] = new_state
is_mine = self.is_mine_id(new_state.user_id)
if not old_state or should_notify(
old_state, new_state, is_mine, self.server_name
old_state,
new_state,
is_mine,
self.server_name,
last_active_granularity=self._last_active_granularity,
):
state_to_notify.append(new_state)
@@ -791,7 +799,8 @@ class PresenceHandler(BasePresenceHandler):
if self._track_presence:
for state in self.user_to_current_state.values():
# Create a psuedo-device to properly handle time outs. This will
# be overridden by any "real" devices within SYNC_ONLINE_TIMEOUT.
# be overridden by any "real" devices within the sync online
# timeout.
pseudo_device_id = None
self._user_to_device_to_current_state[state.user_id] = {
pseudo_device_id: UserDevicePresenceState(
@@ -804,12 +813,14 @@ class PresenceHandler(BasePresenceHandler):
}
self.wheel_timer.insert(
now=now, obj=state.user_id, then=state.last_active_ts + IDLE_TIMER
now=now,
obj=state.user_id,
then=state.last_active_ts + self._idle_timer,
)
self.wheel_timer.insert(
now=now,
obj=state.user_id,
then=state.last_user_sync_ts + SYNC_ONLINE_TIMEOUT,
then=state.last_user_sync_ts + self._sync_online_timeout,
)
if self.is_mine_id(state.user_id):
self.wheel_timer.insert(
@@ -996,6 +1007,9 @@ class PresenceHandler(BasePresenceHandler):
# When overriding disabled presence, don't kick off all the
# wheel timers.
persist=not self._track_presence,
idle_timer=self._idle_timer,
sync_online_timeout=self._sync_online_timeout,
last_active_granularity=self._last_active_granularity,
)
if force_notify:
@@ -1108,6 +1122,9 @@ class PresenceHandler(BasePresenceHandler):
syncing_user_devices=syncing_user_devices,
user_to_devices=self._user_to_device_to_current_state,
now=now,
idle_timer=self._idle_timer,
sync_online_timeout=self._sync_online_timeout,
last_active_granularity=self._last_active_granularity,
)
return await self._update_states(changes)
@@ -1695,6 +1712,8 @@ def should_notify(
new_state: UserPresenceState,
is_mine: bool,
our_server_name: str,
*,
last_active_granularity: int,
) -> bool:
"""Decides if a presence state change should be sent to interested parties."""
user_location = "remote"
@@ -1741,7 +1760,7 @@ def should_notify(
if (
new_state.last_active_ts - old_state.last_active_ts
> LAST_ACTIVE_GRANULARITY
> last_active_granularity
):
# Only notify about last active bumps if we're not currently active
if not new_state.currently_active:
@@ -1752,7 +1771,7 @@ def should_notify(
).inc()
return True
elif new_state.last_active_ts - old_state.last_active_ts > LAST_ACTIVE_GRANULARITY:
elif new_state.last_active_ts - old_state.last_active_ts > last_active_granularity:
# Always notify for a transition where last active gets bumped.
notify_reason_counter.labels(
locality=user_location,
@@ -2075,6 +2094,10 @@ def handle_timeouts(
syncing_user_devices: AbstractSet[tuple[str, str | None]],
user_to_devices: dict[str, dict[str | None, UserDevicePresenceState]],
now: int,
*,
idle_timer: int,
sync_online_timeout: int,
last_active_granularity: int,
) -> list[UserPresenceState]:
"""Checks the presence of users that have timed out and updates as
appropriate.
@@ -2085,6 +2108,11 @@ def handle_timeouts(
syncing_user_devices: A set of (user ID, device ID) tuples with active syncs..
user_to_devices: A map of user ID to device ID to UserDevicePresenceState.
now: Current time in ms.
idle_timer: How long in ms before an inactive device is marked as idle.
sync_online_timeout: How long in ms after the last sync before a device
is marked as offline.
last_active_granularity: How long in ms a user counts as
"currently active" after their last activity.
Returns:
List of UserPresenceState updates
@@ -2101,6 +2129,9 @@ def handle_timeouts(
syncing_user_devices,
user_to_devices.get(user_id, {}),
now,
idle_timer=idle_timer,
sync_online_timeout=sync_online_timeout,
last_active_granularity=last_active_granularity,
)
if new_state:
changes[state.user_id] = new_state
@@ -2114,6 +2145,10 @@ def handle_timeout(
syncing_device_ids: AbstractSet[tuple[str, str | None]],
user_devices: dict[str | None, UserDevicePresenceState],
now: int,
*,
idle_timer: int,
sync_online_timeout: int,
last_active_granularity: int,
) -> UserPresenceState | None:
"""Checks the presence of the user to see if any of the timers have elapsed
@@ -2123,6 +2158,11 @@ def handle_timeout(
syncing_user_devices: A set of (user ID, device ID) tuples with active syncs..
user_devices: A map of device ID to UserDevicePresenceState.
now: Current time in ms.
idle_timer: How long in ms before an inactive device is marked as idle.
sync_online_timeout: How long in ms after the last sync before a device
is marked as offline.
last_active_granularity: How long in ms a user counts as
"currently active" after their last activity.
Returns:
A UserPresenceState update or None if no update.
@@ -2140,7 +2180,7 @@ def handle_timeout(
offline_devices = []
for device_id, device_state in user_devices.items():
if device_state.state == PresenceState.ONLINE:
if now - device_state.last_active_ts > IDLE_TIMER:
if now - device_state.last_active_ts > idle_timer:
# Currently online, but last activity ages ago so auto
# idle
device_state.state = PresenceState.UNAVAILABLE
@@ -2161,7 +2201,7 @@ def handle_timeout(
online_timeout = (
BUSY_ONLINE_TIMEOUT
if device_state.state == PresenceState.BUSY
else SYNC_ONLINE_TIMEOUT
else sync_online_timeout
)
if now - sync_or_active > online_timeout:
# Mark the device as going offline.
@@ -2180,7 +2220,7 @@ def handle_timeout(
state = state.copy_and_replace(state=new_presence)
changed = True
if now - state.last_active_ts > LAST_ACTIVE_GRANULARITY:
if now - state.last_active_ts > last_active_granularity:
# So that we send down a notification that we've
# stopped updating.
changed = True
@@ -2209,6 +2249,10 @@ def handle_update(
wheel_timer: WheelTimer,
now: int,
persist: bool,
*,
idle_timer: int,
sync_online_timeout: int,
last_active_granularity: int,
) -> tuple[UserPresenceState, bool, bool]:
"""Given a presence update:
1. Add any appropriate timers.
@@ -2223,6 +2267,11 @@ def handle_update(
now: Time now in ms
persist: True if this state should persist until another update occurs.
Skips insertion into wheel timers.
idle_timer: How long in ms before an inactive device is marked as idle.
sync_online_timeout: How long in ms after the last sync before a device
is marked as offline.
last_active_granularity: How long in ms a user counts as
"currently active" after their last activity.
Returns:
3-tuple: `(new_state, persist_and_notify, federation_ping)` where:
@@ -2242,17 +2291,17 @@ def handle_update(
# Idle timer
if not persist:
wheel_timer.insert(
now=now, obj=user_id, then=new_state.last_active_ts + IDLE_TIMER
now=now, obj=user_id, then=new_state.last_active_ts + idle_timer
)
active = now - new_state.last_active_ts < LAST_ACTIVE_GRANULARITY
active = now - new_state.last_active_ts < last_active_granularity
new_state = new_state.copy_and_replace(currently_active=active)
if active and not persist:
wheel_timer.insert(
now=now,
obj=user_id,
then=new_state.last_active_ts + LAST_ACTIVE_GRANULARITY,
then=new_state.last_active_ts + last_active_granularity,
)
if new_state.state != PresenceState.OFFLINE:
@@ -2261,7 +2310,7 @@ def handle_update(
wheel_timer.insert(
now=now,
obj=user_id,
then=new_state.last_user_sync_ts + SYNC_ONLINE_TIMEOUT,
then=new_state.last_user_sync_ts + sync_online_timeout,
)
last_federate = new_state.last_federation_update_ts
@@ -2287,7 +2336,13 @@ def handle_update(
)
# Check whether the change was something worth notifying about
if should_notify(prev_state, new_state, is_mine, our_server_name):
if should_notify(
prev_state,
new_state,
is_mine,
our_server_name,
last_active_granularity=last_active_granularity,
):
new_state = new_state.copy_and_replace(last_federation_update_ts=now)
persist_and_notify = True
+222 -37
View File
@@ -36,6 +36,11 @@ from synapse.api.presence import UserDevicePresenceState, UserPresenceState
from synapse.api.room_versions import (
RoomVersion,
)
from synapse.config.server import (
DEFAULT_IDLE_TIMER,
DEFAULT_LAST_ACTIVE_GRANULARITY,
DEFAULT_SYNC_ONLINE_TIMEOUT,
)
from synapse.crypto.event_signing import add_hashes_and_signatures
from synapse.events import EventBase, make_event_from_dict
from synapse.federation.sender import FederationSender
@@ -44,9 +49,6 @@ from synapse.handlers.presence import (
EXTERNAL_PROCESS_EXPIRY,
FEDERATION_PING_INTERVAL,
FEDERATION_TIMEOUT,
IDLE_TIMER,
LAST_ACTIVE_GRANULARITY,
SYNC_ONLINE_TIMEOUT,
PresenceHandler,
handle_timeout,
handle_update,
@@ -94,6 +96,9 @@ class PresenceUpdateTestCase(unittest.HomeserverTestCase):
wheel_timer=wheel_timer,
now=now,
persist=False,
idle_timer=DEFAULT_IDLE_TIMER,
sync_online_timeout=DEFAULT_SYNC_ONLINE_TIMEOUT,
last_active_granularity=DEFAULT_LAST_ACTIVE_GRANULARITY,
)
self.assertTrue(persist_and_notify)
@@ -105,16 +110,20 @@ class PresenceUpdateTestCase(unittest.HomeserverTestCase):
self.assertEqual(wheel_timer.insert.call_count, 3)
wheel_timer.insert.assert_has_calls(
[
call(now=now, obj=user_id, then=new_state.last_active_ts + IDLE_TIMER),
call(
now=now,
obj=user_id,
then=new_state.last_user_sync_ts + SYNC_ONLINE_TIMEOUT,
then=new_state.last_active_ts + DEFAULT_IDLE_TIMER,
),
call(
now=now,
obj=user_id,
then=new_state.last_active_ts + LAST_ACTIVE_GRANULARITY,
then=new_state.last_user_sync_ts + DEFAULT_SYNC_ONLINE_TIMEOUT,
),
call(
now=now,
obj=user_id,
then=new_state.last_active_ts + DEFAULT_LAST_ACTIVE_GRANULARITY,
),
],
any_order=True,
@@ -142,6 +151,9 @@ class PresenceUpdateTestCase(unittest.HomeserverTestCase):
wheel_timer=wheel_timer,
now=now,
persist=False,
idle_timer=DEFAULT_IDLE_TIMER,
sync_online_timeout=DEFAULT_SYNC_ONLINE_TIMEOUT,
last_active_granularity=DEFAULT_LAST_ACTIVE_GRANULARITY,
)
self.assertFalse(persist_and_notify)
@@ -154,16 +166,20 @@ class PresenceUpdateTestCase(unittest.HomeserverTestCase):
self.assertEqual(wheel_timer.insert.call_count, 3)
wheel_timer.insert.assert_has_calls(
[
call(now=now, obj=user_id, then=new_state.last_active_ts + IDLE_TIMER),
call(
now=now,
obj=user_id,
then=new_state.last_user_sync_ts + SYNC_ONLINE_TIMEOUT,
then=new_state.last_active_ts + DEFAULT_IDLE_TIMER,
),
call(
now=now,
obj=user_id,
then=new_state.last_active_ts + LAST_ACTIVE_GRANULARITY,
then=new_state.last_user_sync_ts + DEFAULT_SYNC_ONLINE_TIMEOUT,
),
call(
now=now,
obj=user_id,
then=new_state.last_active_ts + DEFAULT_LAST_ACTIVE_GRANULARITY,
),
],
any_order=True,
@@ -177,7 +193,7 @@ class PresenceUpdateTestCase(unittest.HomeserverTestCase):
prev_state = UserPresenceState.default(user_id)
prev_state = prev_state.copy_and_replace(
state=PresenceState.ONLINE,
last_active_ts=now - LAST_ACTIVE_GRANULARITY - 10,
last_active_ts=now - DEFAULT_LAST_ACTIVE_GRANULARITY - 10,
currently_active=True,
)
@@ -193,6 +209,9 @@ class PresenceUpdateTestCase(unittest.HomeserverTestCase):
wheel_timer=wheel_timer,
now=now,
persist=False,
idle_timer=DEFAULT_IDLE_TIMER,
sync_online_timeout=DEFAULT_SYNC_ONLINE_TIMEOUT,
last_active_granularity=DEFAULT_LAST_ACTIVE_GRANULARITY,
)
self.assertFalse(persist_and_notify)
@@ -205,16 +224,20 @@ class PresenceUpdateTestCase(unittest.HomeserverTestCase):
self.assertEqual(wheel_timer.insert.call_count, 3)
wheel_timer.insert.assert_has_calls(
[
call(now=now, obj=user_id, then=new_state.last_active_ts + IDLE_TIMER),
call(
now=now,
obj=user_id,
then=new_state.last_user_sync_ts + SYNC_ONLINE_TIMEOUT,
then=new_state.last_active_ts + DEFAULT_IDLE_TIMER,
),
call(
now=now,
obj=user_id,
then=new_state.last_active_ts + LAST_ACTIVE_GRANULARITY,
then=new_state.last_user_sync_ts + DEFAULT_SYNC_ONLINE_TIMEOUT,
),
call(
now=now,
obj=user_id,
then=new_state.last_active_ts + DEFAULT_LAST_ACTIVE_GRANULARITY,
),
],
any_order=True,
@@ -228,7 +251,7 @@ class PresenceUpdateTestCase(unittest.HomeserverTestCase):
prev_state = UserPresenceState.default(user_id)
prev_state = prev_state.copy_and_replace(
state=PresenceState.ONLINE,
last_active_ts=now - LAST_ACTIVE_GRANULARITY - 1,
last_active_ts=now - DEFAULT_LAST_ACTIVE_GRANULARITY - 1,
currently_active=True,
)
@@ -242,6 +265,9 @@ class PresenceUpdateTestCase(unittest.HomeserverTestCase):
wheel_timer=wheel_timer,
now=now,
persist=False,
idle_timer=DEFAULT_IDLE_TIMER,
sync_online_timeout=DEFAULT_SYNC_ONLINE_TIMEOUT,
last_active_granularity=DEFAULT_LAST_ACTIVE_GRANULARITY,
)
self.assertTrue(persist_and_notify)
@@ -253,11 +279,15 @@ class PresenceUpdateTestCase(unittest.HomeserverTestCase):
self.assertEqual(wheel_timer.insert.call_count, 2)
wheel_timer.insert.assert_has_calls(
[
call(now=now, obj=user_id, then=new_state.last_active_ts + IDLE_TIMER),
call(
now=now,
obj=user_id,
then=new_state.last_user_sync_ts + SYNC_ONLINE_TIMEOUT,
then=new_state.last_active_ts + DEFAULT_IDLE_TIMER,
),
call(
now=now,
obj=user_id,
then=new_state.last_user_sync_ts + DEFAULT_SYNC_ONLINE_TIMEOUT,
),
],
any_order=True,
@@ -283,6 +313,9 @@ class PresenceUpdateTestCase(unittest.HomeserverTestCase):
wheel_timer=wheel_timer,
now=now,
persist=False,
idle_timer=DEFAULT_IDLE_TIMER,
sync_online_timeout=DEFAULT_SYNC_ONLINE_TIMEOUT,
last_active_granularity=DEFAULT_LAST_ACTIVE_GRANULARITY,
)
self.assertFalse(persist_and_notify)
@@ -323,6 +356,9 @@ class PresenceUpdateTestCase(unittest.HomeserverTestCase):
wheel_timer=wheel_timer,
now=now,
persist=False,
idle_timer=DEFAULT_IDLE_TIMER,
sync_online_timeout=DEFAULT_SYNC_ONLINE_TIMEOUT,
last_active_granularity=DEFAULT_LAST_ACTIVE_GRANULARITY,
)
self.assertTrue(persist_and_notify)
@@ -351,6 +387,9 @@ class PresenceUpdateTestCase(unittest.HomeserverTestCase):
wheel_timer=wheel_timer,
now=now,
persist=False,
idle_timer=DEFAULT_IDLE_TIMER,
sync_online_timeout=DEFAULT_SYNC_ONLINE_TIMEOUT,
last_active_granularity=DEFAULT_LAST_ACTIVE_GRANULARITY,
)
self.assertTrue(persist_and_notify)
@@ -365,7 +404,7 @@ class PresenceUpdateTestCase(unittest.HomeserverTestCase):
call(
now=now,
obj=user_id,
then=new_state.last_user_sync_ts + SYNC_ONLINE_TIMEOUT,
then=new_state.last_user_sync_ts + DEFAULT_SYNC_ONLINE_TIMEOUT,
)
],
any_order=True,
@@ -442,6 +481,9 @@ class PresenceUpdateTestCase(unittest.HomeserverTestCase):
wheel_timer=wheel_timer,
now=now,
persist=True,
idle_timer=DEFAULT_IDLE_TIMER,
sync_online_timeout=DEFAULT_SYNC_ONLINE_TIMEOUT,
last_active_granularity=DEFAULT_LAST_ACTIVE_GRANULARITY,
)
wheel_timer.insert.assert_not_called()
@@ -506,6 +548,9 @@ class PresenceUpdateTestCase(unittest.HomeserverTestCase):
wheel_timer=wheel_timer,
now=now,
persist=False,
idle_timer=DEFAULT_IDLE_TIMER,
sync_online_timeout=DEFAULT_SYNC_ONLINE_TIMEOUT,
last_active_granularity=DEFAULT_LAST_ACTIVE_GRANULARITY,
)
# Check that the user is offline.
@@ -556,7 +601,7 @@ class PresenceTimeoutTestCase(unittest.TestCase):
state = UserPresenceState.default(user_id)
state = state.copy_and_replace(
state=PresenceState.ONLINE,
last_active_ts=now - IDLE_TIMER - 1,
last_active_ts=now - DEFAULT_IDLE_TIMER - 1,
last_user_sync_ts=now,
status_msg=status_msg,
)
@@ -574,6 +619,9 @@ class PresenceTimeoutTestCase(unittest.TestCase):
syncing_device_ids=set(),
user_devices={device_id: device_state},
now=now,
idle_timer=DEFAULT_IDLE_TIMER,
sync_online_timeout=DEFAULT_SYNC_ONLINE_TIMEOUT,
last_active_granularity=DEFAULT_LAST_ACTIVE_GRANULARITY,
)
self.assertIsNotNone(new_state)
@@ -594,7 +642,7 @@ class PresenceTimeoutTestCase(unittest.TestCase):
state = UserPresenceState.default(user_id)
state = state.copy_and_replace(
state=PresenceState.BUSY,
last_active_ts=now - IDLE_TIMER - 1,
last_active_ts=now - DEFAULT_IDLE_TIMER - 1,
last_user_sync_ts=now,
status_msg=status_msg,
)
@@ -612,6 +660,9 @@ class PresenceTimeoutTestCase(unittest.TestCase):
syncing_device_ids=set(),
user_devices={device_id: device_state},
now=now,
idle_timer=DEFAULT_IDLE_TIMER,
sync_online_timeout=DEFAULT_SYNC_ONLINE_TIMEOUT,
last_active_granularity=DEFAULT_LAST_ACTIVE_GRANULARITY,
)
self.assertIsNotNone(new_state)
@@ -629,7 +680,7 @@ class PresenceTimeoutTestCase(unittest.TestCase):
state = state.copy_and_replace(
state=PresenceState.ONLINE,
last_active_ts=0,
last_user_sync_ts=now - SYNC_ONLINE_TIMEOUT - 1,
last_user_sync_ts=now - DEFAULT_SYNC_ONLINE_TIMEOUT - 1,
status_msg=status_msg,
)
device_state = UserDevicePresenceState(
@@ -646,6 +697,9 @@ class PresenceTimeoutTestCase(unittest.TestCase):
syncing_device_ids=set(),
user_devices={device_id: device_state},
now=now,
idle_timer=DEFAULT_IDLE_TIMER,
sync_online_timeout=DEFAULT_SYNC_ONLINE_TIMEOUT,
last_active_granularity=DEFAULT_LAST_ACTIVE_GRANULARITY,
)
self.assertIsNotNone(new_state)
@@ -662,8 +716,8 @@ class PresenceTimeoutTestCase(unittest.TestCase):
state = UserPresenceState.default(user_id)
state = state.copy_and_replace(
state=PresenceState.ONLINE,
last_active_ts=now - SYNC_ONLINE_TIMEOUT - 1,
last_user_sync_ts=now - SYNC_ONLINE_TIMEOUT - 1,
last_active_ts=now - DEFAULT_SYNC_ONLINE_TIMEOUT - 1,
last_user_sync_ts=now - DEFAULT_SYNC_ONLINE_TIMEOUT - 1,
status_msg=status_msg,
)
device_state = UserDevicePresenceState(
@@ -680,6 +734,9 @@ class PresenceTimeoutTestCase(unittest.TestCase):
syncing_device_ids={(user_id, device_id)},
user_devices={device_id: device_state},
now=now,
idle_timer=DEFAULT_IDLE_TIMER,
sync_online_timeout=DEFAULT_SYNC_ONLINE_TIMEOUT,
last_active_granularity=DEFAULT_LAST_ACTIVE_GRANULARITY,
)
self.assertIsNotNone(new_state)
@@ -715,6 +772,9 @@ class PresenceTimeoutTestCase(unittest.TestCase):
syncing_device_ids=set(),
user_devices={device_id: device_state},
now=now,
idle_timer=DEFAULT_IDLE_TIMER,
sync_online_timeout=DEFAULT_SYNC_ONLINE_TIMEOUT,
last_active_granularity=DEFAULT_LAST_ACTIVE_GRANULARITY,
)
self.assertIsNotNone(new_state)
@@ -746,6 +806,9 @@ class PresenceTimeoutTestCase(unittest.TestCase):
syncing_device_ids=set(),
user_devices={device_id: device_state},
now=now,
idle_timer=DEFAULT_IDLE_TIMER,
sync_online_timeout=DEFAULT_SYNC_ONLINE_TIMEOUT,
last_active_granularity=DEFAULT_LAST_ACTIVE_GRANULARITY,
)
self.assertIsNone(new_state)
@@ -766,7 +829,14 @@ class PresenceTimeoutTestCase(unittest.TestCase):
# Note that this is a remote user so we do not have their device information.
new_state = handle_timeout(
state, is_mine=False, syncing_device_ids=set(), user_devices={}, now=now
state,
is_mine=False,
syncing_device_ids=set(),
user_devices={},
now=now,
idle_timer=DEFAULT_IDLE_TIMER,
sync_online_timeout=DEFAULT_SYNC_ONLINE_TIMEOUT,
last_active_granularity=DEFAULT_LAST_ACTIVE_GRANULARITY,
)
self.assertIsNotNone(new_state)
@@ -783,7 +853,7 @@ class PresenceTimeoutTestCase(unittest.TestCase):
state = UserPresenceState.default(user_id)
state = state.copy_and_replace(
state=PresenceState.ONLINE,
last_active_ts=now - LAST_ACTIVE_GRANULARITY - 1,
last_active_ts=now - DEFAULT_LAST_ACTIVE_GRANULARITY - 1,
last_user_sync_ts=now,
last_federation_update_ts=now,
status_msg=status_msg,
@@ -802,6 +872,9 @@ class PresenceTimeoutTestCase(unittest.TestCase):
syncing_device_ids=set(),
user_devices={device_id: device_state},
now=now,
idle_timer=DEFAULT_IDLE_TIMER,
sync_online_timeout=DEFAULT_SYNC_ONLINE_TIMEOUT,
last_active_granularity=DEFAULT_LAST_ACTIVE_GRANULARITY,
)
self.assertIsNotNone(new_state)
@@ -860,7 +933,7 @@ class PresenceHandlerInitTestCase(unittest.HomeserverTestCase):
self.assertEqual(state.state, PresenceState.ONLINE)
# Advance such that the user should timeout.
self.reactor.advance(SYNC_ONLINE_TIMEOUT / 1000)
self.reactor.advance(DEFAULT_SYNC_ONLINE_TIMEOUT / 1000)
self.reactor.pump([5])
# Check that the user is now offline.
@@ -900,7 +973,7 @@ class PresenceHandlerInitTestCase(unittest.HomeserverTestCase):
self.assertEqual(state.state, PresenceState.ONLINE)
# Advance slightly and sync.
self.reactor.advance(SYNC_ONLINE_TIMEOUT / 1000 / 2)
self.reactor.advance(DEFAULT_SYNC_ONLINE_TIMEOUT / 1000 / 2)
self.get_success(
presence_handler.user_syncing(
self.user_id,
@@ -917,7 +990,7 @@ class PresenceHandlerInitTestCase(unittest.HomeserverTestCase):
self.assertEqual(state.state, expected_state)
# Advance such that the user's preloaded data times out, but not the new sync.
self.reactor.advance(SYNC_ONLINE_TIMEOUT / 1000 / 2)
self.reactor.advance(DEFAULT_SYNC_ONLINE_TIMEOUT / 1000 / 2)
self.reactor.pump([5])
# Check that the user is in the sync state (as the client is currently syncing still).
@@ -927,6 +1000,118 @@ class PresenceHandlerInitTestCase(unittest.HomeserverTestCase):
self.assertEqual(state.state, sync_state)
# Timer values used by `PresenceConfigurableTimersTestCase`, all larger than
# the corresponding defaults.
_CUSTOM_TIMERS_CONFIG = {
"presence": {
"last_active_granularity": "2m",
"sync_online_timeout": "3m",
"idle_timeout": "20m",
}
}
class PresenceConfigurableTimersTestCase(unittest.HomeserverTestCase):
"""Tests that the presence state machine timers can be changed via the
`presence` config section.
Each test checks that nothing happens where the default timer would have
fired, and that the transition then occurs once the configured timer
elapses.
"""
device_id = "dev-1"
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
self.presence_handler = hs.get_presence_handler()
self.user_id = f"@test:{hs.config.server.server_name}"
self.user_id_obj = UserID.from_string(self.user_id)
def _get_state(self) -> UserPresenceState:
return self.get_success(self.presence_handler.get_state(self.user_id_obj))
@override_config(_CUSTOM_TIMERS_CONFIG)
def test_config_parsing(self) -> None:
config = self.hs.config.server
self.assertEqual(config.presence_last_active_granularity, 2 * 60 * 1000)
self.assertEqual(config.presence_sync_online_timeout, 3 * 60 * 1000)
self.assertEqual(config.presence_idle_timeout, 20 * 60 * 1000)
@override_config(_CUSTOM_TIMERS_CONFIG)
def test_sync_online_timeout(self) -> None:
"""A user only goes offline once the configured sync timeout passes."""
with self.get_success(
self.presence_handler.user_syncing(
self.user_id, self.device_id, True, PresenceState.ONLINE
)
):
pass
self.assertEqual(self._get_state().state, PresenceState.ONLINE)
# Well past the DEFAULT_SYNC_ONLINE_TIMEOUT, but short of the
# configured 3m: still online.
self.reactor.advance(2 * DEFAULT_SYNC_ONLINE_TIMEOUT / 1000)
self.reactor.pump([5])
self.assertEqual(self._get_state().state, PresenceState.ONLINE)
# Past the configured timeout: offline.
self.reactor.advance(3 * 60)
self.reactor.pump([5])
self.assertEqual(self._get_state().state, PresenceState.OFFLINE)
@override_config(_CUSTOM_TIMERS_CONFIG)
def test_idle_timeout(self) -> None:
"""A continuously syncing but inactive user only goes idle once the
configured idle timeout passes."""
# Leave the sync open so the device never times out.
self.get_success(
self.presence_handler.user_syncing(
self.user_id, self.device_id, True, PresenceState.ONLINE
)
)
# Well past the DEFAULT_IDLE_TIMER, but short of the configured 20m:
# still online.
self.reactor.advance(2 * DEFAULT_IDLE_TIMER / 1000)
self.reactor.pump([5])
self.assertEqual(self._get_state().state, PresenceState.ONLINE)
# Past the configured timeout: idle.
self.reactor.advance(15 * 60)
self.reactor.pump([5])
self.assertEqual(self._get_state().state, PresenceState.UNAVAILABLE)
@override_config(_CUSTOM_TIMERS_CONFIG)
def test_last_active_granularity(self) -> None:
"""A user remains "currently active" for the configured duration
after their last activity."""
with self.get_success(
self.presence_handler.user_syncing(
self.user_id, self.device_id, True, PresenceState.ONLINE
)
):
pass
self.assertTrue(self._get_state().currently_active)
# Past the DEFAULT_LAST_ACTIVE_GRANULARITY, but short of the
# configured 2m: still currently active.
self.reactor.advance(90)
self.reactor.pump([5])
state = self._get_state()
self.assertEqual(state.state, PresenceState.ONLINE)
self.assertTrue(state.currently_active)
# Past the configured granularity (but short of the 3m sync timeout):
# no longer currently active, but still online.
self.reactor.advance(60)
self.reactor.pump([5])
state = self._get_state()
self.assertEqual(state.state, PresenceState.ONLINE)
self.assertFalse(state.currently_active)
class PresenceHandlerTestCase(BaseMultiWorkerStreamTestCase):
user_id = "@test:server"
user_id_obj = UserID.from_string(user_id)
@@ -979,14 +1164,14 @@ class PresenceHandlerTestCase(BaseMultiWorkerStreamTestCase):
# Check that if we wait a while without telling the handler the user has
# stopped syncing that their presence state doesn't get timed out.
self.reactor.advance(SYNC_ONLINE_TIMEOUT / 2)
self.reactor.advance(DEFAULT_SYNC_ONLINE_TIMEOUT / 2)
state = self.get_success(self.presence_handler.get_state(self.user_id_obj))
self.assertEqual(state.state, PresenceState.ONLINE)
self.assertEqual(state.status_msg, status_msg)
# Check that if the timeout fires, then the syncing user gets timed out
self.reactor.advance(SYNC_ONLINE_TIMEOUT)
self.reactor.advance(DEFAULT_SYNC_ONLINE_TIMEOUT)
state = self.get_success(self.presence_handler.get_state(self.user_id_obj))
# status_msg should remain even after going offline
@@ -1273,7 +1458,7 @@ class PresenceHandlerTestCase(BaseMultiWorkerStreamTestCase):
)
# 2. Wait half the idle timer.
self.reactor.advance(IDLE_TIMER / 1000 / 2)
self.reactor.advance(DEFAULT_IDLE_TIMER / 1000 / 2)
self.reactor.pump([0.1])
# 3. Sync with the second device.
@@ -1300,7 +1485,7 @@ class PresenceHandlerTestCase(BaseMultiWorkerStreamTestCase):
# When testing with workers, make another random sync (with any *different*
# user) to keep the process information from expiring.
#
# This is due to EXTERNAL_PROCESS_EXPIRY being equivalent to IDLE_TIMER.
# This is due to EXTERNAL_PROCESS_EXPIRY being equivalent to DEFAULT_IDLE_TIMER.
if test_with_workers:
with self.get_success(
worker_presence_handler.user_syncing(
@@ -1314,7 +1499,7 @@ class PresenceHandlerTestCase(BaseMultiWorkerStreamTestCase):
# 5. Advance such that the first device should be discarded (the idle timer),
# then pump so _handle_timeouts function to called.
self.reactor.advance(IDLE_TIMER / 1000 / 2)
self.reactor.advance(DEFAULT_IDLE_TIMER / 1000 / 2)
self.reactor.pump([0.01])
# 6. Assert the expected presence state.
@@ -1330,7 +1515,7 @@ class PresenceHandlerTestCase(BaseMultiWorkerStreamTestCase):
# 7. Advance such that the second device should be discarded (half the idle timer),
# then pump so _handle_timeouts function to called.
self.reactor.advance(IDLE_TIMER / 1000 / 2)
self.reactor.advance(DEFAULT_IDLE_TIMER / 1000 / 2)
self.reactor.pump([0.1])
# 8. The devices are still "syncing" (the sync context managers were never
@@ -1533,7 +1718,7 @@ class PresenceHandlerTestCase(BaseMultiWorkerStreamTestCase):
# 5. Advance such that the first device should be discarded (the sync timeout),
# then pump so _handle_timeouts function to called.
self.reactor.advance(SYNC_ONLINE_TIMEOUT / 1000)
self.reactor.advance(DEFAULT_SYNC_ONLINE_TIMEOUT / 1000)
self.reactor.pump([5])
# 6. Assert the expected presence state.
@@ -1556,7 +1741,7 @@ class PresenceHandlerTestCase(BaseMultiWorkerStreamTestCase):
if dev_1_state == PresenceState.BUSY or dev_2_state == PresenceState.BUSY:
timeout = BUSY_ONLINE_TIMEOUT
else:
timeout = SYNC_ONLINE_TIMEOUT
timeout = DEFAULT_SYNC_ONLINE_TIMEOUT
self.reactor.advance(timeout / 1000)
self.reactor.pump([5])
@@ -1629,7 +1814,7 @@ class PresenceHandlerTestCase(BaseMultiWorkerStreamTestCase):
# Advance such that the device would be discarded if it was not busy,
# then pump so _handle_timeouts function to called.
self.reactor.advance(IDLE_TIMER / 1000)
self.reactor.advance(DEFAULT_IDLE_TIMER / 1000)
self.reactor.pump([5])
# The account should still be busy.
@@ -1682,7 +1867,7 @@ class PresenceHandlerTestCase(BaseMultiWorkerStreamTestCase):
self.assertEqual(state.state, PresenceState.ONLINE)
# The timeout should not fire and the state should be the same.
self.reactor.advance(SYNC_ONLINE_TIMEOUT)
self.reactor.advance(DEFAULT_SYNC_ONLINE_TIMEOUT)
state = self.get_success(self.presence_handler.get_state(self.user_id_obj))
self.assertEqual(state.state, PresenceState.ONLINE)