Spawning a background process comes with a bunch of overhead, so let's try to reduce the number of background processes we need to spawn when handling inbound fed. Currently, we seem to be doing roughly one per command. Instead, lets keep the background process alive for a bit waiting for a new command to come in.
106 lines
3.7 KiB
Python
106 lines
3.7 KiB
Python
#
|
|
# This file is licensed under the Affero General Public License (AGPL) version 3.
|
|
#
|
|
# Copyright (C) 2025 Element Creations Ltd
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Affero General Public License as
|
|
# published by the Free Software Foundation, either version 3 of the
|
|
# License, or (at your option) any later version.
|
|
#
|
|
# See the GNU Affero General Public License for more details:
|
|
# <https://www.gnu.org/licenses/agpl-3.0.html>.
|
|
#
|
|
|
|
|
|
from unittest.mock import Mock
|
|
|
|
from twisted.internet.defer import Deferred
|
|
from twisted.internet.testing import MemoryReactor
|
|
|
|
from synapse.server import HomeServer
|
|
from synapse.util.bacckground_queue import BackgroundQueue
|
|
from synapse.util.clock import Clock
|
|
|
|
from tests.unittest import HomeserverTestCase
|
|
|
|
|
|
class BackgroundQueueTests(HomeserverTestCase):
|
|
def prepare(
|
|
self, reactor: MemoryReactor, clock: Clock, homeserver: HomeServer
|
|
) -> None:
|
|
self._process_item_mock = Mock(spec_set=[])
|
|
|
|
self.queue = BackgroundQueue[int](
|
|
hs=homeserver,
|
|
name="test_queue",
|
|
callback=self._process_item_mock,
|
|
timeout_ms=1000,
|
|
)
|
|
|
|
def test_simple_call(self) -> None:
|
|
"""Test that items added to the queue are processed."""
|
|
# Register a deferred to be the return value of the callback.
|
|
callback_result_deferred: Deferred[None] = Deferred()
|
|
self._process_item_mock.side_effect = callback_result_deferred
|
|
|
|
# Adding an item should cause the callback to be invoked.
|
|
self.queue.add(1)
|
|
|
|
self._process_item_mock.assert_called_once_with(1)
|
|
self._process_item_mock.reset_mock()
|
|
|
|
# Adding another item should not cause the callback to be invoked again
|
|
# until the previous one has completed.
|
|
self.queue.add(2)
|
|
self._process_item_mock.assert_not_called()
|
|
|
|
# Once the first callback completes, the second item should be
|
|
# processed.
|
|
callback_result_deferred.callback(None)
|
|
self._process_item_mock.assert_called_once_with(2)
|
|
|
|
def test_timeout(self) -> None:
|
|
"""Test that the background process wakes up if its idle, and that it
|
|
times out after being idle."""
|
|
|
|
# Register a deferred to be the return value of the callback.
|
|
callback_result_deferred: Deferred[None] = Deferred()
|
|
self._process_item_mock.side_effect = callback_result_deferred
|
|
|
|
# Adding an item should cause the callback to be invoked.
|
|
self.queue.add(1)
|
|
|
|
self._process_item_mock.assert_called_once_with(1)
|
|
self._process_item_mock.reset_mock()
|
|
|
|
# Let the callback complete.
|
|
callback_result_deferred.callback(None)
|
|
|
|
# Advance the clock by less than the timeout, and add another item.
|
|
self.reactor.advance(0.5)
|
|
self.assertIsNotNone(self.queue._wakeup_event)
|
|
self.queue.add(2)
|
|
|
|
# The callback should be invoked again.
|
|
callback_result_deferred = Deferred()
|
|
self._process_item_mock.side_effect = callback_result_deferred
|
|
self._process_item_mock.assert_called_once_with(2)
|
|
self._process_item_mock.reset_mock()
|
|
|
|
# Let the callback complete.
|
|
callback_result_deferred.callback(None)
|
|
|
|
# Advance the clock by more than the timeout.
|
|
self.reactor.advance(1.5)
|
|
|
|
# The background process should have exited, we check this by checking
|
|
# the internal wakeup event has been removed.
|
|
self.assertIsNone(self.queue._wakeup_event)
|
|
|
|
# Add another item. This should cause a new background process to be
|
|
# started.
|
|
self.queue.add(3)
|
|
|
|
self._process_item_mock.assert_called_once_with(3)
|