Use context vars instead of thread-local storage for change logging

This commit is contained in:
jeremystretch 2022-11-02 17:40:43 -04:00 committed by Jeremy Stretch
parent 8400509358
commit cd8943144b
5 changed files with 31 additions and 35 deletions

View File

@ -3,8 +3,7 @@ from contextlib import contextmanager
from django.db.models.signals import m2m_changed, pre_delete, post_save from django.db.models.signals import m2m_changed, pre_delete, post_save
from extras.signals import clear_webhooks, clear_webhook_queue, handle_changed_object, handle_deleted_object from extras.signals import clear_webhooks, clear_webhook_queue, handle_changed_object, handle_deleted_object
from netbox import thread_locals from netbox.context import current_request, webhooks_queue
from netbox.request_context import set_request
from .webhooks import flush_webhooks from .webhooks import flush_webhooks
@ -16,8 +15,8 @@ def change_logging(request):
:param request: WSGIRequest object with a unique `id` set :param request: WSGIRequest object with a unique `id` set
""" """
set_request(request) current_request.set(request)
thread_locals.webhook_queue = [] webhooks_queue.set([])
# Connect our receivers to the post_save and post_delete signals. # Connect our receivers to the post_save and post_delete signals.
post_save.connect(handle_changed_object, dispatch_uid='handle_changed_object') post_save.connect(handle_changed_object, dispatch_uid='handle_changed_object')
@ -35,8 +34,8 @@ def change_logging(request):
clear_webhooks.disconnect(clear_webhook_queue, dispatch_uid='clear_webhook_queue') clear_webhooks.disconnect(clear_webhook_queue, dispatch_uid='clear_webhook_queue')
# Flush queued webhooks to RQ # Flush queued webhooks to RQ
flush_webhooks(thread_locals.webhook_queue) flush_webhooks(webhooks_queue.get())
del thread_locals.webhook_queue
# Clear the request from thread-local storage # Clear context vars
set_request(None) current_request.set(None)
webhooks_queue.set([])

View File

@ -7,9 +7,8 @@ from django.dispatch import receiver, Signal
from django_prometheus.models import model_deletes, model_inserts, model_updates from django_prometheus.models import model_deletes, model_inserts, model_updates
from extras.validators import CustomValidator from extras.validators import CustomValidator
from netbox import thread_locals
from netbox.config import get_config from netbox.config import get_config
from netbox.request_context import get_request from netbox.context import current_request, webhooks_queue
from netbox.signals import post_clean from netbox.signals import post_clean
from .choices import ObjectChangeActionChoices from .choices import ObjectChangeActionChoices
from .models import ConfigRevision, CustomField, ObjectChange from .models import ConfigRevision, CustomField, ObjectChange
@ -30,7 +29,7 @@ def handle_changed_object(sender, instance, **kwargs):
if not hasattr(instance, 'to_objectchange'): if not hasattr(instance, 'to_objectchange'):
return return
request = get_request() request = current_request.get()
m2m_changed = False m2m_changed = False
def is_same_object(instance, webhook_data): def is_same_object(instance, webhook_data):
@ -69,13 +68,14 @@ def handle_changed_object(sender, instance, **kwargs):
objectchange.save() objectchange.save()
# If this is an M2M change, update the previously queued webhook (from post_save) # If this is an M2M change, update the previously queued webhook (from post_save)
webhook_queue = thread_locals.webhook_queue queue = webhooks_queue.get()
if m2m_changed and webhook_queue and is_same_object(instance, webhook_queue[-1]): if m2m_changed and queue and is_same_object(instance, queue[-1]):
instance.refresh_from_db() # Ensure that we're working with fresh M2M assignments instance.refresh_from_db() # Ensure that we're working with fresh M2M assignments
webhook_queue[-1]['data'] = serialize_for_webhook(instance) queue[-1]['data'] = serialize_for_webhook(instance)
webhook_queue[-1]['snapshots']['postchange'] = get_snapshots(instance, action)['postchange'] queue[-1]['snapshots']['postchange'] = get_snapshots(instance, action)['postchange']
else: else:
enqueue_object(webhook_queue, instance, request.user, request.id, action) enqueue_object(queue, instance, request.user, request.id, action)
webhooks_queue.set(queue)
# Increment metric counters # Increment metric counters
if action == ObjectChangeActionChoices.ACTION_CREATE: if action == ObjectChangeActionChoices.ACTION_CREATE:
@ -91,7 +91,7 @@ def handle_deleted_object(sender, instance, **kwargs):
if not hasattr(instance, 'to_objectchange'): if not hasattr(instance, 'to_objectchange'):
return return
request = get_request() request = current_request.get()
# Record an ObjectChange if applicable # Record an ObjectChange if applicable
if hasattr(instance, 'to_objectchange'): if hasattr(instance, 'to_objectchange'):
@ -101,8 +101,9 @@ def handle_deleted_object(sender, instance, **kwargs):
objectchange.save() objectchange.save()
# Enqueue webhooks # Enqueue webhooks
webhook_queue = thread_locals.webhook_queue queue = webhooks_queue.get()
enqueue_object(webhook_queue, instance, request.user, request.id, ObjectChangeActionChoices.ACTION_DELETE) enqueue_object(queue, instance, request.user, request.id, ObjectChangeActionChoices.ACTION_DELETE)
webhooks_queue.set(queue)
# Increment metric counters # Increment metric counters
model_deletes.labels(instance._meta.model_name).inc() model_deletes.labels(instance._meta.model_name).inc()
@ -113,10 +114,8 @@ def clear_webhook_queue(sender, **kwargs):
Delete any queued webhooks (e.g. because of an aborted bulk transaction) Delete any queued webhooks (e.g. because of an aborted bulk transaction)
""" """
logger = logging.getLogger('webhooks') logger = logging.getLogger('webhooks')
webhook_queue = thread_locals.webhook_queue logger.info(f"Clearing {len(webhooks_queue.get())} queued webhooks ({sender})")
webhooks_queue.set([])
logger.info(f"Clearing {len(webhook_queue)} queued webhooks ({sender})")
webhook_queue.clear()
# #

View File

@ -1,3 +0,0 @@
import threading
thread_locals = threading.local()

10
netbox/netbox/context.py Normal file
View File

@ -0,0 +1,10 @@
from contextvars import ContextVar
__all__ = (
'current_request',
'webhooks_queue',
)
current_request = ContextVar('current_request')
webhooks_queue = ContextVar('webhooks_queue')

View File

@ -1,9 +0,0 @@
from netbox import thread_locals
def set_request(request):
thread_locals.request = request
def get_request():
return getattr(thread_locals, 'request', None)