Opened 5 months ago

Last modified 5 months ago

#35583 closed Bug

asgiref.sync.sync_to_async cannot be affected by close_old_connections — at Version 1

Reported by: Alexandr Onufrienko Owned by:
Component: Database layer (models, ORM) Version:
Severity: Normal Keywords:
Cc: Carlton Gibson Triage Stage: Unreviewed
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description (last modified by Alexandr Onufrienko)

Rel. to https://code.djangoproject.com/ticket/34914#comment:3
partially rel. to this implementation https://github.com/django/channels/blob/main/channels/db.py

  1. Let's assume, that we have django command, that creates a "server" that listening for a message from queue, or periodically do some job.
  2. Second point - our service have async function and requires DB calls (dummy example in _test function)
  3. let's assume. that we have Postgres DB running in docker compose - and to reproduce this bug, we should call docker compose restart db

Similar to Django itself and, for example, to Django channels https://github.com/django/channels/blob/main/channels/db.py we want to be able to close closed connection (simplest way to reproduce - DB was restarted).

Expected behavior: we are assuming, that we do same steps as we can see in Django project examples and we should see success message in logs.
Actual: we will see failure: the connection is closed

What we can see in code itself:

  1. sync_to_async class will use its own isolated ThreadPoolExecutor
  2. django connections will be created withing this ThreadPoolExecutor and will be unavailable for us (this is my assumption)

Found workaround
Calling this code will implicitly have access to hidden DB pool and will close closed connections

asyncio.run(sync_to_async(close_old_connections)())

Notes:

  1. I'm not really sure - is it a feature request or a bug (taking into account, how hard to find theoretical reason of it)
  2. I didn't dive into details of async_to_sync function/class

Code snippet for Django command

import asyncio
import time

from asgiref.sync import sync_to_async
from django.db import close_old_connections
from django.core.management.base import BaseCommand

# This can be any model
from django.contrib.auth.models import User


class Command(BaseCommand):
    def handle(self, *args, **options):
        while True:
            process_message()
            time.sleep(3)


def process_message():
    print("close_old_connections")
    close_old_connections()

    print("run test")
    try:
        test()
        print("success")
    except Exception as e:
        print("failure:", str(e))
    print("done test")


def test():
    asyncio.run(_test())


async def _test():
    """
    Synthetic case - most likely, we will use ``sync_to_async`` as decorator for part of functions
    and will use it directly for other calls ``sync_to_async``.
    """
    await User.objects.afirst()
    await sync_to_async(
        User.objects.first
    )()
    await sync_to_async(
        User.objects.first,
        thread_sensitive=False,
    )()

Change History (1)

comment:1 by Alexandr Onufrienko, 5 months ago

Description: modified (diff)
Note: See TracTickets for help on using tickets.
Back to Top