1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Fixes #3452: Queue deletion ObjectChanges until after response is sent

This commit is contained in:
Jeremy Stretch
2019-08-26 16:52:05 -04:00
parent 03ac2721bc
commit 6e66f8d68a
8 changed files with 77 additions and 106 deletions

View File

@ -6,7 +6,6 @@ from datetime import timedelta
from django.conf import settings
from django.db.models.signals import post_delete, post_save
from django.utils import timezone
from django.utils.functional import curry
from django_prometheus.models import model_deletes, model_inserts, model_updates
from .constants import (
@ -19,31 +18,27 @@ from .webhooks import enqueue_webhooks
_thread_locals = threading.local()
def cache_changed_object(instance, **kwargs):
action = OBJECTCHANGE_ACTION_CREATE if kwargs['created'] else OBJECTCHANGE_ACTION_UPDATE
# Cache the object for further processing was the response has completed.
_thread_locals.changed_objects.append(
(instance, action)
)
def cache_changed_object(sender, instance, **kwargs):
"""
Cache an object being created or updated for the changelog.
"""
if hasattr(instance, 'to_objectchange'):
action = OBJECTCHANGE_ACTION_CREATE if kwargs['created'] else OBJECTCHANGE_ACTION_UPDATE
objectchange = instance.to_objectchange(action)
_thread_locals.changed_objects.append(
(instance, objectchange)
)
def _record_object_deleted(request, instance, **kwargs):
# TODO: Can we cache deletions for later processing like we do for saves? Currently this will trigger an exception
# when trying to serialize ManyToMany relations after the object has been deleted. This should be doable if we alter
# log_change() to return ObjectChanges to be saved rather than saving them directly.
# Record that the object was deleted
if hasattr(instance, 'log_change'):
instance.log_change(request.user, request.id, OBJECTCHANGE_ACTION_DELETE)
# Enqueue webhooks
enqueue_webhooks(instance, request.user, request.id, OBJECTCHANGE_ACTION_DELETE)
# Increment metric counters
model_deletes.labels(instance._meta.model_name).inc()
def cache_deleted_object(sender, instance, **kwargs):
"""
Cache an object being deleted for the changelog.
"""
if hasattr(instance, 'to_objectchange'):
objectchange = instance.to_objectchange(OBJECTCHANGE_ACTION_DELETE)
_thread_locals.changed_objects.append(
(instance, objectchange)
)
def purge_objectchange_cache(sender, **kwargs):
@ -79,12 +74,9 @@ class ObjectChangeMiddleware(object):
# the same request.
request.id = uuid.uuid4()
# Signals don't include the request context, so we're currying it into the post_delete function ahead of time.
record_object_deleted = curry(_record_object_deleted, request)
# Connect our receivers to the post_save and post_delete signals.
post_save.connect(cache_changed_object, dispatch_uid='record_object_saved')
post_delete.connect(record_object_deleted, dispatch_uid='record_object_deleted')
post_save.connect(cache_changed_object, dispatch_uid='cache_changed_object')
post_delete.connect(cache_deleted_object, dispatch_uid='cache_deleted_object')
# Provide a hook for purging the change cache
purge_changelog.connect(purge_objectchange_cache)
@ -95,27 +87,26 @@ class ObjectChangeMiddleware(object):
# If the change cache has been purged (e.g. due to an exception) abort the logging of all changes resulting from
# this request.
if _thread_locals.changed_objects is None:
# Delete ObjectChanges representing deletions, since these have already been written
ObjectChange.objects.filter(request_id=request.id).delete()
return response
# Create records for any cached objects that were created/updated.
for obj, action in _thread_locals.changed_objects:
for obj, objectchange in _thread_locals.changed_objects:
# Record the change
if hasattr(obj, 'log_change'):
obj.log_change(request.user, request.id, action)
objectchange.user = request.user
objectchange.request_id = request.id
objectchange.save()
# Enqueue webhooks
enqueue_webhooks(obj, request.user, request.id, action)
enqueue_webhooks(obj, request.user, request.id, objectchange.action)
# Increment metric counters
if action == OBJECTCHANGE_ACTION_CREATE:
if objectchange.action == OBJECTCHANGE_ACTION_CREATE:
model_inserts.labels(obj._meta.model_name).inc()
elif action == OBJECTCHANGE_ACTION_UPDATE:
elif objectchange.action == OBJECTCHANGE_ACTION_UPDATE:
model_updates.labels(obj._meta.model_name).inc()
elif objectchange.action == OBJECTCHANGE_ACTION_DELETE:
model_deletes.labels(obj._meta.model_name).inc()
# Housekeeping: 1% chance of clearing out expired ObjectChanges
if _thread_locals.changed_objects and settings.CHANGELOG_RETENTION and random.randint(1, 100) == 1: