11import contextlib
2+ import uuid
23from contextvars import ContextVar
34from os .path import join , normpath
45
56from django .conf import settings
67from django .contrib .staticfiles import finders , storage
8+ from django .dispatch import Signal
79from django .utils .functional import LazyObject
810from django .utils .translation import gettext_lazy as _ , ngettext
911
@@ -28,8 +30,10 @@ def url(self):
2830 return storage .staticfiles_storage .url (self .path )
2931
3032
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 ()
3337
3438
3539class DebugConfiguredStorage (LazyObject ):
@@ -59,7 +63,12 @@ def url(self, path):
5963 # The ContextVar wasn't set yet. Since the toolbar wasn't properly
6064 # configured to handle this request, we don't need to capture
6165 # 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+ )
6372 return super ().url (path )
6473
6574 self ._wrapped = DebugStaticFilesStorage ()
@@ -73,6 +82,7 @@ class StaticFilesPanel(panels.Panel):
7382 A panel to display the found staticfiles.
7483 """
7584
85+ is_async = True
7686 name = "Static files"
7787 template = "debug_toolbar/panels/staticfiles.html"
7888
@@ -87,12 +97,28 @@ def __init__(self, *args, **kwargs):
8797 super ().__init__ (* args , ** kwargs )
8898 self .num_found = 0
8999 self .used_paths = []
100+ self .request_id = str (uuid .uuid4 ())
90101
91- def enable_instrumentation (self ):
102+ @classmethod
103+ def ready (cls ):
92104 storage .staticfiles_storage = DebugConfiguredStorage ()
93105
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+
94119 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 )
96122
97123 @property
98124 def num_used (self ):
@@ -108,17 +134,6 @@ def nav_subtitle(self):
108134 "%(num_used)s file used" , "%(num_used)s files used" , num_used
109135 ) % {"num_used" : num_used }
110136
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-
122137 def generate_stats (self , request , response ):
123138 self .record_stats (
124139 {
0 commit comments