-
-
Notifications
You must be signed in to change notification settings - Fork 33.1k
Fixed #31253 -- Fixed data loss possibility when using caching from async code. #12430
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
Conversation
Can we add a regression test for this fix? (maybe a simple view that uses cache in @andrewgodwin What do you think? |
It's not a critical backport, but given the chance of data corruption if someone tries to use caching from async code I think it would be worthwhile. |
I'm happy to write a test, but I'm not sure I fully grok async Python and Django. This was simply noticed by inspection. So a little guidance would be appreciated. This is what I started with, but it passes on diff --git a/tests/asgi/tests.py b/tests/asgi/tests.py
index 3a26d3130f..9d8b4868b6 100644
--- a/tests/asgi/tests.py
+++ b/tests/asgi/tests.py
@@ -6,6 +6,7 @@ from asgiref.sync import async_to_sync
from asgiref.testing import ApplicationCommunicator
from django.core.asgi import get_asgi_application
+from django.core.cache import caches
from django.core.signals import request_started
from django.db import close_old_connections
from django.test import SimpleTestCase, override_settings
@@ -167,3 +168,20 @@ class ASGITest(SimpleTestCase):
response_body = await communicator.receive_output()
self.assertEqual(response_body['type'], 'http.response.body')
self.assertEqual(response_body['body'], b'')
+
+ @async_to_sync
+ async def test_cache_in_view(self):
+ application = get_asgi_application()
+ communicator = ApplicationCommunicator(
+ application,
+ self._get_scope(path='/cache/'),
+ )
+ caches['default'].set('foo', 'bar')
+ await communicator.send_input({'type': 'http.request'})
+ response_start = await communicator.receive_output()
+ self.assertEqual(response_start['type'], 'http.response.start')
+ self.assertEqual(response_start['status'], 200)
+ response_body = await communicator.receive_output()
+ self.assertEqual(response_body['type'], 'http.response.body')
+ self.assertEqual(response_body['body'], b'bar')
+ self.assertEqual(caches['default'].get('foo'), 'bar')
diff --git a/tests/asgi/urls.py b/tests/asgi/urls.py
index ff8d21ea7c..fe7b3b6a89 100644
--- a/tests/asgi/urls.py
+++ b/tests/asgi/urls.py
@@ -1,3 +1,4 @@
+from django.core.cache import caches
from django.http import FileResponse, HttpResponse
from django.urls import path
@@ -14,6 +15,11 @@ def hello_meta(request):
)
+def cache(request):
+ val = caches['default'].get('foo')
+ return HttpResponse(val)
+
+
test_filename = __file__
@@ -21,4 +27,5 @@ urlpatterns = [
path('', hello),
path('file/', lambda x: FileResponse(open(test_filename, 'rb'))),
path('meta/', hello_meta),
+ path('cache/', cache),
] |
Yes 🤔 @andrewgodwin Can you help with a regression test? |
Unfortunately it's not possible to write a reliable test for the underlying fault, as you need to trigger a precise race condition - specifically, you need to have two coroutines be scheduled to touch the same cache object at exactly the same time. Otherwise, this will appear to work as normal. If you just want to test that the code exists, making two coroutines and then verifying that they both get a different cache object (by |
Good idea 🚀 Thanks 👍 |
I added a test that fails before the change. Can you take a look and let me know if it is what you had in mind? |
…sync code. Case missed in a415ce7.
@jdufresne Thanks 👍 I pushed minor edits to tests. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Super. Thanks @jdufresne.
Case missed in a415ce7.