Skip to content

Commit 0d60099

Browse files
committed
Move request object and webhook queue to TLS
1 parent 94069e7 commit 0d60099

File tree

6 files changed

+42
-29
lines changed

6 files changed

+42
-29
lines changed

netbox/extras/context_managers.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
from django.db.models.signals import m2m_changed, pre_delete, post_save
44

5-
from extras.signals import clear_webhooks, _clear_webhook_queue, _handle_changed_object, _handle_deleted_object
6-
from utilities.utils import curry
5+
from extras.signals import clear_webhooks, clear_webhook_queue, handle_changed_object, handle_deleted_object
6+
from netbox import thread_locals
7+
from netbox.request_context import set_request
78
from .webhooks import flush_webhooks
89

910

@@ -15,12 +16,8 @@ def change_logging(request):
1516
1617
:param request: WSGIRequest object with a unique `id` set
1718
"""
18-
webhook_queue = []
19-
20-
# Curry signals receivers to pass the current request
21-
handle_changed_object = curry(_handle_changed_object, request, webhook_queue)
22-
handle_deleted_object = curry(_handle_deleted_object, request, webhook_queue)
23-
clear_webhook_queue = curry(_clear_webhook_queue, webhook_queue)
19+
set_request(request)
20+
thread_locals.webhook_queue = []
2421

2522
# Connect our receivers to the post_save and post_delete signals.
2623
post_save.connect(handle_changed_object, dispatch_uid='handle_changed_object')
@@ -38,5 +35,8 @@ def change_logging(request):
3835
clear_webhooks.disconnect(clear_webhook_queue, dispatch_uid='clear_webhook_queue')
3936

4037
# Flush queued webhooks to RQ
41-
flush_webhooks(webhook_queue)
42-
del webhook_queue
38+
flush_webhooks(thread_locals.webhook_queue)
39+
del thread_locals.webhook_queue
40+
41+
# Clear the request from thread-local storage
42+
set_request(None)

netbox/extras/signals.py

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
from django.dispatch import receiver, Signal
77
from django_prometheus.models import model_deletes, model_inserts, model_updates
88

9+
from netbox import thread_locals
10+
from netbox.request_context import get_request
911
from netbox.signals import post_clean
1012
from .choices import ObjectChangeActionChoices
1113
from .models import CustomField, ObjectChange
@@ -20,22 +22,23 @@
2022
clear_webhooks = Signal()
2123

2224

23-
def _handle_changed_object(request, webhook_queue, sender, instance, **kwargs):
25+
def handle_changed_object(sender, instance, **kwargs):
2426
"""
2527
Fires when an object is created or updated.
2628
"""
29+
if not hasattr(instance, 'to_objectchange'):
30+
return
31+
32+
request = get_request()
33+
m2m_changed = False
34+
2735
def is_same_object(instance, webhook_data):
2836
return (
2937
ContentType.objects.get_for_model(instance) == webhook_data['content_type'] and
3038
instance.pk == webhook_data['object_id'] and
3139
request.id == webhook_data['request_id']
3240
)
3341

34-
if not hasattr(instance, 'to_objectchange'):
35-
return
36-
37-
m2m_changed = False
38-
3942
# Determine the type of change being made
4043
if kwargs.get('created'):
4144
action = ObjectChangeActionChoices.ACTION_CREATE
@@ -65,6 +68,7 @@ def is_same_object(instance, webhook_data):
6568
objectchange.save()
6669

6770
# If this is an M2M change, update the previously queued webhook (from post_save)
71+
webhook_queue = thread_locals.webhook_queue
6872
if m2m_changed and webhook_queue and is_same_object(instance, webhook_queue[-1]):
6973
instance.refresh_from_db() # Ensure that we're working with fresh M2M assignments
7074
webhook_queue[-1]['data'] = serialize_for_webhook(instance)
@@ -79,13 +83,15 @@ def is_same_object(instance, webhook_data):
7983
model_updates.labels(instance._meta.model_name).inc()
8084

8185

82-
def _handle_deleted_object(request, webhook_queue, sender, instance, **kwargs):
86+
def handle_deleted_object(sender, instance, **kwargs):
8387
"""
8488
Fires when an object is deleted.
8589
"""
8690
if not hasattr(instance, 'to_objectchange'):
8791
return
8892

93+
request = get_request()
94+
8995
# Record an ObjectChange if applicable
9096
if hasattr(instance, 'to_objectchange'):
9197
objectchange = instance.to_objectchange(ObjectChangeActionChoices.ACTION_DELETE)
@@ -94,19 +100,21 @@ def _handle_deleted_object(request, webhook_queue, sender, instance, **kwargs):
94100
objectchange.save()
95101

96102
# Enqueue webhooks
103+
webhook_queue = thread_locals.webhook_queue
97104
enqueue_object(webhook_queue, instance, request.user, request.id, ObjectChangeActionChoices.ACTION_DELETE)
98105

99106
# Increment metric counters
100107
model_deletes.labels(instance._meta.model_name).inc()
101108

102109

103-
def _clear_webhook_queue(webhook_queue, sender, **kwargs):
110+
def clear_webhook_queue(sender, **kwargs):
104111
"""
105112
Delete any queued webhooks (e.g. because of an aborted bulk transaction)
106113
"""
107114
logger = logging.getLogger('webhooks')
108-
logger.info(f"Clearing {len(webhook_queue)} queued webhooks ({sender})")
115+
webhook_queue = thread_locals.webhook_queue
109116

117+
logger.info(f"Clearing {len(webhook_queue)} queued webhooks ({sender})")
110118
webhook_queue.clear()
111119

112120

netbox/netbox/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import threading
2+
3+
thread_locals = threading.local()

netbox/netbox/middleware.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1+
import logging
12
import uuid
23
from urllib import parse
3-
import logging
44

55
from django.conf import settings
6-
from django.contrib.auth.middleware import RemoteUserMiddleware as RemoteUserMiddleware_
76
from django.contrib import auth
7+
from django.contrib.auth.middleware import RemoteUserMiddleware as RemoteUserMiddleware_
88
from django.core.exceptions import ImproperlyConfigured
99
from django.db import ProgrammingError
1010
from django.http import Http404, HttpResponseRedirect
@@ -114,7 +114,7 @@ def _get_groups(self, request):
114114
return groups
115115

116116

117-
class ObjectChangeMiddleware(object):
117+
class ObjectChangeMiddleware:
118118
"""
119119
This middleware performs three functions in response to an object being created, updated, or deleted:
120120

netbox/netbox/request_context.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from netbox import thread_locals
2+
3+
4+
def set_request(request):
5+
thread_locals.request = request
6+
7+
8+
def get_request():
9+
return getattr(thread_locals, 'request', None)

netbox/utilities/utils.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -327,13 +327,6 @@ def decode_value(value: Any, _decode_keys: bool) -> Any:
327327
return {urllib.parse.unquote(k): decode_value(v, decode_keys) for k, v in encoded_dict.items()}
328328

329329

330-
# Taken from django.utils.functional (<3.0)
331-
def curry(_curried_func, *args, **kwargs):
332-
def _curried(*moreargs, **morekwargs):
333-
return _curried_func(*args, *moreargs, **{**kwargs, **morekwargs})
334-
return _curried
335-
336-
337330
def array_to_string(array):
338331
"""
339332
Generate an efficient, human-friendly string from a set of integers. Intended for use with ArrayField.

0 commit comments

Comments
 (0)