Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Delay of 0.1 seconds between asyncio MPD commands causes hang #173

Open
dphoyes opened this issue Jun 1, 2021 · 0 comments
Open

Delay of 0.1 seconds between asyncio MPD commands causes hang #173

dphoyes opened this issue Jun 1, 2021 · 0 comments
Assignees

Comments

@dphoyes
Copy link
Contributor

dphoyes commented Jun 1, 2021

Consider this snippet:

import asyncio
import mpd.asyncio

async def main():
    client = mpd.asyncio.MPDClient()
    await client.connect("192.168.0.2", 6600)

    for _ in range(2):
        print("Status:")
        print((await client.status())["state"])
        await asyncio.sleep(0.1)

asyncio.run(main())

Running this with Python 3.9.5 and python-mpd2 3.0.4, I get this output:

Status:
stop
Status:

At this point, the second call to client.status() never completes, and the program hangs. If the 0.1 second delay is changed to any other value, the problem goes away. It seems like this is some kind of race condition with IMMEDIATE_COMMAND_TIMEOUT.

yakshaver2000 pushed a commit to yakshaver2000/python-mpd2 that referenced this issue Feb 9, 2023
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())
```
yakshaver2000 added a commit to yakshaver2000/python-mpd2 that referenced this issue Feb 9, 2023
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())
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants