Convert `Linearizer` tests from `inlineCallbacks` to async (#12353)

Signed-off-by: Sean Quah <seanq@element.io>
This commit is contained in:
Sean Quah 2022-04-05 14:56:09 +01:00 committed by GitHub
parent 66053b6bfb
commit 41b5f72677
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 133 additions and 90 deletions

1
changelog.d/12353.misc Normal file
View File

@ -0,0 +1 @@
Convert `Linearizer` tests from `inlineCallbacks` to async.

View File

@ -13,160 +13,202 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from typing import Callable, Hashable, Tuple
from twisted.internet import defer, reactor from twisted.internet import defer, reactor
from twisted.internet.defer import CancelledError from twisted.internet.base import ReactorBase
from twisted.internet.defer import CancelledError, Deferred
from synapse.logging.context import LoggingContext, current_context from synapse.logging.context import LoggingContext, current_context
from synapse.util import Clock
from synapse.util.async_helpers import Linearizer from synapse.util.async_helpers import Linearizer
from tests import unittest from tests import unittest
class LinearizerTestCase(unittest.TestCase): class LinearizerTestCase(unittest.TestCase):
@defer.inlineCallbacks def _start_task(
def test_linearizer(self): self, linearizer: Linearizer, key: Hashable
) -> Tuple["Deferred[None]", "Deferred[None]", Callable[[], None]]:
"""Starts a task which acquires the linearizer lock, blocks, then completes.
Args:
linearizer: The `Linearizer`.
key: The `Linearizer` key.
Returns:
A tuple containing:
* A cancellable `Deferred` for the entire task.
* A `Deferred` that resolves once the task acquires the lock.
* A function that unblocks the task. Must be called by the caller
to allow the task to release the lock and complete.
"""
acquired_d: "Deferred[None]" = Deferred()
unblock_d: "Deferred[None]" = Deferred()
async def task() -> None:
with await linearizer.queue(key):
acquired_d.callback(None)
await unblock_d
d = defer.ensureDeferred(task())
def unblock() -> None:
unblock_d.callback(None)
# The next task, if it exists, will acquire the lock and require a kick of
# the reactor to advance.
self._pump()
return d, acquired_d, unblock
def _pump(self) -> None:
"""Pump the reactor to advance `Linearizer`s."""
assert isinstance(reactor, ReactorBase)
while reactor.getDelayedCalls():
reactor.runUntilCurrent()
def test_linearizer(self) -> None:
"""Tests that a task is queued up behind an earlier task."""
linearizer = Linearizer() linearizer = Linearizer()
key = object() key = object()
d1 = linearizer.queue(key) _, acquired_d1, unblock1 = self._start_task(linearizer, key)
cm1 = yield d1 self.assertTrue(acquired_d1.called)
d2 = linearizer.queue(key) _, acquired_d2, unblock2 = self._start_task(linearizer, key)
self.assertFalse(d2.called) self.assertFalse(acquired_d2.called)
with cm1: # Once the first task is done, the second task can continue.
self.assertFalse(d2.called) unblock1()
self.assertTrue(acquired_d2.called)
with (yield d2): unblock2()
pass
@defer.inlineCallbacks def test_linearizer_is_queued(self) -> None:
def test_linearizer_is_queued(self): """Tests `Linearizer.is_queued`.
Runs through the same scenario as `test_linearizer`.
"""
linearizer = Linearizer() linearizer = Linearizer()
key = object() key = object()
d1 = linearizer.queue(key) _, acquired_d1, unblock1 = self._start_task(linearizer, key)
cm1 = yield d1 self.assertTrue(acquired_d1.called)
# Since d1 gets called immediately, "is_queued" should return false. # Since the first task acquires the lock immediately, "is_queued" should return
# false.
self.assertFalse(linearizer.is_queued(key)) self.assertFalse(linearizer.is_queued(key))
d2 = linearizer.queue(key) _, acquired_d2, unblock2 = self._start_task(linearizer, key)
self.assertFalse(d2.called) self.assertFalse(acquired_d2.called)
# Now d2 is queued up behind successful completion of cm1 # Now the second task is queued up behind the first.
self.assertTrue(linearizer.is_queued(key)) self.assertTrue(linearizer.is_queued(key))
with cm1: unblock1()
self.assertFalse(d2.called)
# cm1 still not done, so d2 still queued. # And now the second task acquires the lock and nothing is in the queue again.
self.assertTrue(linearizer.is_queued(key)) self.assertTrue(acquired_d2.called)
# And now d2 is called and nothing is in the queue again
self.assertFalse(linearizer.is_queued(key)) self.assertFalse(linearizer.is_queued(key))
with (yield d2): unblock2()
self.assertFalse(linearizer.is_queued(key))
self.assertFalse(linearizer.is_queued(key)) self.assertFalse(linearizer.is_queued(key))
def test_lots_of_queued_things(self): def test_lots_of_queued_things(self) -> None:
# we have one slow thing, and lots of fast things queued up behind it. """Tests lots of fast things queued up behind a slow thing.
# it should *not* explode the stack.
The stack should *not* explode when the slow thing completes.
"""
linearizer = Linearizer() linearizer = Linearizer()
key = ""
@defer.inlineCallbacks async def func(i: int) -> None:
def func(i, sleep=False):
with LoggingContext("func(%s)" % i) as lc: with LoggingContext("func(%s)" % i) as lc:
with (yield linearizer.queue("")): with (await linearizer.queue(key)):
self.assertEqual(current_context(), lc) self.assertEqual(current_context(), lc)
if sleep:
yield Clock(reactor).sleep(0)
self.assertEqual(current_context(), lc) self.assertEqual(current_context(), lc)
func(0, sleep=True) _, _, unblock = self._start_task(linearizer, key)
for i in range(1, 100): for i in range(1, 100):
func(i) defer.ensureDeferred(func(i))
return func(1000) d = defer.ensureDeferred(func(1000))
unblock()
self.successResultOf(d)
@defer.inlineCallbacks def test_multiple_entries(self) -> None:
def test_multiple_entries(self): """Tests a `Linearizer` with a concurrency above 1."""
limiter = Linearizer(max_count=3) limiter = Linearizer(max_count=3)
key = object() key = object()
d1 = limiter.queue(key) _, acquired_d1, unblock1 = self._start_task(limiter, key)
cm1 = yield d1 self.assertTrue(acquired_d1.called)
d2 = limiter.queue(key) _, acquired_d2, unblock2 = self._start_task(limiter, key)
cm2 = yield d2 self.assertTrue(acquired_d2.called)
d3 = limiter.queue(key) _, acquired_d3, unblock3 = self._start_task(limiter, key)
cm3 = yield d3 self.assertTrue(acquired_d3.called)
d4 = limiter.queue(key) # These next two tasks have to wait.
self.assertFalse(d4.called) _, acquired_d4, unblock4 = self._start_task(limiter, key)
self.assertFalse(acquired_d4.called)
d5 = limiter.queue(key) _, acquired_d5, unblock5 = self._start_task(limiter, key)
self.assertFalse(d5.called) self.assertFalse(acquired_d5.called)
with cm1: # Once the first task completes, the fourth task can continue.
self.assertFalse(d4.called) unblock1()
self.assertFalse(d5.called) self.assertTrue(acquired_d4.called)
self.assertFalse(acquired_d5.called)
cm4 = yield d4 # Once the third task completes, the fifth task can continue.
self.assertFalse(d5.called) unblock3()
self.assertTrue(acquired_d5.called)
with cm3: # Make all tasks finish.
self.assertFalse(d5.called) unblock2()
unblock4()
unblock5()
cm5 = yield d5 # The next task shouldn't have to wait.
_, acquired_d6, unblock6 = self._start_task(limiter, key)
self.assertTrue(acquired_d6)
unblock6()
with cm2: def test_cancellation(self) -> None:
pass """Tests cancellation while waiting for a `Linearizer`."""
with cm4:
pass
with cm5:
pass
d6 = limiter.queue(key)
with (yield d6):
pass
@defer.inlineCallbacks
def test_cancellation(self):
linearizer = Linearizer() linearizer = Linearizer()
key = object() key = object()
d1 = linearizer.queue(key) d1, acquired_d1, unblock1 = self._start_task(linearizer, key)
cm1 = yield d1 self.assertTrue(acquired_d1.called)
d2 = linearizer.queue(key) # Create a second task, waiting for the first task.
self.assertFalse(d2.called) d2, acquired_d2, _ = self._start_task(linearizer, key)
self.assertFalse(acquired_d2.called)
d3 = linearizer.queue(key) # Create a third task, waiting for the second task.
self.assertFalse(d3.called) d3, acquired_d3, unblock3 = self._start_task(linearizer, key)
self.assertFalse(acquired_d3.called)
# Cancel the waiting second task.
d2.cancel() d2.cancel()
with cm1: unblock1()
pass self.successResultOf(d1)
self.assertTrue(d2.called) self.assertTrue(d2.called)
try: self.failureResultOf(d2, CancelledError)
yield d2
self.fail("Expected d2 to raise CancelledError")
except CancelledError:
pass
with (yield d3): # The third task should continue running.
pass self.assertTrue(
acquired_d3.called,
"Third task did not get the lock after the second task was cancelled",
)
unblock3()
self.successResultOf(d3)