Skip to content

Commit 6fc5ce8

Browse files
authored
Async compatible StaticFilesPanel (#1983)
* incoperate signals and contextvars to record staticfiles for concurrent requests * remove used_static_files contextvar and its dependencies allow on app ready monkey patching * async static files panel test * update doc * Code review changes * suggested changes
1 parent 9a81958 commit 6fc5ce8

File tree

5 files changed

+52
-17
lines changed

5 files changed

+52
-17
lines changed

debug_toolbar/panels/staticfiles.py

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import contextlib
2+
import uuid
23
from contextvars import ContextVar
34
from os.path import join, normpath
45

56
from django.conf import settings
67
from django.contrib.staticfiles import finders, storage
8+
from django.dispatch import Signal
79
from django.utils.functional import LazyObject
810
from 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

3539
class 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
{

docs/architecture.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ Problematic Parts
8181
the main benefit of the toolbar
8282
- Support for async and multi-threading: ``debug_toolbar.middleware.DebugToolbarMiddleware``
8383
is now async compatible and can process async requests. However certain
84-
panels such as ``SQLPanel``, ``TimerPanel``, ``StaticFilesPanel``,
84+
panels such as ``SQLPanel``, ``TimerPanel``,
8585
``RequestPanel``, ``HistoryPanel`` and ``ProfilingPanel`` aren't fully
8686
compatible and currently being worked on. For now, these panels
8787
are disabled by default when running in async environment.

tests/panels/test_staticfiles.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from django.conf import settings
22
from django.contrib.staticfiles import finders
3+
from django.shortcuts import render
4+
from django.test import AsyncRequestFactory
35

46
from ..base import BaseTestCase
57

@@ -27,6 +29,17 @@ def test_default_case(self):
2729
self.panel.get_staticfiles_dirs(), finders.FileSystemFinder().locations
2830
)
2931

32+
async def test_store_staticfiles_with_async_context(self):
33+
async def get_response(request):
34+
# template contains one static file
35+
return render(request, "staticfiles/async_static.html")
36+
37+
self._get_response = get_response
38+
async_request = AsyncRequestFactory().get("/")
39+
response = await self.panel.process_request(async_request)
40+
self.panel.generate_stats(self.request, response)
41+
self.assertEqual(self.panel.num_used, 1)
42+
3043
def test_insert_content(self):
3144
"""
3245
Test that the panel only inserts content after generate_stats and

tests/templates/base.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
<html>
33
<head>
44
<title>{{ title }}</title>
5+
{% block head %}{% endblock %}
56
</head>
67
<body>
78
{% block content %}{% endblock %}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{% extends "base.html" %}
2+
{% load static %}
3+
4+
{% block head %}
5+
<link rel="stylesheet" href="{% static 'additional_static/base.css' %}">
6+
{% endblock %}

0 commit comments

Comments
 (0)