Skip to content

Commit 18dcce9

Browse files
authored
🚚 release (#178)
2 parents 63aa595 + ea6dbc9 commit 18dcce9

11 files changed

Lines changed: 2324 additions & 28 deletions

‎netbox_diode_plugin/__init__.py‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,25 @@ class NetBoxDiodePluginConfig(PluginConfig):
7878
# fields (empty-QuerySet truthiness bug), so under load every
7979
# instance.clean()/save() re-queries extras_customfield. Cache
8080
# is invalidated on CustomField post_save/post_delete signals.
81+
#
82+
# apply_buffer_change_logging: keep the audit trail intact but
83+
# cut the per-save change-logging cost. During apply,
84+
# ObjectChange serialisation skips the per-m2m-relation SELECTs
85+
# that dominate `to_objectchange` (one query per m2m field on
86+
# every save), and the rows are collected in an in-memory
87+
# buffer instead of being written one at a time. On successful
88+
# commit the buffer is flushed as a single `bulk_create`, with
89+
# all objects' m2m relations resolved in one query per relation,
90+
# and `post_save` is re-emitted so receivers connected to
91+
# `post_save(ObjectChange)` still fire. The flush runs inline at
92+
# commit, so the audit log stays immediately consistent.
93+
# Mutually exclusive in intent with `apply_bypass_change_logging`
94+
# - if both are enabled, bypass wins (no rows produced at all).
8195
"apply_bypass_counter_updates": False,
8296
"apply_bypass_change_logging": False,
8397
"apply_bypass_search_indexing": False,
8498
"apply_bypass_customfield_query_cache": False,
99+
"apply_buffer_change_logging": False,
85100

86101
# Per-entity retry on Postgres deadlock (SQLSTATE 40P01) during
87102
# the apply phase of /bulk-plan-apply. Cross-batch concurrent

‎netbox_diode_plugin/api/applier.py‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@
1010
from django.db.utils import IntegrityError
1111
from rest_framework.exceptions import ValidationError as ValidationError
1212

13+
from .change_log_buffer import snapshot_for_apply
1314
from .common import NON_FIELD_ERRORS, Change, ChangeSet, ChangeSetException, ChangeSetResult, ChangeType, error_from_validation_error
14-
from .matcher import find_existing_object, invalidate_find_obj_entry
15+
from .matcher import find_existing_object, invalidate_find_obj_entry, requires_pre_save_match
1516
from .plugin_utils import get_object_type_model, legal_fields
1617
from .profile import profiled
1718
from .supported_models import get_serializer_for_model
@@ -78,6 +79,7 @@ def _try_find_and_update_existing_instance(data: dict, object_type: str, seriali
7879
try:
7980
instance = find_existing_object(data, object_type)
8081
if instance:
82+
snapshot_for_apply(instance)
8183
serializer = serializer_class(instance, data=data, partial=True, context={"request": request})
8284
serializer.is_valid(raise_exception=True)
8385
result = serializer.save()
@@ -110,8 +112,13 @@ def _apply_change(data: dict, model_class: models.Model, change: Change, created
110112
# For component types that may be auto-created from e.g. DeviceType or ModuleType templates,
111113
# try to find existing object first before attempting to create.
112114
# This prevents duplicates when components are instantiated during Device/Module save()
115+
# The same find-first path also handles types whose logical match
116+
# criteria are not enforced by a DB unique constraint (see
117+
# matcher._REQUIRES_PRE_SAVE_MATCH): concurrent planners would
118+
# otherwise each emit CREATE for the same logical row and both
119+
# inserts would succeed without IntegrityError to fall back on.
113120
instance = None
114-
if _is_auto_created_component(change.object_type):
121+
if _is_auto_created_component(change.object_type) or requires_pre_save_match(change.object_type):
115122
instance = _try_find_and_update_existing_instance(data, change.object_type, serializer_class, request)
116123

117124
if not instance:
@@ -124,6 +131,7 @@ def _apply_change(data: dict, model_class: models.Model, change: Change, created
124131
elif change_type == ChangeType.UPDATE:
125132
if object_id := change.object_id:
126133
instance = model_class.objects.get(id=object_id)
134+
snapshot_for_apply(instance)
127135
serializer = serializer_class(instance, data=data, partial=True, context={"request": request})
128136
serializer.is_valid(raise_exception=True)
129137
serializer.save()

0 commit comments

Comments
 (0)