Skip to content

Commit

Permalink
asyncio: fix race condition in command queue
Browse files Browse the repository at this point in the history
When two commands were issued exactly 0.1 seconds apart, then
`mpd.asyncio.MPDClient.__run` would incorrectly send an "idle" command
to the server, and then attempt to parse the server's response to the
second command as an "idle" response, causing errors like

```
mpd.base.ProtocolError: Expected key 'volume', got 'repeat'
```

as described by Mic92#195, and hangs as described by
Mic92#173.

The root of the problem is that wrapping `asyncio.Queue.get` in
`asyncio.wait_for` can result in a `TimeoutError` exception even when
the queue is not empty, as demonstrated by the following example
(tested with python 3.9.2 and 3.11.2):

```python
import asyncio

TIMEOUT = 0.1

async def get_from_queue(queue):
    try:
        await asyncio.wait_for(
            queue.get(),
            timeout=TIMEOUT
        )
    except asyncio.exceptions.TimeoutError:
        # This is counterintuitive: The "get" operation has timed out,
        # but the queue is not empty!
        assert not queue.empty()
    else:
        # This block is never executed.
        assert False

async def main():
    queue = asyncio.Queue()
    task = asyncio.create_task(get_from_queue(queue))
    await asyncio.sleep(TIMEOUT)
    queue.put_nowait(1)
    await task

asyncio.run(main())
```
  • Loading branch information
yakshaver2000 committed Feb 9, 2023
1 parent d632d7b commit f86b138
Showing 1 changed file with 7 additions and 0 deletions.
7 changes: 7 additions & 0 deletions mpd/asyncio.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,13 @@ async def __run(self):
# in this case is intended, and is just what asyncio.Queue
# suggests for "get with timeout".

if not self.__command_queue.empty():
# A __command_queue.put() has happened after the
# asyncio.wait_for() timeout but before execution of
# this coroutine resumed. Looping around again will
# fetch the new entry from the queue.
continue

if self.__idle_failed:
# We could try for a more elaborate path where we now
# await the command queue indefinitely, but as we're
Expand Down

0 comments on commit f86b138

Please sign in to comment.