Skip to content

Off by 1 - Canceling async Redis command leaves connection open, in unsafe state for future commands #2624

Closed
@drago-balto

Description

@drago-balto

Version: 4.5.1

Platform: Python 3.8 / Ubuntu (but really, any platform will likely suffer the same issue)

Description: Canceling async Redis command leaves connection open, in unsafe state for future commands

The issue here is really the same as #2579, except that it generalizes it to all commands (as opposed to just blocking commands).

If async Redis request is canceled at the right time, after the command was sent but before the response was received and parsed, the connection is left in an unsafe state for future commands. The following redis operation on the same connection will send the command, and then promptly continue to read the response from the previous, canceled command. From that point on, the connection will remain in this weird, off-by-1 state.

Here's a script reproducing the problem:

import asyncio
from redis.asyncio import Redis
import sys


async def main():
    myhost, mypassword = sys.argv[1:]
    async with Redis(host=myhost, password=mypassword, ssl=True, single_connection_client=True) as r:

        await r.set('foo', 'foo')
        await r.set('bar', 'bar')

        t = asyncio.create_task(r.get('foo'))
        await asyncio.sleep(0.001)
        t.cancel()
        try:
            await t
            print('try again, we did not cancel the task in time')
        except asyncio.CancelledError:
            print('managed to cancel the task, connection is left open with unread response')

        print('bar:', await r.get('bar'))
        print('ping:', await r.ping())
        print('foo:', await r.get('foo'))


if __name__ == '__main__':
    asyncio.run(main())

Running this against a server in the cloud (such that requests typically take > 5ms to complete) results in this:

$ python redis_cancel.py hostname password
managed to cancel the task, connection is left open with unread response
bar: b'foo'
ping: False
foo: b'PONG'

I believe the solution is simple. This method needs to be modified to disconnect connection when current request is canceled. For example, do this:

    async def _send_command_parse_response(self, conn, command_name, *args, **options):
        """
        Send a command and parse the response
        """
        try:
            await conn.send_command(*args)
            return await self.parse_response(conn, command_name, **options)
        except asyncio.CancelledError:
            await conn.disconnect()
            raise

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions