def get_states(self, target_user_ids, as_event=False): """Get the presence state for users. Args: target_user_ids (list) as_event (bool): Whether to format it as a client event or not. Returns: list """ updates = yield self.current_state_for_users(target_user_ids) updates = updates.values() for user_id in set(target_user_ids) - set(u.user_id for u in updates): updates.append(UserPresenceState.default(user_id)) now = self.clock.time_msec() if as_event: defer.returnValue([ { "type": "m.presence", "content": _format_user_presence_state(state, now), } for state in updates ]) else: defer.returnValue([ _format_user_presence_state(state, now) for state in updates ])
def test_remote_ping_timer(self): wheel_timer = Mock() user_id = "@foo:bar" now = 5000000 prev_state = UserPresenceState.default(user_id) prev_state = prev_state.copy_and_replace( state=PresenceState.ONLINE, last_active_ts=now, ) new_state = prev_state.copy_and_replace( state=PresenceState.ONLINE, ) state, persist_and_notify, federation_ping = handle_update( prev_state, new_state, is_mine=False, wheel_timer=wheel_timer, now=now ) self.assertFalse(persist_and_notify) self.assertFalse(federation_ping) self.assertFalse(state.currently_active) self.assertEquals(new_state.state, state.state) self.assertEquals(new_state.status_msg, state.status_msg) self.assertEquals(wheel_timer.insert.call_count, 1) wheel_timer.insert.assert_has_calls([ call( now=now, obj=user_id, then=new_state.last_federation_update_ts + FEDERATION_TIMEOUT ), ], any_order=True)
def _handle_timeouts(self): """Checks the presence of users that have timed out and updates as appropriate. """ now = self.clock.time_msec() with Measure(self.clock, "presence_handle_timeouts"): # Fetch the list of users that *may* have timed out. Things may have # changed since the timeout was set, so we won't necessarily have to # take any action. users_to_check = self.wheel_timer.fetch(now) states = [ self.user_to_current_state.get( user_id, UserPresenceState.default(user_id) ) for user_id in set(users_to_check) ] timers_fired_counter.inc_by(len(states)) changes = handle_timeouts( states, is_mine_fn=self.hs.is_mine_id, user_to_num_current_syncs=self.user_to_num_current_syncs, now=now, ) preserve_fn(self._update_states)(changes)
def current_state_for_users(self, user_ids): """Get the current presence state for multiple users. Returns: dict: `user_id` -> `UserPresenceState` """ states = { user_id: self.user_to_current_state.get(user_id, None) for user_id in user_ids } missing = [user_id for user_id, state in states.items() if not state] if missing: # There are things not in our in memory cache. Lets pull them out of # the database. res = yield self.store.get_presence_for_users(missing) states.update({state.user_id: state for state in res}) missing = [user_id for user_id, state in states.items() if not state] if missing: new = { user_id: UserPresenceState.default(user_id) for user_id in missing } states.update(new) self.user_to_current_state.update(new) defer.returnValue(states)
def test_online_to_idle(self): wheel_timer = Mock() user_id = "@foo:bar" now = 5000000 prev_state = UserPresenceState.default(user_id) prev_state = prev_state.copy_and_replace( state=PresenceState.ONLINE, last_active_ts=now, currently_active=True, ) new_state = prev_state.copy_and_replace( state=PresenceState.UNAVAILABLE, ) state, persist_and_notify, federation_ping = handle_update( prev_state, new_state, is_mine=True, wheel_timer=wheel_timer, now=now ) self.assertTrue(persist_and_notify) self.assertEquals(new_state.state, state.state) self.assertEquals(state.last_federation_update_ts, now) self.assertEquals(new_state.state, state.state) self.assertEquals(new_state.status_msg, state.status_msg) self.assertEquals(wheel_timer.insert.call_count, 1) wheel_timer.insert.assert_has_calls([ call( now=now, obj=user_id, then=new_state.last_user_sync_ts + SYNC_ONLINE_TIMEOUT ) ], any_order=True)
def test_online_to_offline(self): wheel_timer = Mock() user_id = "@foo:bar" now = 5000000 prev_state = UserPresenceState.default(user_id) prev_state = prev_state.copy_and_replace( state=PresenceState.ONLINE, last_active_ts=now, currently_active=True, ) new_state = prev_state.copy_and_replace( state=PresenceState.OFFLINE, ) state, persist_and_notify, federation_ping = handle_update( prev_state, new_state, is_mine=True, wheel_timer=wheel_timer, now=now ) self.assertTrue(persist_and_notify) self.assertEquals(new_state.state, state.state) self.assertEquals(state.last_federation_update_ts, now) self.assertEquals(wheel_timer.insert.call_count, 0)
def test_no_timeout(self): user_id = "@foo:bar" now = 5000000 state = UserPresenceState.default(user_id) state = state.copy_and_replace( state=PresenceState.ONLINE, last_active_ts=now, last_user_sync_ts=now, last_federation_update_ts=now, ) new_state = handle_timeout(state, is_mine=True, syncing_user_ids=set(), now=now) self.assertIsNone(new_state)
def test_sync_timeout(self): user_id = "@foo:bar" now = 5000000 state = UserPresenceState.default(user_id) state = state.copy_and_replace( state=PresenceState.ONLINE, last_active_ts=0, last_user_sync_ts=now - SYNC_ONLINE_TIMEOUT - 1, ) new_state = handle_timeout(state, is_mine=True, syncing_user_ids=set(), now=now) self.assertIsNotNone(new_state) self.assertEquals(new_state.state, PresenceState.OFFLINE)
def _handle_timeouts(self): """Checks the presence of users that have timed out and updates as appropriate. """ logger.info("Handling presence timeouts") now = self.clock.time_msec() try: with Measure(self.clock, "presence_handle_timeouts"): # Fetch the list of users that *may* have timed out. Things may have # changed since the timeout was set, so we won't necessarily have to # take any action. users_to_check = set(self.wheel_timer.fetch(now)) # Check whether the lists of syncing processes from an external # process have expired. expired_process_ids = [ process_id for process_id, last_update in self.external_process_last_updated_ms.items() if now - last_update > EXTERNAL_PROCESS_EXPIRY ] for process_id in expired_process_ids: users_to_check.update( self.external_process_last_updated_ms.pop(process_id, ()) ) self.external_process_last_update.pop(process_id) states = [ self.user_to_current_state.get( user_id, UserPresenceState.default(user_id) ) for user_id in users_to_check ] timers_fired_counter.inc_by(len(states)) changes = handle_timeouts( states, is_mine_fn=self.is_mine_id, syncing_user_ids=self.get_currently_syncing_users(), now=now, ) preserve_fn(self._update_states)(changes) except: logger.exception("Exception in _handle_timeouts loop")
def test_idle_timer(self): user_id = "@foo:bar" now = 5000000 state = UserPresenceState.default(user_id) state = state.copy_and_replace( state=PresenceState.ONLINE, last_active_ts=now - IDLE_TIMER - 1, last_user_sync_ts=now, ) new_state = handle_timeout( state, is_mine=True, user_to_num_current_syncs={}, now=now ) self.assertIsNotNone(new_state) self.assertEquals(new_state.state, PresenceState.UNAVAILABLE)
def test_last_active(self): user_id = "@foo:bar" now = 5000000 state = UserPresenceState.default(user_id) state = state.copy_and_replace( state=PresenceState.ONLINE, last_active_ts=now - LAST_ACTIVE_GRANULARITY - 1, last_user_sync_ts=now, last_federation_update_ts=now, ) new_state = handle_timeout( state, is_mine=True, user_to_num_current_syncs={}, now=now ) self.assertIsNotNone(new_state) self.assertEquals(state, new_state)
def test_federation_timeout(self): user_id = "@foo:bar" now = 5000000 state = UserPresenceState.default(user_id) state = state.copy_and_replace( state=PresenceState.ONLINE, last_active_ts=now, last_user_sync_ts=now, last_federation_update_ts=now - FEDERATION_TIMEOUT - 1, ) new_state = handle_timeout( state, is_mine=False, user_to_num_current_syncs={}, now=now ) self.assertIsNotNone(new_state) self.assertEquals(new_state.state, PresenceState.OFFLINE)
def test_online_to_online_last_active_noop(self): wheel_timer = Mock() user_id = "@foo:bar" now = 5000000 prev_state = UserPresenceState.default(user_id) prev_state = prev_state.copy_and_replace( state=PresenceState.ONLINE, last_active_ts=now - LAST_ACTIVE_GRANULARITY - 10, currently_active=True, ) new_state = prev_state.copy_and_replace( state=PresenceState.ONLINE, last_active_ts=now ) state, persist_and_notify, federation_ping = handle_update( prev_state, new_state, is_mine=True, wheel_timer=wheel_timer, now=now ) self.assertFalse(persist_and_notify) self.assertTrue(federation_ping) self.assertTrue(state.currently_active) self.assertEquals(new_state.state, state.state) self.assertEquals(new_state.status_msg, state.status_msg) self.assertEquals(state.last_federation_update_ts, now) self.assertEquals(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, ), call( now=now, obj=user_id, then=new_state.last_active_ts + LAST_ACTIVE_GRANULARITY, ), ], any_order=True, )
def test_sync_online(self): user_id = "@foo:bar" now = 5000000 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, ) new_state = handle_timeout( state, is_mine=True, user_to_num_current_syncs={ user_id: 1, }, now=now ) self.assertIsNotNone(new_state) self.assertEquals(new_state.state, PresenceState.ONLINE)
def process_replication_rows(self, token, rows): states = [ UserPresenceState( row.user_id, row.state, row.last_active_ts, row.last_federation_update_ts, row.last_user_sync_ts, row.status_msg, row.currently_active, ) for row in rows ] for state in states: self.user_to_current_state[state.user_id] = state stream_id = token yield self.notify_from_replication(states, stream_id)
async def _handle_timeouts(self): """Checks the presence of users that have timed out and updates as appropriate. """ logger.debug("Handling presence timeouts") now = self.clock.time_msec() # Fetch the list of users that *may* have timed out. Things may have # changed since the timeout was set, so we won't necessarily have to # take any action. users_to_check = set(self.wheel_timer.fetch(now)) # Check whether the lists of syncing processes from an external # process have expired. expired_process_ids = [ process_id for process_id, last_update in self.external_process_last_updated_ms.items() if now - last_update > EXTERNAL_PROCESS_EXPIRY ] for process_id in expired_process_ids: # For each expired process drop tracking info and check the users # that were syncing on that process to see if they need to be timed # out. users_to_check.update( self.external_process_to_current_syncs.pop(process_id, ())) self.external_process_last_updated_ms.pop(process_id) states = [ self.user_to_current_state.get(user_id, UserPresenceState.default(user_id)) for user_id in users_to_check ] timers_fired_counter.inc(len(states)) changes = handle_timeouts( states, is_mine_fn=self.is_mine_id, syncing_user_ids=self.get_currently_syncing_users(), now=now, ) return await self._update_states(changes)
def test_last_active(self): user_id = "@foo:bar" now = 5000000 state = UserPresenceState.default(user_id) state = state.copy_and_replace( state=PresenceState.ONLINE, last_active_ts=now - LAST_ACTIVE_GRANULARITY - 1, last_user_sync_ts=now, last_federation_update_ts=now, ) new_state = handle_timeout(state, is_mine=True, syncing_user_ids=set(), now=now) self.assertIsNotNone(new_state) self.assertEquals(state, new_state)
def test_federation_ping(self): user_id = "@foo:bar" now = 5000000 state = UserPresenceState.default(user_id) state = state.copy_and_replace( state=PresenceState.ONLINE, last_active_ts=now, last_user_sync_ts=now, last_federation_update_ts=now - FEDERATION_PING_INTERVAL - 1, ) new_state = handle_timeout(state, is_mine=True, syncing_user_ids=set(), now=now) self.assertIsNotNone(new_state) self.assertEquals(new_state, new_state)
def test_offline_to_online(self): wheel_timer = Mock() user_id = "@foo:bar" now = 5000000 prev_state = UserPresenceState.default(user_id) new_state = prev_state.copy_and_replace(state=PresenceState.ONLINE, last_active_ts=now) state, persist_and_notify, federation_ping = handle_update( prev_state, new_state, is_mine=True, wheel_timer=wheel_timer, now=now) self.assertTrue(persist_and_notify) self.assertTrue(state.currently_active) self.assertEquals(new_state.state, state.state) self.assertEquals(new_state.status_msg, state.status_msg) self.assertEquals(state.last_federation_update_ts, now) self.assertEquals(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, ), call( now=now, obj=user_id, then=new_state.last_active_ts + LAST_ACTIVE_GRANULARITY, ), ], any_order=True, )
def get_presence_for_users(self, user_ids): rows = yield self.db_pool.simple_select_many_batch( table="presence_stream", column="user_id", iterable=user_ids, keyvalues={}, retcols=( "user_id", "state", "last_active_ts", "last_federation_update_ts", "last_user_sync_ts", "status_msg", "currently_active", ), desc="get_presence_for_users", ) for row in rows: row["currently_active"] = bool(row["currently_active"]) return {row["user_id"]: UserPresenceState(**row) for row in rows}
def test_online_to_offline(self): wheel_timer = Mock() user_id = "@foo:bar" now = 5000000 prev_state = UserPresenceState.default(user_id) prev_state = prev_state.copy_and_replace(state=PresenceState.ONLINE, last_active_ts=now, currently_active=True) new_state = prev_state.copy_and_replace(state=PresenceState.OFFLINE) state, persist_and_notify, federation_ping = handle_update( prev_state, new_state, is_mine=True, wheel_timer=wheel_timer, now=now) self.assertTrue(persist_and_notify) self.assertEquals(new_state.state, state.state) self.assertEquals(state.last_federation_update_ts, now) self.assertEquals(wheel_timer.insert.call_count, 0)
def test_remote_ping_timer(self): wheel_timer = Mock() user_id = "@foo:bar" now = 5000000 prev_state = UserPresenceState.default(user_id) prev_state = prev_state.copy_and_replace(state=PresenceState.ONLINE, last_active_ts=now) new_state = prev_state.copy_and_replace(state=PresenceState.ONLINE) state, persist_and_notify, federation_ping = handle_update( prev_state, new_state, is_mine=False, wheel_timer=wheel_timer, now=now) self.assertFalse(persist_and_notify) self.assertFalse(federation_ping) self.assertFalse(state.currently_active) self.assertEquals(new_state.state, state.state) self.assertEquals(new_state.status_msg, state.status_msg) self.assertEquals(wheel_timer.insert.call_count, 1) wheel_timer.insert.assert_has_calls( [ call( now=now, obj=user_id, then=new_state.last_federation_update_ts + FEDERATION_TIMEOUT, ) ], any_order=True, )
async def _update_states(self, new_states): """Updates presence of users. Sets the appropriate timeouts. Pokes the notifier and federation if and only if the changed presence state should be sent to clients/servers. """ now = self.clock.time_msec() with Measure(self.clock, "presence_update_states"): # NOTE: We purposefully don't await between now and when we've # calculated what we want to do with the new states, to avoid races. to_notify = {} # Changes we want to notify everyone about to_federation_ping = {} # These need sending keep-alives # Only bother handling the last presence change for each user new_states_dict = {} for new_state in new_states: new_states_dict[new_state.user_id] = new_state new_state = new_states_dict.values() for new_state in new_states: user_id = new_state.user_id # Its fine to not hit the database here, as the only thing not in # the current state cache are OFFLINE states, where the only field # of interest is last_active which is safe enough to assume is 0 # here. prev_state = self.user_to_current_state.get( user_id, UserPresenceState.default(user_id) ) new_state, should_notify, should_ping = handle_update( prev_state, new_state, is_mine=self.is_mine_id(user_id), wheel_timer=self.wheel_timer, now=now, ) self.user_to_current_state[user_id] = new_state if should_notify: to_notify[user_id] = new_state elif should_ping: to_federation_ping[user_id] = new_state # TODO: We should probably ensure there are no races hereafter presence_updates_counter.inc(len(new_states)) if to_notify: notified_presence_counter.inc(len(to_notify)) await self._persist_and_notify(list(to_notify.values())) self.unpersisted_users_changes |= {s.user_id for s in new_states} self.unpersisted_users_changes -= set(to_notify.keys()) to_federation_ping = { user_id: state for user_id, state in to_federation_ping.items() if user_id not in to_notify } if to_federation_ping: federation_presence_out_counter.inc(len(to_federation_ping)) self._push_to_remotes(to_federation_ping.values())
def from_data(data): return PresenceRow(state=UserPresenceState.from_dict(data))
def from_data(data): return PresenceDestinationsRow( state=UserPresenceState.from_dict(data["state"]), destinations=data["dests"] )
def from_data(data): return PresenceDestinationsRow( state=UserPresenceState.from_dict(data["state"]), destinations=data["dests"], )
def _update_states(self, new_states): """Updates presence of users. Sets the appropriate timeouts. Pokes the notifier and federation if and only if the changed presence state should be sent to clients/servers. """ now = self.clock.time_msec() with Measure(self.clock, "presence_update_states"): # NOTE: We purposefully don't yield between now and when we've # calculated what we want to do with the new states, to avoid races. to_notify = {} # Changes we want to notify everyone about to_federation_ping = {} # These need sending keep-alives for new_state in new_states: user_id = new_state.user_id # Its fine to not hit the database here, as the only thing not in # the current state cache are OFFLINE states, where the only field # of interest is last_active which is safe enough to assume is 0 # here. prev_state = self.user_to_current_state.get( user_id, UserPresenceState.default(user_id) ) new_state, should_notify, should_ping = handle_update( prev_state, new_state, is_mine=self.hs.is_mine_id(user_id), wheel_timer=self.wheel_timer, now=now ) self.user_to_current_state[user_id] = new_state if should_notify: to_notify[user_id] = new_state elif should_ping: to_federation_ping[user_id] = new_state # TODO: We should probably ensure there are no races hereafter presence_updates_counter.inc_by(len(new_states)) if to_notify: notified_presence_counter.inc_by(len(to_notify)) yield self._persist_and_notify(to_notify.values()) self.unpersisted_users_changes |= set(s.user_id for s in new_states) self.unpersisted_users_changes -= set(to_notify.keys()) to_federation_ping = { user_id: state for user_id, state in to_federation_ping.items() if user_id not in to_notify } if to_federation_ping: federation_presence_out_counter.inc_by(len(to_federation_ping)) _, _, hosts_to_states = yield self._get_interested_parties( to_federation_ping.values() ) self._push_to_remotes(hosts_to_states)
def from_data(data): return PresenceRow( state=UserPresenceState.from_dict(data) )
def process_replication(self, result): # The federation stream contains things that we want to send out, e.g. # presence, typing, etc. fed_stream = result.get("federation") if fed_stream: latest_id = int(fed_stream["position"]) # The federation stream containis a bunch of different types of # rows that need to be handled differently. We parse the rows, put # them into the appropriate collection and then send them off. presence_to_send = {} keyed_edus = {} edus = {} failures = {} device_destinations = set() # Parse the rows in the stream for row in fed_stream["rows"]: position, typ, content_js = row content = json.loads(content_js) if typ == send_queue.PRESENCE_TYPE: destination = content["destination"] state = UserPresenceState.from_dict(content["state"]) presence_to_send.setdefault(destination, []).append(state) elif typ == send_queue.KEYED_EDU_TYPE: key = content["key"] edu = Edu(**content["edu"]) keyed_edus.setdefault(edu.destination, {})[(edu.destination, tuple(key))] = edu elif typ == send_queue.EDU_TYPE: edu = Edu(**content) edus.setdefault(edu.destination, []).append(edu) elif typ == send_queue.FAILURE_TYPE: destination = content["destination"] failure = content["failure"] failures.setdefault(destination, []).append(failure) elif typ == send_queue.DEVICE_MESSAGE_TYPE: device_destinations.add(content["destination"]) else: raise Exception("Unrecognised federation type: %r", typ) # We've finished collecting, send everything off for destination, states in presence_to_send.items(): self.federation_sender.send_presence(destination, states) for destination, edu_map in keyed_edus.items(): for key, edu in edu_map.items(): self.federation_sender.send_edu( edu.destination, edu.edu_type, edu.content, key=key, ) for destination, edu_list in edus.items(): for edu in edu_list: self.federation_sender.send_edu( edu.destination, edu.edu_type, edu.content, key=None, ) for destination, failure_list in failures.items(): for failure in failure_list: self.federation_sender.send_failure(destination, failure) for destination in device_destinations: self.federation_sender.send_device_messages(destination) # Record where we are in the stream. yield self.store.update_federation_out_pos("federation", latest_id) # We also need to poke the federation sender when new events happen event_stream = result.get("events") if event_stream: latest_pos = event_stream["position"] self.federation_sender.notify_new_events(latest_pos)