channels: Failing test when calling django orm code wrapped in database_sync_to_async
I’m trying to test my Channels consumer which calls database_sync_to_async code. The consumer looks something like this:
class MyConsumer(AsyncJsonWebsocketConsumer):
async def connect(self):
my_obj = await self.get_obj()
...other code
@database_sync_to_async
def get_obj(self):
return MyModel.objects.get(filter_condition)
The test is using the @pytest.mark.asyncio and @pytest.mark.django_db decorators ie:
@pytest.mark.asyncio
@pytest.mark.django_db
async def test_heartbeat():
communicator = WebsocketCommunicator(MyConsumer, '<path>')
await communicator.connect()
await communicator.disconnect()
I’m using the following command to run the test:
./manage.py test xxx/tests.py::test_heartbeat
The test itself passes, however at the end of the test run I always get the following error:
=============================================== ERRORS ===============================================
________________________________ ERROR at teardown of test_heartbeat _________________________________
self = <django.db.backends.utils.CursorWrapper object at 0x7fbc7b8d80b8>, sql = 'DROP DATABASE "test"'
params = None
ignored_wrapper_args = (False, {'connection': <django.db.backends.postgresql.base.DatabaseWrapper object at 0x7fbc7b0481d0>, 'cursor': <django.db.backends.utils.CursorWrapper object at 0x7fbc7b8d80b8>})
def _execute(self, sql, params, *ignored_wrapper_args):
self.db.validate_no_broken_transaction()
with self.db.wrap_database_errors:
if params is None:
> return self.cursor.execute(sql)
E psycopg2.OperationalError: database "test" is being accessed by other users
E DETAIL: There is 1 other session using the database.
I can make the test failure go away by removing all references to database_sync_to_async in the consumer, but my understanding is that is poor practice to have sync code (like calling django orm) running inside an async function.
Strangely when I get the failing test two tests run (one pass and one fail), but when I remove the references to database_sync_to_async only one test runs.
Here are the versions of my libraries:
django==2.0.6
daphne==2.2.0
asgiref==2.3.2
channels-redis==2.2.1
pytest==3.6.1
pytest-asyncio==0.8.0
pytest-django==3.1.2
About this issue
- Original URL
- State: open
- Created 6 years ago
- Reactions: 6
- Comments: 33 (17 by maintainers)
Commits related to this issue
- django_db_setup: warn instead of crash with teardown errors Previously it would cause pytest to crash, and this is just something to warn about anyway. Ref: https://github.com/django/channels/issues... — committed to blueyed/pytest-django by blueyed 5 years ago
- django_db_setup: warn instead of crash with teardown errors (#726) Previously it would cause pytest to crash, and this is just something to warn about anyway. Ref: https://github.com/django/chann... — committed to pytest-dev/pytest-django by blueyed 5 years ago
- SyncToAsync: use executor attribute This allows for overriding it easily in derived classes, and it seems to be better than changing the loop's default executor (in the case of ASGI_THREADS being set... — committed to blueyed/asgiref by blueyed 5 years ago
- Fix DatabaseSyncToAsync - copy/use connections from the main thread, which are in an atomic block (used in tests) - do not close connections in atomic blocks Ref: https://github.com/django/channel... — committed to blueyed/channels by blueyed 5 years ago
- SyncToAsync: use executor attribute This allows for overriding it easily in derived classes, and it seems to be better than changing the loop's default executor (in the case of ASGI_THREADS being set... — committed to blueyed/asgiref by blueyed 5 years ago
- Fix DatabaseSyncToAsync - copy/use connections from the main thread, which are in an atomic block (used in tests) - do not close connections in atomic blocks Ref: https://github.com/django/channel... — committed to blueyed/channels by blueyed 5 years ago
- SyncToAsync: use executor attribute This allows for overriding it easily in derived classes, and it seems to be better than changing the loop's default executor (in the case of ASGI_THREADS being set... — committed to blueyed/asgiref by blueyed 5 years ago
- SyncToAsync: use executor attribute This allows for overriding it easily in derived classes, and it seems to be better than changing the loop's default executor (in the case of ASGI_THREADS being set... — committed to blueyed/asgiref by blueyed 5 years ago
- SyncToAsync: use executor attribute This allows for overriding it easily in derived classes, and it seems to be better than changing the loop's default executor (in the case of ASGI_THREADS being set... — committed to blueyed/asgiref by blueyed 5 years ago
- SyncToAsync: use executor attribute This allows for overriding it easily in derived classes, and it seems to be better than changing the loop's default executor (in the case of ASGI_THREADS being set... — committed to blueyed/asgiref by blueyed 5 years ago
- Fix DatabaseSyncToAsync - copy/use connections from the main thread, which are in an atomic block (used in tests) - do not close connections in atomic blocks Ref: https://github.com/django/channel... — committed to blueyed/channels by blueyed 5 years ago
- Fix DatabaseSyncToAsync - copy/use connections from the main thread, which are in an atomic block (used in tests) - do not close connections in atomic blocks Ref: https://github.com/django/channel... — committed to blueyed/channels by blueyed 5 years ago
- Fix DatabaseSyncToAsync - copy/use connections from the main thread, which are in an atomic block (used in tests) - do not close connections in atomic blocks Ref: https://github.com/django/channel... — committed to blueyed/channels by blueyed 5 years ago
- Fix DatabaseSyncToAsync - copy/use connections from the main thread, which are in an atomic block (used in tests) - do not close connections in atomic blocks Ref: https://github.com/django/channel... — committed to blueyed/channels by blueyed 5 years ago
- django_db_setup: warn instead of crash with teardown errors (#726) Previously it would cause pytest to crash, and this is just something to warn about anyway. Ref: https://github.com/django/chann... — committed to beyondgeeks/django-pytest by beyondgeeks 5 years ago
I don’t suggest this as a final fix. But we were able to bypass this on django 3.1 + channels 2.4 with such fixture:
It is specific for asgi and channels implementation, not threadsafe, not asyncio safe
@vijayshan thanks for your feedback. I did try to use transaction=True in my pytest.mark.django_db, but didn’t seem to help. However I did not try to run your example code exactly, so it’s possible there’s something specific to my database that’s causing those errors.
One thing I noticed was with just a single test case, it passes but as soon as you add more test cases there’s a bigger chance some connections won’t be closed properly resulting in the error message from my original post. Also in case it helps there’s another issue someone created in the pytest-asyncio github that exactly describes my problem: https://github.com/pytest-dev/pytest-asyncio/issues/82. Unfortunately no solution there either.
Anyway in my case I came up with a really hacky fix. It involves calling raw sql to close those connections just before the last test ends. I’m using postgres so my code looks like:
Kind of ugly, I know, but at least now my tests are passing all the time.
@cybergrind Thank you. This garbage is still broken, so this fix is salvation.
I’ve reproduced this problem in a more minimal form with this test repo: https://github.com/adamchainz/channels-bug-connection-closed
The issue only appeared in the client project I was working on when upgrading to asgiref 3.5.1+, which fixed thread-sensitive mode. This fix causes queries from a tested consumer to run on the main thread inside the
atomic()
from theTestCase
.As a workaround I’ve added this to the project’s test runner class to disable the calls to
close_old_connections()
in@database_sync_to_async
:This issue actually seems to be a repeat of #462. Historically that was fixed in 9ae27cb835763075a81ba3823d20bd68f3ace46b, which made Channels disable its connection-closing logic during tests. Unfortunately this fix seems to have been lost when moving from the testing
Client
to the newer testing communicators (Client
removed in 66bc2981f1c69f35aed9815e5b90d19d5c66387d).I think a fix should look something like the previous methodolgy, removing the calls to
close_old_connections()
during tests.Out of curiosity: I see that nobody here has mentioned
CONN_MAX_AGE
. It occurs to me that if one setssettings.DATABASES['default']['CONN_MAX_AGE']=0
, the error in this bug should never happen (because@database_sync_to_async
should close its connection each time it’s called).CONN_MAX_AGE=0
may be a viable approach for some people.@tomek-rej Have you tried setting the transaction parameter on the pytest.mark.django_db decorator to true?
This is a sample test: `@pytest.mark.django_db(transaction=True) @pytest.mark.asyncio async def test_my_consumer_normal():
and this is the consumer: `class ChatConsumer(AsyncWebsocketConsumer): “”" ChatConsumer: The chat consumer class is responsible for all chat interactions “”"
` My tests are passing.
I have transitioned our project to a different approach for unit tests: use the main-thread database connection. This makes tests faster since they don’t need to disconnect/reconnect (assuming you’ve configured Django to persist connections – which is a valid assumption, because if you didn’t you wouldn’t be seeing this error, right?).
This means … er … replacing
@database_sync_to_async
. I have no compunction about doing this: I don’t believe database connections should run on the default thread pool. (This lets you configure the number of database connections your Django app maintains. That’s a good thing.)In
cjworkbench/sync.py
, we have:In our
tests/utils.py
file, we have:Now:
@database_sync_to_async
, import it from your ownsync
module – not fromchannels
.DbTestCase
and runself.run_with_async_db(async_method(*args, **kwargs))
The calling convention mimicsasyncio.run(task(...))
, notasync_to_sync(task)(...)
.In my opinion, the separate-thread-pool approach should be part of Channels proper. I’m less certain about how Channels proper should address unit testing.
Here’s my ugly hack:
… and then instead of
async_to_async(func)(params)
, I runself.loop.run_until_complete(func(params))
.This sort of test case should be the exception, not the norm. You can usually structure your code so that each single test either tests scheduling or database queries but not both.