Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit 1bf1436

Browse files
authored
Combine logic about not overriding BUSY presence. (#16170)
Simplify some of the presence code by reducing duplicated code between worker & non-worker modes. The main change is to push some of the logic from `user_syncing` into `set_state`. This is done by passing whether the user is setting the presence via a `/sync` with a new `is_sync` flag to `set_state`. If this is `true` some additional logic is performed: * Don't override `busy` presence. * Update the `last_user_sync_ts`. * Never update the status message.
1 parent 501da8e commit 1bf1436

File tree

4 files changed

+99
-104
lines changed

4 files changed

+99
-104
lines changed

changelog.d/16170.misc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Simplify presence code when using workers.

synapse/handlers/presence.py

Lines changed: 63 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -151,15 +151,13 @@ def __init__(self, hs: "HomeServer"):
151151

152152
self._federation_queue = PresenceFederationQueue(hs, self)
153153

154-
self._busy_presence_enabled = hs.config.experimental.msc3026_enabled
155-
156154
self.VALID_PRESENCE: Tuple[str, ...] = (
157155
PresenceState.ONLINE,
158156
PresenceState.UNAVAILABLE,
159157
PresenceState.OFFLINE,
160158
)
161159

162-
if self._busy_presence_enabled:
160+
if hs.config.experimental.msc3026_enabled:
163161
self.VALID_PRESENCE += (PresenceState.BUSY,)
164162

165163
active_presence = self.store.take_presence_startup_info()
@@ -255,17 +253,19 @@ async def set_state(
255253
self,
256254
target_user: UserID,
257255
state: JsonDict,
258-
ignore_status_msg: bool = False,
259256
force_notify: bool = False,
257+
is_sync: bool = False,
260258
) -> None:
261259
"""Set the presence state of the user.
262260
263261
Args:
264262
target_user: The ID of the user to set the presence state of.
265263
state: The presence state as a JSON dictionary.
266-
ignore_status_msg: True to ignore the "status_msg" field of the `state` dict.
267-
If False, the user's current status will be updated.
268264
force_notify: Whether to force notification of the update to clients.
265+
is_sync: True if this update was from a sync, which results in
266+
*not* overriding a previously set BUSY status, updating the
267+
user's last_user_sync_ts, and ignoring the "status_msg" field of
268+
the `state` dict.
269269
"""
270270

271271
@abc.abstractmethod
@@ -491,23 +491,18 @@ async def user_syncing(
491491
if not affect_presence or not self._presence_enabled:
492492
return _NullContextManager()
493493

494-
prev_state = await self.current_state_for_user(user_id)
495-
if prev_state.state != PresenceState.BUSY:
496-
# We set state here but pass ignore_status_msg = True as we don't want to
497-
# cause the status message to be cleared.
498-
# Note that this causes last_active_ts to be incremented which is not
499-
# what the spec wants: see comment in the BasePresenceHandler version
500-
# of this function.
501-
await self.set_state(
502-
UserID.from_string(user_id),
503-
{"presence": presence_state},
504-
ignore_status_msg=True,
505-
)
494+
# Note that this causes last_active_ts to be incremented which is not
495+
# what the spec wants.
496+
await self.set_state(
497+
UserID.from_string(user_id),
498+
state={"presence": presence_state},
499+
is_sync=True,
500+
)
506501

507502
curr_sync = self._user_to_num_current_syncs.get(user_id, 0)
508503
self._user_to_num_current_syncs[user_id] = curr_sync + 1
509504

510-
# If we went from no in flight sync to some, notify replication
505+
# If this is the first in-flight sync, notify replication
511506
if self._user_to_num_current_syncs[user_id] == 1:
512507
self.mark_as_coming_online(user_id)
513508

@@ -518,7 +513,7 @@ def _end() -> None:
518513
if user_id in self._user_to_num_current_syncs:
519514
self._user_to_num_current_syncs[user_id] -= 1
520515

521-
# If we went from one in flight sync to non, notify replication
516+
# If there are no more in-flight syncs, notify replication
522517
if self._user_to_num_current_syncs[user_id] == 0:
523518
self.mark_as_going_offline(user_id)
524519

@@ -598,17 +593,19 @@ async def set_state(
598593
self,
599594
target_user: UserID,
600595
state: JsonDict,
601-
ignore_status_msg: bool = False,
602596
force_notify: bool = False,
597+
is_sync: bool = False,
603598
) -> None:
604599
"""Set the presence state of the user.
605600
606601
Args:
607602
target_user: The ID of the user to set the presence state of.
608603
state: The presence state as a JSON dictionary.
609-
ignore_status_msg: True to ignore the "status_msg" field of the `state` dict.
610-
If False, the user's current status will be updated.
611604
force_notify: Whether to force notification of the update to clients.
605+
is_sync: True if this update was from a sync, which results in
606+
*not* overriding a previously set BUSY status, updating the
607+
user's last_user_sync_ts, and ignoring the "status_msg" field of
608+
the `state` dict.
612609
"""
613610
presence = state["presence"]
614611

@@ -626,8 +623,8 @@ async def set_state(
626623
instance_name=self._presence_writer_instance,
627624
user_id=user_id,
628625
state=state,
629-
ignore_status_msg=ignore_status_msg,
630626
force_notify=force_notify,
627+
is_sync=is_sync,
631628
)
632629

633630
async def bump_presence_active_time(self, user: UserID) -> None:
@@ -992,45 +989,13 @@ async def user_syncing(
992989
curr_sync = self.user_to_num_current_syncs.get(user_id, 0)
993990
self.user_to_num_current_syncs[user_id] = curr_sync + 1
994991

995-
prev_state = await self.current_state_for_user(user_id)
996-
997-
# If they're busy then they don't stop being busy just by syncing,
998-
# so just update the last sync time.
999-
if prev_state.state != PresenceState.BUSY:
1000-
# XXX: We set_state separately here and just update the last_active_ts above
1001-
# This keeps the logic as similar as possible between the worker and single
1002-
# process modes. Using set_state will actually cause last_active_ts to be
1003-
# updated always, which is not what the spec calls for, but synapse has done
1004-
# this for... forever, I think.
1005-
await self.set_state(
1006-
UserID.from_string(user_id),
1007-
{"presence": presence_state},
1008-
ignore_status_msg=True,
1009-
)
1010-
# Retrieve the new state for the logic below. This should come from the
1011-
# in-memory cache.
1012-
prev_state = await self.current_state_for_user(user_id)
1013-
1014-
# To keep the single process behaviour consistent with worker mode, run the
1015-
# same logic as `update_external_syncs_row`, even though it looks weird.
1016-
if prev_state.state == PresenceState.OFFLINE:
1017-
await self._update_states(
1018-
[
1019-
prev_state.copy_and_replace(
1020-
state=PresenceState.ONLINE,
1021-
last_active_ts=self.clock.time_msec(),
1022-
last_user_sync_ts=self.clock.time_msec(),
1023-
)
1024-
]
1025-
)
1026-
# otherwise, set the new presence state & update the last sync time,
1027-
# but don't update last_active_ts as this isn't an indication that
1028-
# they've been active (even though it's probably been updated by
1029-
# set_state above)
1030-
else:
1031-
await self._update_states(
1032-
[prev_state.copy_and_replace(last_user_sync_ts=self.clock.time_msec())]
1033-
)
992+
# Note that this causes last_active_ts to be incremented which is not
993+
# what the spec wants.
994+
await self.set_state(
995+
UserID.from_string(user_id),
996+
state={"presence": presence_state},
997+
is_sync=True,
998+
)
1034999

10351000
async def _end() -> None:
10361001
try:
@@ -1080,32 +1045,27 @@ async def update_external_syncs_row(
10801045
process_id, set()
10811046
)
10821047

1083-
updates = []
1048+
# USER_SYNC is sent when a user starts or stops syncing on a remote
1049+
# process. (But only for the initial and last device.)
1050+
#
1051+
# When a user *starts* syncing it also calls set_state(...) which
1052+
# will update the state, last_active_ts, and last_user_sync_ts.
1053+
# Simply ensure the user is tracked as syncing in this case.
1054+
#
1055+
# When a user *stops* syncing, update the last_user_sync_ts and mark
1056+
# them as no longer syncing. Note this doesn't quite match the
1057+
# monolith behaviour, which updates last_user_sync_ts at the end of
1058+
# every sync, not just the last in-flight sync.
10841059
if is_syncing and user_id not in process_presence:
1085-
if prev_state.state == PresenceState.OFFLINE:
1086-
updates.append(
1087-
prev_state.copy_and_replace(
1088-
state=PresenceState.ONLINE,
1089-
last_active_ts=sync_time_msec,
1090-
last_user_sync_ts=sync_time_msec,
1091-
)
1092-
)
1093-
else:
1094-
updates.append(
1095-
prev_state.copy_and_replace(last_user_sync_ts=sync_time_msec)
1096-
)
10971060
process_presence.add(user_id)
1098-
elif user_id in process_presence:
1099-
updates.append(
1100-
prev_state.copy_and_replace(last_user_sync_ts=sync_time_msec)
1061+
elif not is_syncing and user_id in process_presence:
1062+
new_state = prev_state.copy_and_replace(
1063+
last_user_sync_ts=sync_time_msec
11011064
)
1065+
await self._update_states([new_state])
11021066

1103-
if not is_syncing:
11041067
process_presence.discard(user_id)
11051068

1106-
if updates:
1107-
await self._update_states(updates)
1108-
11091069
self.external_process_last_updated_ms[process_id] = self.clock.time_msec()
11101070

11111071
async def update_external_syncs_clear(self, process_id: str) -> None:
@@ -1204,17 +1164,19 @@ async def set_state(
12041164
self,
12051165
target_user: UserID,
12061166
state: JsonDict,
1207-
ignore_status_msg: bool = False,
12081167
force_notify: bool = False,
1168+
is_sync: bool = False,
12091169
) -> None:
12101170
"""Set the presence state of the user.
12111171
12121172
Args:
12131173
target_user: The ID of the user to set the presence state of.
12141174
state: The presence state as a JSON dictionary.
1215-
ignore_status_msg: True to ignore the "status_msg" field of the `state` dict.
1216-
If False, the user's current status will be updated.
12171175
force_notify: Whether to force notification of the update to clients.
1176+
is_sync: True if this update was from a sync, which results in
1177+
*not* overriding a previously set BUSY status, updating the
1178+
user's last_user_sync_ts, and ignoring the "status_msg" field of
1179+
the `state` dict.
12181180
"""
12191181
status_msg = state.get("status_msg", None)
12201182
presence = state["presence"]
@@ -1227,18 +1189,27 @@ async def set_state(
12271189
return
12281190

12291191
user_id = target_user.to_string()
1192+
now = self.clock.time_msec()
12301193

12311194
prev_state = await self.current_state_for_user(user_id)
12321195

1196+
# Syncs do not override a previous presence of busy.
1197+
#
1198+
# TODO: This is a hack for lack of multi-device support. Unfortunately
1199+
# removing this requires coordination with clients.
1200+
if prev_state.state == PresenceState.BUSY and is_sync:
1201+
presence = PresenceState.BUSY
1202+
12331203
new_fields = {"state": presence}
12341204

1235-
if not ignore_status_msg:
1236-
new_fields["status_msg"] = status_msg
1205+
if presence == PresenceState.ONLINE or presence == PresenceState.BUSY:
1206+
new_fields["last_active_ts"] = now
12371207

1238-
if presence == PresenceState.ONLINE or (
1239-
presence == PresenceState.BUSY and self._busy_presence_enabled
1240-
):
1241-
new_fields["last_active_ts"] = self.clock.time_msec()
1208+
if is_sync:
1209+
new_fields["last_user_sync_ts"] = now
1210+
else:
1211+
# Syncs do not override the status message.
1212+
new_fields["status_msg"] = status_msg
12421213

12431214
await self._update_states(
12441215
[prev_state.copy_and_replace(**new_fields)], force_notify=force_notify

synapse/replication/http/presence.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ class ReplicationPresenceSetState(ReplicationEndpoint):
7373
7474
{
7575
"state": { ... },
76-
"ignore_status_msg": false,
77-
"force_notify": false
76+
"force_notify": false,
77+
"is_sync": false
7878
}
7979
8080
200 OK
@@ -96,13 +96,13 @@ def __init__(self, hs: "HomeServer"):
9696
async def _serialize_payload( # type: ignore[override]
9797
user_id: str,
9898
state: JsonDict,
99-
ignore_status_msg: bool = False,
10099
force_notify: bool = False,
100+
is_sync: bool = False,
101101
) -> JsonDict:
102102
return {
103103
"state": state,
104-
"ignore_status_msg": ignore_status_msg,
105104
"force_notify": force_notify,
105+
"is_sync": is_sync,
106106
}
107107

108108
async def _handle_request( # type: ignore[override]
@@ -111,8 +111,8 @@ async def _handle_request( # type: ignore[override]
111111
await self._presence_handler.set_state(
112112
UserID.from_string(user_id),
113113
content["state"],
114-
content["ignore_status_msg"],
115114
content["force_notify"],
115+
content.get("is_sync", False),
116116
)
117117

118118
return (200, {})

tests/handlers/test_presence.py

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -641,13 +641,20 @@ def test_external_process_timeout(self) -> None:
641641
"""Test that if an external process doesn't update the records for a while
642642
we time out their syncing users presence.
643643
"""
644-
process_id = "1"
645644

646-
# Notify handler that a user is now syncing.
645+
# Create a worker and use it to handle /sync traffic instead.
646+
# This is used to test that presence changes get replicated from workers
647+
# to the main process correctly.
648+
worker_to_sync_against = self.make_worker_hs(
649+
"synapse.app.generic_worker", {"worker_name": "synchrotron"}
650+
)
651+
worker_presence_handler = worker_to_sync_against.get_presence_handler()
652+
647653
self.get_success(
648-
self.presence_handler.update_external_syncs_row(
649-
process_id, self.user_id, True, self.clock.time_msec()
650-
)
654+
worker_presence_handler.user_syncing(
655+
self.user_id, True, PresenceState.ONLINE
656+
),
657+
by=0.1,
651658
)
652659

653660
# Check that if we wait a while without telling the handler the user has
@@ -820,7 +827,7 @@ def test_set_presence_from_syncing_keeps_busy(
820827
# This is used to test that presence changes get replicated from workers
821828
# to the main process correctly.
822829
worker_to_sync_against = self.make_worker_hs(
823-
"synapse.app.generic_worker", {"worker_name": "presence_writer"}
830+
"synapse.app.generic_worker", {"worker_name": "synchrotron"}
824831
)
825832

826833
# Set presence to BUSY
@@ -832,14 +839,30 @@ def test_set_presence_from_syncing_keeps_busy(
832839
self.get_success(
833840
worker_to_sync_against.get_presence_handler().user_syncing(
834841
self.user_id, True, PresenceState.ONLINE
835-
)
842+
),
843+
by=0.1,
836844
)
837845

838846
# Check against the main process that the user's presence did not change.
839847
state = self.get_success(self.presence_handler.get_state(self.user_id_obj))
840848
# we should still be busy
841849
self.assertEqual(state.state, PresenceState.BUSY)
842850

851+
# Advance such that the device would be discarded if it was not busy,
852+
# then pump so _handle_timeouts function to called.
853+
self.reactor.advance(IDLE_TIMER / 1000)
854+
self.reactor.pump([5])
855+
856+
# The account should still be busy.
857+
state = self.get_success(self.presence_handler.get_state(self.user_id_obj))
858+
self.assertEqual(state.state, PresenceState.BUSY)
859+
860+
# Ensure that a /presence call can set the user *off* busy.
861+
self._set_presencestate_with_status_msg(PresenceState.ONLINE, status_msg)
862+
863+
state = self.get_success(self.presence_handler.get_state(self.user_id_obj))
864+
self.assertEqual(state.state, PresenceState.ONLINE)
865+
843866
def _set_presencestate_with_status_msg(
844867
self, state: str, status_msg: Optional[str]
845868
) -> None:

0 commit comments

Comments
 (0)