You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

467 lines
19 KiB

  1. # Copyright 2017 Vector Creations Ltd
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. """A replication client for use by synapse workers.
  15. """
  16. import logging
  17. from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Set, Tuple
  18. from twisted.internet.defer import Deferred
  19. from twisted.internet.interfaces import IAddress, IConnector
  20. from twisted.internet.protocol import ReconnectingClientFactory
  21. from twisted.python.failure import Failure
  22. from synapse.api.constants import EventTypes, ReceiptTypes
  23. from synapse.federation import send_queue
  24. from synapse.federation.sender import FederationSender
  25. from synapse.logging.context import PreserveLoggingContext, make_deferred_yieldable
  26. from synapse.metrics.background_process_metrics import run_as_background_process
  27. from synapse.replication.tcp.protocol import ClientReplicationStreamProtocol
  28. from synapse.replication.tcp.streams import (
  29. AccountDataStream,
  30. DeviceListsStream,
  31. PushersStream,
  32. PushRulesStream,
  33. ReceiptsStream,
  34. TagAccountDataStream,
  35. ToDeviceStream,
  36. TypingStream,
  37. )
  38. from synapse.replication.tcp.streams.events import (
  39. EventsStream,
  40. EventsStreamEventRow,
  41. EventsStreamRow,
  42. )
  43. from synapse.types import PersistedEventPosition, ReadReceipt, StreamKeyType, UserID
  44. from synapse.util.async_helpers import Linearizer, timeout_deferred
  45. from synapse.util.metrics import Measure
  46. if TYPE_CHECKING:
  47. from synapse.replication.tcp.handler import ReplicationCommandHandler
  48. from synapse.server import HomeServer
  49. logger = logging.getLogger(__name__)
  50. # How long we allow callers to wait for replication updates before timing out.
  51. _WAIT_FOR_REPLICATION_TIMEOUT_SECONDS = 30
  52. class DirectTcpReplicationClientFactory(ReconnectingClientFactory):
  53. """Factory for building connections to the master. Will reconnect if the
  54. connection is lost.
  55. Accepts a handler that is passed to `ClientReplicationStreamProtocol`.
  56. """
  57. initialDelay = 0.1
  58. maxDelay = 1 # Try at least once every N seconds
  59. def __init__(
  60. self,
  61. hs: "HomeServer",
  62. client_name: str,
  63. command_handler: "ReplicationCommandHandler",
  64. ):
  65. self.client_name = client_name
  66. self.command_handler = command_handler
  67. self.server_name = hs.config.server.server_name
  68. self.hs = hs
  69. self._clock = hs.get_clock() # As self.clock is defined in super class
  70. hs.get_reactor().addSystemEventTrigger("before", "shutdown", self.stopTrying)
  71. def startedConnecting(self, connector: IConnector) -> None:
  72. logger.info("Connecting to replication: %r", connector.getDestination())
  73. def buildProtocol(self, addr: IAddress) -> ClientReplicationStreamProtocol:
  74. logger.info("Connected to replication: %r", addr)
  75. return ClientReplicationStreamProtocol(
  76. self.hs,
  77. self.client_name,
  78. self.server_name,
  79. self._clock,
  80. self.command_handler,
  81. )
  82. def clientConnectionLost(self, connector: IConnector, reason: Failure) -> None:
  83. logger.error("Lost replication conn: %r", reason)
  84. ReconnectingClientFactory.clientConnectionLost(self, connector, reason)
  85. def clientConnectionFailed(self, connector: IConnector, reason: Failure) -> None:
  86. logger.error("Failed to connect to replication: %r", reason)
  87. ReconnectingClientFactory.clientConnectionFailed(self, connector, reason)
  88. class ReplicationDataHandler:
  89. """Handles incoming stream updates from replication.
  90. This instance notifies the slave data store about updates. Can be subclassed
  91. to handle updates in additional ways.
  92. """
  93. def __init__(self, hs: "HomeServer"):
  94. self.store = hs.get_datastores().main
  95. self.notifier = hs.get_notifier()
  96. self._reactor = hs.get_reactor()
  97. self._clock = hs.get_clock()
  98. self._streams = hs.get_replication_streams()
  99. self._instance_name = hs.get_instance_name()
  100. self._typing_handler = hs.get_typing_handler()
  101. self._notify_pushers = hs.config.worker.start_pushers
  102. self._pusher_pool = hs.get_pusherpool()
  103. self._presence_handler = hs.get_presence_handler()
  104. self.send_handler: Optional[FederationSenderHandler] = None
  105. if hs.should_send_federation():
  106. self.send_handler = FederationSenderHandler(hs)
  107. # Map from stream to list of deferreds waiting for the stream to
  108. # arrive at a particular position. The lists are sorted by stream position.
  109. self._streams_to_waiters: Dict[str, List[Tuple[int, Deferred]]] = {}
  110. async def on_rdata(
  111. self, stream_name: str, instance_name: str, token: int, rows: list
  112. ) -> None:
  113. """Called to handle a batch of replication data with a given stream token.
  114. By default this just pokes the slave store. Can be overridden in subclasses to
  115. handle more.
  116. Args:
  117. stream_name: name of the replication stream for this batch of rows
  118. instance_name: the instance that wrote the rows.
  119. token: stream token for this batch of rows
  120. rows: a list of Stream.ROW_TYPE objects as returned by Stream.parse_row.
  121. """
  122. self.store.process_replication_rows(stream_name, instance_name, token, rows)
  123. if self.send_handler:
  124. await self.send_handler.process_replication_rows(stream_name, token, rows)
  125. if stream_name == TypingStream.NAME:
  126. self._typing_handler.process_replication_rows(token, rows)
  127. self.notifier.on_new_event(
  128. StreamKeyType.TYPING, token, rooms=[row.room_id for row in rows]
  129. )
  130. elif stream_name == PushRulesStream.NAME:
  131. self.notifier.on_new_event(
  132. StreamKeyType.PUSH_RULES, token, users=[row.user_id for row in rows]
  133. )
  134. elif stream_name in (AccountDataStream.NAME, TagAccountDataStream.NAME):
  135. self.notifier.on_new_event(
  136. StreamKeyType.ACCOUNT_DATA, token, users=[row.user_id for row in rows]
  137. )
  138. elif stream_name == ReceiptsStream.NAME:
  139. self.notifier.on_new_event(
  140. StreamKeyType.RECEIPT, token, rooms=[row.room_id for row in rows]
  141. )
  142. await self._pusher_pool.on_new_receipts(
  143. token, token, {row.room_id for row in rows}
  144. )
  145. elif stream_name == ToDeviceStream.NAME:
  146. entities = [row.entity for row in rows if row.entity.startswith("@")]
  147. if entities:
  148. self.notifier.on_new_event(
  149. StreamKeyType.TO_DEVICE, token, users=entities
  150. )
  151. elif stream_name == DeviceListsStream.NAME:
  152. all_room_ids: Set[str] = set()
  153. for row in rows:
  154. if row.entity.startswith("@"):
  155. room_ids = await self.store.get_rooms_for_user(row.entity)
  156. all_room_ids.update(room_ids)
  157. self.notifier.on_new_event(
  158. StreamKeyType.DEVICE_LIST, token, rooms=all_room_ids
  159. )
  160. elif stream_name == PushersStream.NAME:
  161. for row in rows:
  162. if row.deleted:
  163. self.stop_pusher(row.user_id, row.app_id, row.pushkey)
  164. else:
  165. await self.start_pusher(row.user_id, row.app_id, row.pushkey)
  166. elif stream_name == EventsStream.NAME:
  167. # We shouldn't get multiple rows per token for events stream, so
  168. # we don't need to optimise this for multiple rows.
  169. for row in rows:
  170. if row.type != EventsStreamEventRow.TypeId:
  171. continue
  172. assert isinstance(row, EventsStreamRow)
  173. assert isinstance(row.data, EventsStreamEventRow)
  174. if row.data.rejected:
  175. continue
  176. extra_users: Tuple[UserID, ...] = ()
  177. if row.data.type == EventTypes.Member and row.data.state_key:
  178. extra_users = (UserID.from_string(row.data.state_key),)
  179. max_token = self.store.get_room_max_token()
  180. event_pos = PersistedEventPosition(instance_name, token)
  181. await self.notifier.on_new_room_event_args(
  182. event_pos=event_pos,
  183. max_room_stream_token=max_token,
  184. extra_users=extra_users,
  185. room_id=row.data.room_id,
  186. event_id=row.data.event_id,
  187. event_type=row.data.type,
  188. state_key=row.data.state_key,
  189. membership=row.data.membership,
  190. )
  191. await self._presence_handler.process_replication_rows(
  192. stream_name, instance_name, token, rows
  193. )
  194. # Notify any waiting deferreds. The list is ordered by position so we
  195. # just iterate through the list until we reach a position that is
  196. # greater than the received row position.
  197. waiting_list = self._streams_to_waiters.get(stream_name, [])
  198. # Index of first item with a position after the current token, i.e we
  199. # have called all deferreds before this index. If not overwritten by
  200. # loop below means either a) no items in list so no-op or b) all items
  201. # in list were called and so the list should be cleared. Setting it to
  202. # `len(list)` works for both cases.
  203. index_of_first_deferred_not_called = len(waiting_list)
  204. for idx, (position, deferred) in enumerate(waiting_list):
  205. if position <= token:
  206. try:
  207. with PreserveLoggingContext():
  208. deferred.callback(None)
  209. except Exception:
  210. # The deferred has been cancelled or timed out.
  211. pass
  212. else:
  213. # The list is sorted by position so we don't need to continue
  214. # checking any further entries in the list.
  215. index_of_first_deferred_not_called = idx
  216. break
  217. # Drop all entries in the waiting list that were called in the above
  218. # loop. (This maintains the order so no need to resort)
  219. waiting_list[:] = waiting_list[index_of_first_deferred_not_called:]
  220. async def on_position(
  221. self, stream_name: str, instance_name: str, token: int
  222. ) -> None:
  223. await self.on_rdata(stream_name, instance_name, token, [])
  224. # We poke the generic "replication" notifier to wake anything up that
  225. # may be streaming.
  226. self.notifier.notify_replication()
  227. def on_remote_server_up(self, server: str) -> None:
  228. """Called when get a new REMOTE_SERVER_UP command."""
  229. # Let's wake up the transaction queue for the server in case we have
  230. # pending stuff to send to it.
  231. if self.send_handler:
  232. self.send_handler.wake_destination(server)
  233. async def wait_for_stream_position(
  234. self, instance_name: str, stream_name: str, position: int
  235. ) -> None:
  236. """Wait until this instance has received updates up to and including
  237. the given stream position.
  238. """
  239. if instance_name == self._instance_name:
  240. # We don't get told about updates written by this process, and
  241. # anyway in that case we don't need to wait.
  242. return
  243. current_position = self._streams[stream_name].current_token(self._instance_name)
  244. if position <= current_position:
  245. # We're already past the position
  246. return
  247. # Create a new deferred that times out after N seconds, as we don't want
  248. # to wedge here forever.
  249. deferred: "Deferred[None]" = Deferred()
  250. deferred = timeout_deferred(
  251. deferred, _WAIT_FOR_REPLICATION_TIMEOUT_SECONDS, self._reactor
  252. )
  253. waiting_list = self._streams_to_waiters.setdefault(stream_name, [])
  254. waiting_list.append((position, deferred))
  255. waiting_list.sort(key=lambda t: t[0])
  256. # We measure here to get in flight counts and average waiting time.
  257. with Measure(self._clock, "repl.wait_for_stream_position"):
  258. logger.info("Waiting for repl stream %r to reach %s", stream_name, position)
  259. await make_deferred_yieldable(deferred)
  260. logger.info(
  261. "Finished waiting for repl stream %r to reach %s", stream_name, position
  262. )
  263. def stop_pusher(self, user_id: str, app_id: str, pushkey: str) -> None:
  264. if not self._notify_pushers:
  265. return
  266. key = "%s:%s" % (app_id, pushkey)
  267. pushers_for_user = self._pusher_pool.pushers.get(user_id, {})
  268. pusher = pushers_for_user.pop(key, None)
  269. if pusher is None:
  270. return
  271. logger.info("Stopping pusher %r / %r", user_id, key)
  272. pusher.on_stop()
  273. async def start_pusher(self, user_id: str, app_id: str, pushkey: str) -> None:
  274. if not self._notify_pushers:
  275. return
  276. key = "%s:%s" % (app_id, pushkey)
  277. logger.info("Starting pusher %r / %r", user_id, key)
  278. await self._pusher_pool.start_pusher_by_id(app_id, pushkey, user_id)
  279. class FederationSenderHandler:
  280. """Processes the fedration replication stream
  281. This class is only instantiate on the worker responsible for sending outbound
  282. federation transactions. It receives rows from the replication stream and forwards
  283. the appropriate entries to the FederationSender class.
  284. """
  285. def __init__(self, hs: "HomeServer"):
  286. assert hs.should_send_federation()
  287. self.store = hs.get_datastores().main
  288. self._is_mine_id = hs.is_mine_id
  289. self._hs = hs
  290. # We need to make a temporary value to ensure that mypy picks up the
  291. # right type. We know we should have a federation sender instance since
  292. # `should_send_federation` is True.
  293. sender = hs.get_federation_sender()
  294. assert isinstance(sender, FederationSender)
  295. self.federation_sender = sender
  296. # Stores the latest position in the federation stream we've gotten up
  297. # to. This is always set before we use it.
  298. self.federation_position: Optional[int] = None
  299. self._fed_position_linearizer = Linearizer(name="_fed_position_linearizer")
  300. def wake_destination(self, server: str) -> None:
  301. self.federation_sender.wake_destination(server)
  302. async def process_replication_rows(
  303. self, stream_name: str, token: int, rows: list
  304. ) -> None:
  305. # The federation stream contains things that we want to send out, e.g.
  306. # presence, typing, etc.
  307. if stream_name == "federation":
  308. send_queue.process_rows_for_federation(self.federation_sender, rows)
  309. await self.update_token(token)
  310. # ... and when new receipts happen
  311. elif stream_name == ReceiptsStream.NAME:
  312. await self._on_new_receipts(rows)
  313. # ... as well as device updates and messages
  314. elif stream_name == DeviceListsStream.NAME:
  315. # The entities are either user IDs (starting with '@') whose devices
  316. # have changed, or remote servers that we need to tell about
  317. # changes.
  318. hosts = {row.entity for row in rows if not row.entity.startswith("@")}
  319. for host in hosts:
  320. self.federation_sender.send_device_messages(host, immediate=False)
  321. elif stream_name == ToDeviceStream.NAME:
  322. # The to_device stream includes stuff to be pushed to both local
  323. # clients and remote servers, so we ignore entities that start with
  324. # '@' (since they'll be local users rather than destinations).
  325. hosts = {row.entity for row in rows if not row.entity.startswith("@")}
  326. for host in hosts:
  327. self.federation_sender.send_device_messages(host)
  328. async def _on_new_receipts(
  329. self, rows: Iterable[ReceiptsStream.ReceiptsStreamRow]
  330. ) -> None:
  331. """
  332. Args:
  333. rows: new receipts to be processed
  334. """
  335. for receipt in rows:
  336. # we only want to send on receipts for our own users
  337. if not self._is_mine_id(receipt.user_id):
  338. continue
  339. # Private read receipts never get sent over federation.
  340. if receipt.receipt_type == ReceiptTypes.READ_PRIVATE:
  341. continue
  342. receipt_info = ReadReceipt(
  343. receipt.room_id,
  344. receipt.receipt_type,
  345. receipt.user_id,
  346. [receipt.event_id],
  347. receipt.data,
  348. )
  349. await self.federation_sender.send_read_receipt(receipt_info)
  350. async def update_token(self, token: int) -> None:
  351. """Update the record of where we have processed to in the federation stream.
  352. Called after we have processed a an update received over replication. Sends
  353. a FEDERATION_ACK back to the master, and stores the token that we have processed
  354. in `federation_stream_position` so that we can restart where we left off.
  355. """
  356. self.federation_position = token
  357. # We save and send the ACK to master asynchronously, so we don't block
  358. # processing on persistence. We don't need to do this operation for
  359. # every single RDATA we receive, we just need to do it periodically.
  360. if self._fed_position_linearizer.is_queued(None):
  361. # There is already a task queued up to save and send the token, so
  362. # no need to queue up another task.
  363. return
  364. run_as_background_process("_save_and_send_ack", self._save_and_send_ack)
  365. async def _save_and_send_ack(self) -> None:
  366. """Save the current federation position in the database and send an ACK
  367. to master with where we're up to.
  368. """
  369. # We should only be calling this once we've got a token.
  370. assert self.federation_position is not None
  371. try:
  372. # We linearize here to ensure we don't have races updating the token
  373. #
  374. # XXX this appears to be redundant, since the ReplicationCommandHandler
  375. # has a linearizer which ensures that we only process one line of
  376. # replication data at a time. Should we remove it, or is it doing useful
  377. # service for robustness? Or could we replace it with an assertion that
  378. # we're not being re-entered?
  379. async with self._fed_position_linearizer.queue(None):
  380. # We persist and ack the same position, so we take a copy of it
  381. # here as otherwise it can get modified from underneath us.
  382. current_position = self.federation_position
  383. await self.store.update_federation_out_pos(
  384. "federation", current_position
  385. )
  386. # We ACK this token over replication so that the master can drop
  387. # its in memory queues
  388. self._hs.get_replication_command_handler().send_federation_ack(
  389. current_position
  390. )
  391. except Exception:
  392. logger.exception("Error updating federation stream position")