Skip to content

Commit

Permalink
Initialize locks lazily
Browse files Browse the repository at this point in the history
It seems that during class creation time, the event loop that is automatically
detected by asyncio.Lock is not the same event loop that ends up being used
by asyncio.run. Therefore, when two coroutines try to access the lock at the
same time and one of them needs to await it, a RuntimeError is raised because
of mismatched event loops.
  • Loading branch information
Askaholic committed Oct 7, 2020
1 parent 61dbfc8 commit 099601a
Show file tree
Hide file tree
Showing 2 changed files with 38 additions and 3 deletions.
9 changes: 6 additions & 3 deletions server/asyncio_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,14 @@ def _synchronize(
lock: Optional[asyncio.Lock] = None
) -> AsyncFunc:
"""Wrap an async function with an async lock."""
if lock is None:
lock = asyncio.Lock()

@wraps(function)
async def wrapped(*args, **kwargs):
nonlocal lock

# During testing, functions are called from multiple loops
if lock is None or lock._loop != asyncio.get_event_loop():
lock = asyncio.Lock()

async with lock:
return await function(*args, **kwargs)

Expand Down
32 changes: 32 additions & 0 deletions tests/unit_tests/test_matchmaker_queue.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import asyncio
import functools
import time
from concurrent.futures import CancelledError, TimeoutError

import mock
import pytest
from hypothesis import given
from hypothesis import strategies as st
Expand Down Expand Up @@ -406,3 +408,33 @@ async def find_matches():
matchmaker_queue.on_match_found.assert_called_once_with(
s2, s3, matchmaker_queue
)


@pytest.mark.asyncio
async def test_find_matches_synchronized(queue_factory):
is_matching = False

def make_matches(*args):
nonlocal is_matching

assert not is_matching, "Function call not synchronized"
is_matching = True

time.sleep(0.2)

is_matching = False
return []

with mock.patch(
"server.matchmaker.matchmaker_queue.make_matches",
make_matches
):
queues = [queue_factory(f"Queue{i}") for i in range(5)]
# Ensure that find_matches does not short circuit
for queue in queues:
queue._queue = {mock.Mock(): 1, mock.Mock(): 2}
queue.find_teams = mock.Mock()

await asyncio.gather(*[
queue.find_matches() for queue in queues
])

0 comments on commit 099601a

Please sign in to comment.