1
1
import contextlib
2
+ import uuid
2
3
from contextvars import ContextVar
3
4
from os .path import join , normpath
4
5
5
6
from django .conf import settings
6
7
from django .contrib .staticfiles import finders , storage
8
+ from django .dispatch import Signal
7
9
from django .utils .functional import LazyObject
8
10
from django .utils .translation import gettext_lazy as _ , ngettext
9
11
@@ -28,8 +30,10 @@ def url(self):
28
30
return storage .staticfiles_storage .url (self .path )
29
31
30
32
31
- # This will collect the StaticFile instances across threads.
32
- used_static_files = ContextVar ("djdt_static_used_static_files" )
33
+ # This will record and map the StaticFile instances with its associated
34
+ # request across threads and async concurrent requests state.
35
+ request_id_context_var = ContextVar ("djdt_request_id_store" )
36
+ record_static_file_signal = Signal ()
33
37
34
38
35
39
class DebugConfiguredStorage (LazyObject ):
@@ -59,7 +63,12 @@ def url(self, path):
59
63
# The ContextVar wasn't set yet. Since the toolbar wasn't properly
60
64
# configured to handle this request, we don't need to capture
61
65
# the static file.
62
- used_static_files .get ().append (StaticFile (path ))
66
+ request_id = request_id_context_var .get ()
67
+ record_static_file_signal .send (
68
+ sender = self ,
69
+ staticfile = StaticFile (path ),
70
+ request_id = request_id ,
71
+ )
63
72
return super ().url (path )
64
73
65
74
self ._wrapped = DebugStaticFilesStorage ()
@@ -73,6 +82,7 @@ class StaticFilesPanel(panels.Panel):
73
82
A panel to display the found staticfiles.
74
83
"""
75
84
85
+ is_async = True
76
86
name = "Static files"
77
87
template = "debug_toolbar/panels/staticfiles.html"
78
88
@@ -87,12 +97,28 @@ def __init__(self, *args, **kwargs):
87
97
super ().__init__ (* args , ** kwargs )
88
98
self .num_found = 0
89
99
self .used_paths = []
100
+ self .request_id = str (uuid .uuid4 ())
90
101
91
- def enable_instrumentation (self ):
102
+ @classmethod
103
+ def ready (cls ):
92
104
storage .staticfiles_storage = DebugConfiguredStorage ()
93
105
106
+ def _store_static_files_signal_handler (self , sender , staticfile , ** kwargs ):
107
+ # Only record the static file if the request_id matches the one
108
+ # that was used to create the panel.
109
+ # as sender of the signal and this handler will have multiple
110
+ # concurrent connections and we want to avoid storing of same
111
+ # staticfile from other connections as well.
112
+ if request_id_context_var .get () == self .request_id :
113
+ self .used_paths .append (staticfile )
114
+
115
+ def enable_instrumentation (self ):
116
+ self .ctx_token = request_id_context_var .set (self .request_id )
117
+ record_static_file_signal .connect (self ._store_static_files_signal_handler )
118
+
94
119
def disable_instrumentation (self ):
95
- storage .staticfiles_storage = _original_storage
120
+ record_static_file_signal .disconnect (self ._store_static_files_signal_handler )
121
+ request_id_context_var .reset (self .ctx_token )
96
122
97
123
@property
98
124
def num_used (self ):
@@ -108,17 +134,6 @@ def nav_subtitle(self):
108
134
"%(num_used)s file used" , "%(num_used)s files used" , num_used
109
135
) % {"num_used" : num_used }
110
136
111
- def process_request (self , request ):
112
- reset_token = used_static_files .set ([])
113
- response = super ().process_request (request )
114
- # Make a copy of the used paths so that when the
115
- # ContextVar is reset, our panel still has the data.
116
- self .used_paths = used_static_files .get ().copy ()
117
- # Reset the ContextVar to be empty again, removing the reference
118
- # to the list of used files.
119
- used_static_files .reset (reset_token )
120
- return response
121
-
122
137
def generate_stats (self , request , response ):
123
138
self .record_stats (
124
139
{
0 commit comments