Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

jdufresne
Copy link
Member

Case missed in a415ce7.

@felixxm
Copy link
Member

felixxm commented Feb 7, 2020

Can we add a regression test for this fix? (maybe a simple view that uses cache in tests.asgi.urls 🤔) We should probably backport this change, add release notes and a ticket.

@andrewgodwin What do you think?

@andrewgodwin
Copy link
Member

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.

@jdufresne
Copy link
Member Author

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 master. @felixxm were you expecting a crash? If you have ideas of how I should modify this, please let me know.

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),
 ]

@felixxm
Copy link
Member

felixxm commented Feb 10, 2020

@felixxm were you expecting a crash?

Yes 🤔

@andrewgodwin Can you help with a regression test?

@andrewgodwin
Copy link
Member

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 id()) should be enough.

@felixxm felixxm changed the title Refs #30451 -- Changed CacheHandler._caches to use asgiref.local.Local. Fixed #31253 -- Fixed data loss possibility when using caching from async code. Feb 10, 2020
@felixxm
Copy link
Member

felixxm commented Feb 10, 2020

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 id()) should be enough.

Good idea 🚀 Thanks 👍

@jdufresne
Copy link
Member Author

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?

@felixxm felixxm self-assigned this Feb 11, 2020
@felixxm
Copy link
Member

felixxm commented Feb 11, 2020

@jdufresne Thanks 👍 I pushed minor edits to tests.

Copy link
Member

@carltongibson carltongibson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super. Thanks @jdufresne.

@felixxm felixxm merged commit e3f6e18 into django:master Feb 11, 2020
@jdufresne jdufresne deleted the cache-local branch March 21, 2020 14:07
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

Successfully merging this pull request may close these issues.

4 participants