Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 
 
 
 
 

308 lignes
13 KiB

  1. # Copyright 2018-2021 The Matrix.org Foundation C.I.C.
  2. # Copyright 2020 Sorunome
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. import logging
  16. from collections import Counter
  17. from typing import (
  18. TYPE_CHECKING,
  19. Any,
  20. Counter as CounterType,
  21. Dict,
  22. Iterable,
  23. Optional,
  24. Tuple,
  25. )
  26. from synapse.api.constants import EventContentFields, EventTypes, Membership
  27. from synapse.metrics import event_processing_positions
  28. from synapse.metrics.background_process_metrics import run_as_background_process
  29. from synapse.storage.databases.main.state_deltas import StateDelta
  30. from synapse.types import JsonDict
  31. if TYPE_CHECKING:
  32. from synapse.server import HomeServer
  33. logger = logging.getLogger(__name__)
  34. class StatsHandler:
  35. """Handles keeping the *_stats tables updated with a simple time-series of
  36. information about the users, rooms and media on the server, such that admins
  37. have some idea of who is consuming their resources.
  38. Heavily derived from UserDirectoryHandler
  39. """
  40. def __init__(self, hs: "HomeServer"):
  41. self.hs = hs
  42. self.store = hs.get_datastores().main
  43. self._storage_controllers = hs.get_storage_controllers()
  44. self.state = hs.get_state_handler()
  45. self.clock = hs.get_clock()
  46. self.notifier = hs.get_notifier()
  47. self.is_mine_id = hs.is_mine_id
  48. self.stats_enabled = hs.config.stats.stats_enabled
  49. # The current position in the current_state_delta stream
  50. self.pos: Optional[int] = None
  51. # Guard to ensure we only process deltas one at a time
  52. self._is_processing = False
  53. if self.stats_enabled and hs.config.worker.run_background_tasks:
  54. self.notifier.add_replication_callback(self.notify_new_event)
  55. # We kick this off so that we don't have to wait for a change before
  56. # we start populating stats
  57. self.clock.call_later(0, self.notify_new_event)
  58. def notify_new_event(self) -> None:
  59. """Called when there may be more deltas to process"""
  60. if not self.stats_enabled or self._is_processing:
  61. return
  62. self._is_processing = True
  63. async def process() -> None:
  64. try:
  65. await self._unsafe_process()
  66. finally:
  67. self._is_processing = False
  68. run_as_background_process("stats.notify_new_event", process)
  69. async def _unsafe_process(self) -> None:
  70. # If self.pos is None then means we haven't fetched it from DB
  71. if self.pos is None:
  72. self.pos = await self.store.get_stats_positions()
  73. room_max_stream_ordering = self.store.get_room_max_stream_ordering()
  74. if self.pos > room_max_stream_ordering:
  75. # apparently, we've processed more events than exist in the database!
  76. # this can happen if events are removed with history purge or similar.
  77. logger.warning(
  78. "Event stream ordering appears to have gone backwards (%i -> %i): "
  79. "rewinding stats processor",
  80. self.pos,
  81. room_max_stream_ordering,
  82. )
  83. self.pos = room_max_stream_ordering
  84. # Loop round handling deltas until we're up to date
  85. while True:
  86. # Be sure to read the max stream_ordering *before* checking if there are any outstanding
  87. # deltas, since there is otherwise a chance that we could miss updates which arrive
  88. # after we check the deltas.
  89. room_max_stream_ordering = self.store.get_room_max_stream_ordering()
  90. if self.pos == room_max_stream_ordering:
  91. break
  92. logger.debug(
  93. "Processing room stats %s->%s", self.pos, room_max_stream_ordering
  94. )
  95. (
  96. max_pos,
  97. deltas,
  98. ) = await self._storage_controllers.state.get_current_state_deltas(
  99. self.pos, room_max_stream_ordering
  100. )
  101. if deltas:
  102. logger.debug("Handling %d state deltas", len(deltas))
  103. room_deltas, user_deltas = await self._handle_deltas(deltas)
  104. else:
  105. room_deltas = {}
  106. user_deltas = {}
  107. logger.debug("room_deltas: %s", room_deltas)
  108. logger.debug("user_deltas: %s", user_deltas)
  109. # Always call this so that we update the stats position.
  110. await self.store.bulk_update_stats_delta(
  111. self.clock.time_msec(),
  112. updates={"room": room_deltas, "user": user_deltas},
  113. stream_id=max_pos,
  114. )
  115. logger.debug("Handled room stats to %s -> %s", self.pos, max_pos)
  116. event_processing_positions.labels("stats").set(max_pos)
  117. self.pos = max_pos
  118. async def _handle_deltas(
  119. self, deltas: Iterable[StateDelta]
  120. ) -> Tuple[Dict[str, CounterType[str]], Dict[str, CounterType[str]]]:
  121. """Called with the state deltas to process
  122. Returns:
  123. Two dicts: the room deltas and the user deltas,
  124. mapping from room/user ID to changes in the various fields.
  125. """
  126. room_to_stats_deltas: Dict[str, CounterType[str]] = {}
  127. user_to_stats_deltas: Dict[str, CounterType[str]] = {}
  128. room_to_state_updates: Dict[str, Dict[str, Any]] = {}
  129. for delta in deltas:
  130. logger.debug(
  131. "Handling: %r, %r %r, %s",
  132. delta.room_id,
  133. delta.event_type,
  134. delta.state_key,
  135. delta.event_id,
  136. )
  137. token = await self.store.get_earliest_token_for_stats("room", delta.room_id)
  138. # If the earliest token to begin from is larger than our current
  139. # stream ID, skip processing this delta.
  140. if token is not None and token >= delta.stream_id:
  141. logger.debug(
  142. "Ignoring: %s as earlier than this room's initial ingestion event",
  143. delta.event_id,
  144. )
  145. continue
  146. if delta.event_id is None and delta.prev_event_id is None:
  147. logger.error(
  148. "event ID is None and so is the previous event ID. stream_id: %s",
  149. delta.stream_id,
  150. )
  151. continue
  152. event_content: JsonDict = {}
  153. if delta.event_id is not None:
  154. event = await self.store.get_event(delta.event_id, allow_none=True)
  155. if event:
  156. event_content = event.content or {}
  157. # All the values in this dict are deltas (RELATIVE changes)
  158. room_stats_delta = room_to_stats_deltas.setdefault(delta.room_id, Counter())
  159. room_state = room_to_state_updates.setdefault(delta.room_id, {})
  160. if delta.prev_event_id is None:
  161. # this state event doesn't overwrite another,
  162. # so it is a new effective/current state event
  163. room_stats_delta["current_state_events"] += 1
  164. if delta.event_type == EventTypes.Member:
  165. # we could use StateDeltasHandler._get_key_change here but it's
  166. # a bit inefficient given we're not testing for a specific
  167. # result; might as well just grab the prev_membership and
  168. # membership strings and compare them.
  169. # We take None rather than leave as a previous membership
  170. # in the absence of a previous event because we do not want to
  171. # reduce the leave count when a new-to-the-room user joins.
  172. prev_membership = None
  173. if delta.prev_event_id is not None:
  174. prev_event = await self.store.get_event(
  175. delta.prev_event_id, allow_none=True
  176. )
  177. if prev_event:
  178. prev_event_content = prev_event.content
  179. prev_membership = prev_event_content.get(
  180. "membership", Membership.LEAVE
  181. )
  182. membership = event_content.get("membership", Membership.LEAVE)
  183. if prev_membership is None:
  184. logger.debug("No previous membership for this user.")
  185. elif membership == prev_membership:
  186. pass # noop
  187. elif prev_membership == Membership.JOIN:
  188. room_stats_delta["joined_members"] -= 1
  189. elif prev_membership == Membership.INVITE:
  190. room_stats_delta["invited_members"] -= 1
  191. elif prev_membership == Membership.LEAVE:
  192. room_stats_delta["left_members"] -= 1
  193. elif prev_membership == Membership.BAN:
  194. room_stats_delta["banned_members"] -= 1
  195. elif prev_membership == Membership.KNOCK:
  196. room_stats_delta["knocked_members"] -= 1
  197. else:
  198. raise ValueError(
  199. "%r is not a valid prev_membership" % (prev_membership,)
  200. )
  201. if membership == prev_membership:
  202. pass # noop
  203. elif membership == Membership.JOIN:
  204. room_stats_delta["joined_members"] += 1
  205. elif membership == Membership.INVITE:
  206. room_stats_delta["invited_members"] += 1
  207. elif membership == Membership.LEAVE:
  208. room_stats_delta["left_members"] += 1
  209. elif membership == Membership.BAN:
  210. room_stats_delta["banned_members"] += 1
  211. elif membership == Membership.KNOCK:
  212. room_stats_delta["knocked_members"] += 1
  213. else:
  214. raise ValueError("%r is not a valid membership" % (membership,))
  215. user_id = delta.state_key
  216. if self.is_mine_id(user_id):
  217. # this accounts for transitions like leave → ban and so on.
  218. has_changed_joinedness = (prev_membership == Membership.JOIN) != (
  219. membership == Membership.JOIN
  220. )
  221. if has_changed_joinedness:
  222. membership_delta = +1 if membership == Membership.JOIN else -1
  223. user_to_stats_deltas.setdefault(user_id, Counter())[
  224. "joined_rooms"
  225. ] += membership_delta
  226. room_stats_delta["local_users_in_room"] += membership_delta
  227. elif delta.event_type == EventTypes.Create:
  228. room_state["is_federatable"] = (
  229. event_content.get(EventContentFields.FEDERATE, True) is True
  230. )
  231. room_type = event_content.get(EventContentFields.ROOM_TYPE)
  232. if isinstance(room_type, str):
  233. room_state["room_type"] = room_type
  234. elif delta.event_type == EventTypes.JoinRules:
  235. room_state["join_rules"] = event_content.get("join_rule")
  236. elif delta.event_type == EventTypes.RoomHistoryVisibility:
  237. room_state["history_visibility"] = event_content.get(
  238. "history_visibility"
  239. )
  240. elif delta.event_type == EventTypes.RoomEncryption:
  241. room_state["encryption"] = event_content.get("algorithm")
  242. elif delta.event_type == EventTypes.Name:
  243. room_state["name"] = event_content.get("name")
  244. elif delta.event_type == EventTypes.Topic:
  245. room_state["topic"] = event_content.get("topic")
  246. elif delta.event_type == EventTypes.RoomAvatar:
  247. room_state["avatar"] = event_content.get("url")
  248. elif delta.event_type == EventTypes.CanonicalAlias:
  249. room_state["canonical_alias"] = event_content.get("alias")
  250. elif delta.event_type == EventTypes.GuestAccess:
  251. room_state["guest_access"] = event_content.get(
  252. EventContentFields.GUEST_ACCESS
  253. )
  254. for room_id, state in room_to_state_updates.items():
  255. logger.debug("Updating room_stats_state for %s: %s", room_id, state)
  256. await self.store.update_room_state(room_id, state)
  257. return room_to_stats_deltas, user_to_stats_deltas