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.
This commit is contained in:
Jason Novinger 2026-08-05 14:04:19 -05:00
parent be69c55a99
commit 7d46c995f7
3 changed files with 16 additions and 7 deletions

View File

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

View File

@ -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
# remaining set shrinks and the loop terminates (do not hoist this into a cursor).
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):

View File

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