Пример #1
0
    def test_linearizer_is_queued(self) -> None:
        """Tests `Linearizer.is_queued`.

        Runs through the same scenario as `test_linearizer`.
        """
        linearizer = Linearizer()

        key = object()

        _, acquired_d1, unblock1 = self._start_task(linearizer, key)
        self.assertTrue(acquired_d1.called)

        # Since the first task acquires the lock immediately, "is_queued" should return
        # false.
        self.assertFalse(linearizer.is_queued(key))

        _, acquired_d2, unblock2 = self._start_task(linearizer, key)
        self.assertFalse(acquired_d2.called)

        # Now the second task is queued up behind the first.
        self.assertTrue(linearizer.is_queued(key))

        unblock1()

        # And now the second task acquires the lock and nothing is in the queue again.
        self.assertTrue(acquired_d2.called)
        self.assertFalse(linearizer.is_queued(key))

        unblock2()
        self.assertFalse(linearizer.is_queued(key))
Пример #2
0
    def test_linearizer_is_queued(self):
        linearizer = Linearizer()

        key = object()

        d1 = linearizer.queue(key)
        cm1 = yield d1

        # Since d1 gets called immediately, "is_queued" should return false.
        self.assertFalse(linearizer.is_queued(key))

        d2 = linearizer.queue(key)
        self.assertFalse(d2.called)

        # Now d2 is queued up behind successful completion of cm1
        self.assertTrue(linearizer.is_queued(key))

        with cm1:
            self.assertFalse(d2.called)

            # cm1 still not done, so d2 still queued.
            self.assertTrue(linearizer.is_queued(key))

        # And now d2 is called and nothing is in the queue again
        self.assertFalse(linearizer.is_queued(key))

        with (yield d2):
            self.assertFalse(linearizer.is_queued(key))

        self.assertFalse(linearizer.is_queued(key))
Пример #3
0
class FederationSenderHandler:
    """Processes the fedration replication stream

    This class is only instantiate on the worker responsible for sending outbound
    federation transactions. It receives rows from the replication stream and forwards
    the appropriate entries to the FederationSender class.
    """
    def __init__(self, hs: GenericWorkerServer):
        self.store = hs.get_datastore()
        self._is_mine_id = hs.is_mine_id
        self.federation_sender = hs.get_federation_sender()
        self._hs = hs

        # Stores the latest position in the federation stream we've gotten up
        # to. This is always set before we use it.
        self.federation_position = None

        self._fed_position_linearizer = Linearizer(
            name="_fed_position_linearizer")

    def on_start(self):
        # There may be some events that are persisted but haven't been sent,
        # so send them now.
        self.federation_sender.notify_new_events(
            self.store.get_room_max_stream_ordering())

    def wake_destination(self, server: str):
        self.federation_sender.wake_destination(server)

    async def process_replication_rows(self, stream_name, token, rows):
        # The federation stream contains things that we want to send out, e.g.
        # presence, typing, etc.
        if stream_name == "federation":
            send_queue.process_rows_for_federation(self.federation_sender,
                                                   rows)
            await self.update_token(token)

        # ... and when new receipts happen
        elif stream_name == ReceiptsStream.NAME:
            await self._on_new_receipts(rows)

        # ... as well as device updates and messages
        elif stream_name == DeviceListsStream.NAME:
            # The entities are either user IDs (starting with '@') whose devices
            # have changed, or remote servers that we need to tell about
            # changes.
            hosts = {
                row.entity
                for row in rows if not row.entity.startswith("@")
            }
            for host in hosts:
                self.federation_sender.send_device_messages(host)

        elif stream_name == ToDeviceStream.NAME:
            # The to_device stream includes stuff to be pushed to both local
            # clients and remote servers, so we ignore entities that start with
            # '@' (since they'll be local users rather than destinations).
            hosts = {
                row.entity
                for row in rows if not row.entity.startswith("@")
            }
            for host in hosts:
                self.federation_sender.send_device_messages(host)

    async def _on_new_receipts(self, rows):
        """
        Args:
            rows (Iterable[synapse.replication.tcp.streams.ReceiptsStream.ReceiptsStreamRow]):
                new receipts to be processed
        """
        for receipt in rows:
            # we only want to send on receipts for our own users
            if not self._is_mine_id(receipt.user_id):
                continue
            receipt_info = ReadReceipt(
                receipt.room_id,
                receipt.receipt_type,
                receipt.user_id,
                [receipt.event_id],
                receipt.data,
            )
            await self.federation_sender.send_read_receipt(receipt_info)

    async def update_token(self, token):
        """Update the record of where we have processed to in the federation stream.

        Called after we have processed a an update received over replication. Sends
        a FEDERATION_ACK back to the master, and stores the token that we have processed
         in `federation_stream_position` so that we can restart where we left off.
        """
        self.federation_position = token

        # We save and send the ACK to master asynchronously, so we don't block
        # processing on persistence. We don't need to do this operation for
        # every single RDATA we receive, we just need to do it periodically.

        if self._fed_position_linearizer.is_queued(None):
            # There is already a task queued up to save and send the token, so
            # no need to queue up another task.
            return

        run_as_background_process("_save_and_send_ack",
                                  self._save_and_send_ack)

    async def _save_and_send_ack(self):
        """Save the current federation position in the database and send an ACK
        to master with where we're up to.
        """
        try:
            # We linearize here to ensure we don't have races updating the token
            #
            # XXX this appears to be redundant, since the ReplicationCommandHandler
            # has a linearizer which ensures that we only process one line of
            # replication data at a time. Should we remove it, or is it doing useful
            # service for robustness? Or could we replace it with an assertion that
            # we're not being re-entered?

            with (await self._fed_position_linearizer.queue(None)):
                # We persist and ack the same position, so we take a copy of it
                # here as otherwise it can get modified from underneath us.
                current_position = self.federation_position

                await self.store.update_federation_out_pos(
                    "federation", current_position)

                # We ACK this token over replication so that the master can drop
                # its in memory queues
                self._hs.get_tcp_replication().send_federation_ack(
                    current_position)
        except Exception:
            logger.exception("Error updating federation stream position")