Move the execution_time backfill into its own non-atomic migration
Batching the backfill bounded statement size but not lock duration: sharing a transaction with the AddField meant the ACCESS EXCLUSIVE lock from ALTER TABLE was held for the whole run, which is exactly the case the batching was meant to help. 0025 goes back to adding the column only, and the backfill moves to 0026 with atomic = False so the lock is released first. The backfill now also skips rows which already have a value, making it idempotent and letting an interrupted run simply be resumed. As a separate migration it additionally reaches installations which had already applied 0025, rather than silently leaving their historical jobs unpopulated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
bab3ccd216
commit
5346dab1c3
|
|
@ -1,28 +1,4 @@
|
|||
from django.db import migrations, models
|
||||
from django.db.models import DurationField, ExpressionWrapper, F
|
||||
|
||||
BATCH_SIZE = 5000
|
||||
|
||||
|
||||
def populate_execution_time(apps, schema_editor):
|
||||
"""
|
||||
Populate execution_time for existing jobs which have both a start and completion time recorded.
|
||||
Updates are performed in batches, as installations which retain job history indefinitely can
|
||||
accumulate a very large number of rows.
|
||||
"""
|
||||
Job = apps.get_model("core", "Job")
|
||||
queryset = Job.objects.filter(started__isnull=False, completed__isnull=False)
|
||||
execution_time = ExpressionWrapper(F("completed") - F("started"), output_field=DurationField())
|
||||
|
||||
last_pk = 0
|
||||
while True:
|
||||
pks = list(
|
||||
queryset.filter(pk__gt=last_pk).order_by("pk").values_list("pk", flat=True)[:BATCH_SIZE]
|
||||
)
|
||||
if not pks:
|
||||
break
|
||||
Job.objects.filter(pk__in=pks).update(execution_time=execution_time)
|
||||
last_pk = pks[-1]
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
|
@ -37,8 +13,4 @@ class Migration(migrations.Migration):
|
|||
name="execution_time",
|
||||
field=models.DurationField(blank=True, editable=False, null=True),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=populate_execution_time,
|
||||
reverse_code=migrations.RunPython.noop,
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
from django.db import migrations
|
||||
from django.db.models import DurationField, ExpressionWrapper, F
|
||||
|
||||
BATCH_SIZE = 5000
|
||||
|
||||
|
||||
def populate_execution_time(apps, schema_editor):
|
||||
"""
|
||||
Populate execution_time for existing jobs which have both a start and completion time recorded.
|
||||
Updates are performed in batches, as installations which retain job history indefinitely can
|
||||
accumulate a very large number of rows. Rows which already have a value are skipped, so that an
|
||||
interrupted run can simply be resumed.
|
||||
"""
|
||||
Job = apps.get_model("core", "Job")
|
||||
queryset = Job.objects.filter(
|
||||
started__isnull=False, completed__isnull=False, execution_time__isnull=True
|
||||
)
|
||||
execution_time = ExpressionWrapper(F("completed") - F("started"), output_field=DurationField())
|
||||
|
||||
last_pk = 0
|
||||
while True:
|
||||
pks = list(
|
||||
queryset.filter(pk__gt=last_pk).order_by("pk").values_list("pk", flat=True)[:BATCH_SIZE]
|
||||
)
|
||||
if not pks:
|
||||
break
|
||||
Job.objects.filter(pk__in=pks).update(execution_time=execution_time)
|
||||
last_pk = pks[-1]
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
# The backfill is deliberately kept out of the migration which adds the column, so that the
|
||||
# ACCESS EXCLUSIVE lock taken by ALTER TABLE is not held for its duration. Running without a
|
||||
# wrapping transaction is what allows the batching above to bound the work actually held open.
|
||||
atomic = False
|
||||
|
||||
dependencies = [
|
||||
("core", "0025_add_job_execution_time"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
code=populate_execution_time,
|
||||
reverse_code=migrations.RunPython.noop,
|
||||
),
|
||||
]
|
||||
Loading…
Reference in New Issue