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.
This commit is contained in:
Patrick Cloke 2023-08-28 11:03:23 -04:00 committed by GitHub
parent 501da8ecd8
commit 1bf143699c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 99 additions and 104 deletions

1
changelog.d/16170.misc Normal file
View File

@ -0,0 +1 @@
Simplify presence code when using workers.

View File

@ -151,15 +151,13 @@ class BasePresenceHandler(abc.ABC):
self._federation_queue = PresenceFederationQueue(hs, self) self._federation_queue = PresenceFederationQueue(hs, self)
self._busy_presence_enabled = hs.config.experimental.msc3026_enabled
self.VALID_PRESENCE: Tuple[str, ...] = ( self.VALID_PRESENCE: Tuple[str, ...] = (
PresenceState.ONLINE, PresenceState.ONLINE,
PresenceState.UNAVAILABLE, PresenceState.UNAVAILABLE,
PresenceState.OFFLINE, PresenceState.OFFLINE,
) )
if self._busy_presence_enabled: if hs.config.experimental.msc3026_enabled:
self.VALID_PRESENCE += (PresenceState.BUSY,) self.VALID_PRESENCE += (PresenceState.BUSY,)
active_presence = self.store.take_presence_startup_info() active_presence = self.store.take_presence_startup_info()
@ -255,17 +253,19 @@ class BasePresenceHandler(abc.ABC):
self, self,
target_user: UserID, target_user: UserID,
state: JsonDict, state: JsonDict,
ignore_status_msg: bool = False,
force_notify: bool = False, force_notify: bool = False,
is_sync: bool = False,
) -> None: ) -> None:
"""Set the presence state of the user. """Set the presence state of the user.
Args: Args:
target_user: The ID of the user to set the presence state of. target_user: The ID of the user to set the presence state of.
state: The presence state as a JSON dictionary. state: The presence state as a JSON dictionary.
ignore_status_msg: True to ignore the "status_msg" field of the `state` dict.
If False, the user's current status will be updated.
force_notify: Whether to force notification of the update to clients. force_notify: Whether to force notification of the update to clients.
is_sync: True if this update was from a sync, which results in
*not* overriding a previously set BUSY status, updating the
user's last_user_sync_ts, and ignoring the "status_msg" field of
the `state` dict.
""" """
@abc.abstractmethod @abc.abstractmethod
@ -491,23 +491,18 @@ class WorkerPresenceHandler(BasePresenceHandler):
if not affect_presence or not self._presence_enabled: if not affect_presence or not self._presence_enabled:
return _NullContextManager() return _NullContextManager()
prev_state = await self.current_state_for_user(user_id) # Note that this causes last_active_ts to be incremented which is not
if prev_state.state != PresenceState.BUSY: # what the spec wants.
# We set state here but pass ignore_status_msg = True as we don't want to await self.set_state(
# cause the status message to be cleared. UserID.from_string(user_id),
# Note that this causes last_active_ts to be incremented which is not state={"presence": presence_state},
# what the spec wants: see comment in the BasePresenceHandler version is_sync=True,
# of this function. )
await self.set_state(
UserID.from_string(user_id),
{"presence": presence_state},
ignore_status_msg=True,
)
curr_sync = self._user_to_num_current_syncs.get(user_id, 0) curr_sync = self._user_to_num_current_syncs.get(user_id, 0)
self._user_to_num_current_syncs[user_id] = curr_sync + 1 self._user_to_num_current_syncs[user_id] = curr_sync + 1
# If we went from no in flight sync to some, notify replication # If this is the first in-flight sync, notify replication
if self._user_to_num_current_syncs[user_id] == 1: if self._user_to_num_current_syncs[user_id] == 1:
self.mark_as_coming_online(user_id) self.mark_as_coming_online(user_id)
@ -518,7 +513,7 @@ class WorkerPresenceHandler(BasePresenceHandler):
if user_id in self._user_to_num_current_syncs: if user_id in self._user_to_num_current_syncs:
self._user_to_num_current_syncs[user_id] -= 1 self._user_to_num_current_syncs[user_id] -= 1
# If we went from one in flight sync to non, notify replication # If there are no more in-flight syncs, notify replication
if self._user_to_num_current_syncs[user_id] == 0: if self._user_to_num_current_syncs[user_id] == 0:
self.mark_as_going_offline(user_id) self.mark_as_going_offline(user_id)
@ -598,17 +593,19 @@ class WorkerPresenceHandler(BasePresenceHandler):
self, self,
target_user: UserID, target_user: UserID,
state: JsonDict, state: JsonDict,
ignore_status_msg: bool = False,
force_notify: bool = False, force_notify: bool = False,
is_sync: bool = False,
) -> None: ) -> None:
"""Set the presence state of the user. """Set the presence state of the user.
Args: Args:
target_user: The ID of the user to set the presence state of. target_user: The ID of the user to set the presence state of.
state: The presence state as a JSON dictionary. state: The presence state as a JSON dictionary.
ignore_status_msg: True to ignore the "status_msg" field of the `state` dict.
If False, the user's current status will be updated.
force_notify: Whether to force notification of the update to clients. force_notify: Whether to force notification of the update to clients.
is_sync: True if this update was from a sync, which results in
*not* overriding a previously set BUSY status, updating the
user's last_user_sync_ts, and ignoring the "status_msg" field of
the `state` dict.
""" """
presence = state["presence"] presence = state["presence"]
@ -626,8 +623,8 @@ class WorkerPresenceHandler(BasePresenceHandler):
instance_name=self._presence_writer_instance, instance_name=self._presence_writer_instance,
user_id=user_id, user_id=user_id,
state=state, state=state,
ignore_status_msg=ignore_status_msg,
force_notify=force_notify, force_notify=force_notify,
is_sync=is_sync,
) )
async def bump_presence_active_time(self, user: UserID) -> None: async def bump_presence_active_time(self, user: UserID) -> None:
@ -992,45 +989,13 @@ class PresenceHandler(BasePresenceHandler):
curr_sync = self.user_to_num_current_syncs.get(user_id, 0) curr_sync = self.user_to_num_current_syncs.get(user_id, 0)
self.user_to_num_current_syncs[user_id] = curr_sync + 1 self.user_to_num_current_syncs[user_id] = curr_sync + 1
prev_state = await self.current_state_for_user(user_id) # Note that this causes last_active_ts to be incremented which is not
# what the spec wants.
# If they're busy then they don't stop being busy just by syncing, await self.set_state(
# so just update the last sync time. UserID.from_string(user_id),
if prev_state.state != PresenceState.BUSY: state={"presence": presence_state},
# XXX: We set_state separately here and just update the last_active_ts above is_sync=True,
# This keeps the logic as similar as possible between the worker and single )
# process modes. Using set_state will actually cause last_active_ts to be
# updated always, which is not what the spec calls for, but synapse has done
# this for... forever, I think.
await self.set_state(
UserID.from_string(user_id),
{"presence": presence_state},
ignore_status_msg=True,
)
# Retrieve the new state for the logic below. This should come from the
# in-memory cache.
prev_state = await self.current_state_for_user(user_id)
# To keep the single process behaviour consistent with worker mode, run the
# same logic as `update_external_syncs_row`, even though it looks weird.
if prev_state.state == PresenceState.OFFLINE:
await self._update_states(
[
prev_state.copy_and_replace(
state=PresenceState.ONLINE,
last_active_ts=self.clock.time_msec(),
last_user_sync_ts=self.clock.time_msec(),
)
]
)
# otherwise, set the new presence state & update the last sync time,
# but don't update last_active_ts as this isn't an indication that
# they've been active (even though it's probably been updated by
# set_state above)
else:
await self._update_states(
[prev_state.copy_and_replace(last_user_sync_ts=self.clock.time_msec())]
)
async def _end() -> None: async def _end() -> None:
try: try:
@ -1080,32 +1045,27 @@ class PresenceHandler(BasePresenceHandler):
process_id, set() process_id, set()
) )
updates = [] # USER_SYNC is sent when a user starts or stops syncing on a remote
# process. (But only for the initial and last device.)
#
# When a user *starts* syncing it also calls set_state(...) which
# will update the state, last_active_ts, and last_user_sync_ts.
# Simply ensure the user is tracked as syncing in this case.
#
# When a user *stops* syncing, update the last_user_sync_ts and mark
# them as no longer syncing. Note this doesn't quite match the
# monolith behaviour, which updates last_user_sync_ts at the end of
# every sync, not just the last in-flight sync.
if is_syncing and user_id not in process_presence: if is_syncing and user_id not in process_presence:
if prev_state.state == PresenceState.OFFLINE:
updates.append(
prev_state.copy_and_replace(
state=PresenceState.ONLINE,
last_active_ts=sync_time_msec,
last_user_sync_ts=sync_time_msec,
)
)
else:
updates.append(
prev_state.copy_and_replace(last_user_sync_ts=sync_time_msec)
)
process_presence.add(user_id) process_presence.add(user_id)
elif user_id in process_presence: elif not is_syncing and user_id in process_presence:
updates.append( new_state = prev_state.copy_and_replace(
prev_state.copy_and_replace(last_user_sync_ts=sync_time_msec) last_user_sync_ts=sync_time_msec
) )
await self._update_states([new_state])
if not is_syncing:
process_presence.discard(user_id) process_presence.discard(user_id)
if updates:
await self._update_states(updates)
self.external_process_last_updated_ms[process_id] = self.clock.time_msec() self.external_process_last_updated_ms[process_id] = self.clock.time_msec()
async def update_external_syncs_clear(self, process_id: str) -> None: async def update_external_syncs_clear(self, process_id: str) -> None:
@ -1204,17 +1164,19 @@ class PresenceHandler(BasePresenceHandler):
self, self,
target_user: UserID, target_user: UserID,
state: JsonDict, state: JsonDict,
ignore_status_msg: bool = False,
force_notify: bool = False, force_notify: bool = False,
is_sync: bool = False,
) -> None: ) -> None:
"""Set the presence state of the user. """Set the presence state of the user.
Args: Args:
target_user: The ID of the user to set the presence state of. target_user: The ID of the user to set the presence state of.
state: The presence state as a JSON dictionary. state: The presence state as a JSON dictionary.
ignore_status_msg: True to ignore the "status_msg" field of the `state` dict.
If False, the user's current status will be updated.
force_notify: Whether to force notification of the update to clients. force_notify: Whether to force notification of the update to clients.
is_sync: True if this update was from a sync, which results in
*not* overriding a previously set BUSY status, updating the
user's last_user_sync_ts, and ignoring the "status_msg" field of
the `state` dict.
""" """
status_msg = state.get("status_msg", None) status_msg = state.get("status_msg", None)
presence = state["presence"] presence = state["presence"]
@ -1227,18 +1189,27 @@ class PresenceHandler(BasePresenceHandler):
return return
user_id = target_user.to_string() user_id = target_user.to_string()
now = self.clock.time_msec()
prev_state = await self.current_state_for_user(user_id) prev_state = await self.current_state_for_user(user_id)
# Syncs do not override a previous presence of busy.
#
# TODO: This is a hack for lack of multi-device support. Unfortunately
# removing this requires coordination with clients.
if prev_state.state == PresenceState.BUSY and is_sync:
presence = PresenceState.BUSY
new_fields = {"state": presence} new_fields = {"state": presence}
if not ignore_status_msg: if presence == PresenceState.ONLINE or presence == PresenceState.BUSY:
new_fields["status_msg"] = status_msg new_fields["last_active_ts"] = now
if presence == PresenceState.ONLINE or ( if is_sync:
presence == PresenceState.BUSY and self._busy_presence_enabled new_fields["last_user_sync_ts"] = now
): else:
new_fields["last_active_ts"] = self.clock.time_msec() # Syncs do not override the status message.
new_fields["status_msg"] = status_msg
await self._update_states( await self._update_states(
[prev_state.copy_and_replace(**new_fields)], force_notify=force_notify [prev_state.copy_and_replace(**new_fields)], force_notify=force_notify

View File

@ -73,8 +73,8 @@ class ReplicationPresenceSetState(ReplicationEndpoint):
{ {
"state": { ... }, "state": { ... },
"ignore_status_msg": false, "force_notify": false,
"force_notify": false "is_sync": false
} }
200 OK 200 OK
@ -96,13 +96,13 @@ class ReplicationPresenceSetState(ReplicationEndpoint):
async def _serialize_payload( # type: ignore[override] async def _serialize_payload( # type: ignore[override]
user_id: str, user_id: str,
state: JsonDict, state: JsonDict,
ignore_status_msg: bool = False,
force_notify: bool = False, force_notify: bool = False,
is_sync: bool = False,
) -> JsonDict: ) -> JsonDict:
return { return {
"state": state, "state": state,
"ignore_status_msg": ignore_status_msg,
"force_notify": force_notify, "force_notify": force_notify,
"is_sync": is_sync,
} }
async def _handle_request( # type: ignore[override] async def _handle_request( # type: ignore[override]
@ -111,8 +111,8 @@ class ReplicationPresenceSetState(ReplicationEndpoint):
await self._presence_handler.set_state( await self._presence_handler.set_state(
UserID.from_string(user_id), UserID.from_string(user_id),
content["state"], content["state"],
content["ignore_status_msg"],
content["force_notify"], content["force_notify"],
content.get("is_sync", False),
) )
return (200, {}) return (200, {})

View File

@ -641,13 +641,20 @@ class PresenceHandlerTestCase(BaseMultiWorkerStreamTestCase):
"""Test that if an external process doesn't update the records for a while """Test that if an external process doesn't update the records for a while
we time out their syncing users presence. we time out their syncing users presence.
""" """
process_id = "1"
# Notify handler that a user is now syncing. # Create a worker and use it to handle /sync traffic instead.
# This is used to test that presence changes get replicated from workers
# to the main process correctly.
worker_to_sync_against = self.make_worker_hs(
"synapse.app.generic_worker", {"worker_name": "synchrotron"}
)
worker_presence_handler = worker_to_sync_against.get_presence_handler()
self.get_success( self.get_success(
self.presence_handler.update_external_syncs_row( worker_presence_handler.user_syncing(
process_id, self.user_id, True, self.clock.time_msec() self.user_id, True, PresenceState.ONLINE
) ),
by=0.1,
) )
# Check that if we wait a while without telling the handler the user has # Check that if we wait a while without telling the handler the user has
@ -820,7 +827,7 @@ class PresenceHandlerTestCase(BaseMultiWorkerStreamTestCase):
# This is used to test that presence changes get replicated from workers # This is used to test that presence changes get replicated from workers
# to the main process correctly. # to the main process correctly.
worker_to_sync_against = self.make_worker_hs( worker_to_sync_against = self.make_worker_hs(
"synapse.app.generic_worker", {"worker_name": "presence_writer"} "synapse.app.generic_worker", {"worker_name": "synchrotron"}
) )
# Set presence to BUSY # Set presence to BUSY
@ -832,7 +839,8 @@ class PresenceHandlerTestCase(BaseMultiWorkerStreamTestCase):
self.get_success( self.get_success(
worker_to_sync_against.get_presence_handler().user_syncing( worker_to_sync_against.get_presence_handler().user_syncing(
self.user_id, True, PresenceState.ONLINE self.user_id, True, PresenceState.ONLINE
) ),
by=0.1,
) )
# Check against the main process that the user's presence did not change. # Check against the main process that the user's presence did not change.
@ -840,6 +848,21 @@ class PresenceHandlerTestCase(BaseMultiWorkerStreamTestCase):
# we should still be busy # we should still be busy
self.assertEqual(state.state, PresenceState.BUSY) self.assertEqual(state.state, PresenceState.BUSY)
# 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.pump([5])
# The account should still be busy.
state = self.get_success(self.presence_handler.get_state(self.user_id_obj))
self.assertEqual(state.state, PresenceState.BUSY)
# Ensure that a /presence call can set the user *off* busy.
self._set_presencestate_with_status_msg(PresenceState.ONLINE, status_msg)
state = self.get_success(self.presence_handler.get_state(self.user_id_obj))
self.assertEqual(state.state, PresenceState.ONLINE)
def _set_presencestate_with_status_msg( def _set_presencestate_with_status_msg(
self, state: str, status_msg: Optional[str] self, state: str, status_msg: Optional[str]
) -> None: ) -> None: