Просмотр исходного кода

Fixes #22812: Defer large Job payload fields during batched deletion

The batched job delete can't fast-delete (a global pre_delete receiver forces
per-instance signals), so each batch still instantiates its Job rows. Load only the PK
via only('pk') so those instances don't pull the large data/log_entries payloads,
cutting the resident set per batch. Also drop a dead `no-toggle` CSS class from the
delete-confirmation template (it is defined nowhere and, under Tabler, has no effect)
and use JobStatusChoices.STATUS_COMPLETED in the tests instead of a string literal.
Jason Novinger 2 недель назад
Родитель
Сommit
7d46c995f7

+ 11 - 5
netbox/extras/tests/test_scripts_deletion.py

@@ -7,7 +7,7 @@ from django.db.models import QuerySet
 from django.test import TestCase, override_settings
 from django.test import TestCase, override_settings
 from django.urls import reverse
 from django.urls import reverse
 
 
-from core.choices import ManagedFileRootPathChoices
+from core.choices import JobStatusChoices, ManagedFileRootPathChoices
 from core.models import DataSource, Job
 from core.models import DataSource, Job
 from extras.models import Script, ScriptModule
 from extras.models import Script, ScriptModule
 from extras.validators import CustomValidator
 from extras.validators import CustomValidator
@@ -43,7 +43,7 @@ class ScriptDeletionTestCase(TestCase):
                 object_type=object_type,
                 object_type=object_type,
                 object_id=obj.pk,
                 object_id=obj.pk,
                 name='testjob',
                 name='testjob',
-                status='completed',
+                status=JobStatusChoices.STATUS_COMPLETED,
                 job_id=uuid.uuid4(),
                 job_id=uuid.uuid4(),
                 data={'output': 'x' * 50},
                 data={'output': 'x' * 50},
             )
             )
@@ -164,7 +164,7 @@ class ConfirmCollectorTestCase(TestCase):
         script = Script.objects.create(module=module, name=f'S{uuid.uuid4().hex[:8]}')
         script = Script.objects.create(module=module, name=f'S{uuid.uuid4().hex[:8]}')
         ct = ContentType.objects.get_for_model(Script, for_concrete_model=False)
         ct = ContentType.objects.get_for_model(Script, for_concrete_model=False)
         Job.objects.bulk_create([
         Job.objects.bulk_create([
-            Job(object_type=ct, object_id=script.pk, name='j', status='completed',
+            Job(object_type=ct, object_id=script.pk, name='j', status=JobStatusChoices.STATUS_COMPLETED,
                 job_id=uuid.uuid4(), data={'output': 'x' * 50})
                 job_id=uuid.uuid4(), data={'output': 'x' * 50})
             for _ in range(count)
             for _ in range(count)
         ])
         ])
@@ -226,7 +226,10 @@ class ObjectDeleteViewCountsTestCase(ViewTestCase):
         script = Script.objects.create(module=module, name=f'S{uuid.uuid4().hex[:8]}')
         script = Script.objects.create(module=module, name=f'S{uuid.uuid4().hex[:8]}')
         ct = ContentType.objects.get_for_model(Script, for_concrete_model=False)
         ct = ContentType.objects.get_for_model(Script, for_concrete_model=False)
         Job.objects.bulk_create([
         Job.objects.bulk_create([
-            Job(object_type=ct, object_id=script.pk, name='j', status='completed', job_id=uuid.uuid4())
+            Job(
+                object_type=ct, object_id=script.pk, name='j',
+                status=JobStatusChoices.STATUS_COMPLETED, job_id=uuid.uuid4(),
+            )
             for _ in range(50)
             for _ in range(50)
         ])
         ])
 
 
@@ -249,7 +252,10 @@ class ObjectDeleteViewCountsTestCase(ViewTestCase):
         script = Script.objects.create(module=module, name=f'S{uuid.uuid4().hex[:8]}')
         script = Script.objects.create(module=module, name=f'S{uuid.uuid4().hex[:8]}')
         ct = ContentType.objects.get_for_model(Script, for_concrete_model=False)
         ct = ContentType.objects.get_for_model(Script, for_concrete_model=False)
         Job.objects.bulk_create([
         Job.objects.bulk_create([
-            Job(object_type=ct, object_id=script.pk, name='j', status='completed', job_id=uuid.uuid4())
+            Job(
+                object_type=ct, object_id=script.pk, name='j',
+                status=JobStatusChoices.STATUS_COMPLETED, job_id=uuid.uuid4(),
+            )
             for _ in range(50)
             for _ in range(50)
         ])
         ])
 
 

+ 4 - 1
netbox/netbox/models/features.py

@@ -464,7 +464,10 @@ def batch_delete_jobs(job_queryset):
     # Re-slice the queryset each iteration: it re-queries after each batch delete, so the
     # Re-slice the queryset each iteration: it re-queries after each batch delete, so the
     # remaining set shrinks and the loop terminates (do not hoist this into a cursor).
     # remaining set shrinks and the loop terminates (do not hoist this into a cursor).
     while pks := list(job_pks[:JOB_DELETE_BATCH_SIZE]):
     while pks := list(job_pks[:JOB_DELETE_BATCH_SIZE]):
-        Job.objects.filter(pk__in=pks).delete()
+        # only('pk'): the batch still can't fast-delete (a global pre_delete receiver forces
+        # per-instance signals), so each Job in the batch is instantiated. Loading just the PK
+        # avoids pulling the large data/log_entries payloads into those instances.
+        Job.objects.filter(pk__in=pks).only('pk').delete()
 
 
 
 
 class JobsMixin(models.Model):
 class JobsMixin(models.Model):

+ 1 - 1
netbox/templates/htmx/delete_form.html

@@ -30,7 +30,7 @@
                 {# High-cardinality relations (e.g. jobs) are summarized by count and are not #}
                 {# High-cardinality relations (e.g. jobs) are summarized by count and are not #}
                 {# expandable, since their instances are intentionally not loaded (see #22812). #}
                 {# expandable, since their instances are intentionally not loaded (see #22812). #}
                 {% if instances.count_only %}
                 {% if instances.count_only %}
-                  <div class="accordion-button collapsed no-toggle" style="cursor: default;">
+                  <div class="accordion-button collapsed" style="cursor: default;">
                     {{ object_count }}
                     {{ object_count }}
                     {% if object_count == 1 %}{{ model|meta:"verbose_name" }}{% else %}{{ model|meta:"verbose_name_plural" }}{% endif %}
                     {% if object_count == 1 %}{{ model|meta:"verbose_name" }}{% else %}{{ model|meta:"verbose_name_plural" }}{% endif %}
                   </div>
                   </div>