Run child tables cascade from run.id
Migration ccb21eacf5d5 deletes rows whose run is gone, then adds foreign keys with ON DELETE CASCADE from run_node, run_metric and run_artifact to run.id. Deleting runs is now the one statement. A node or metric written after its run was deleted is dropped with an info line. Migrations run with foreign keys off, since a batch rebuild of run would otherwise cascade into every child row.
This commit is contained in:
@@ -7,6 +7,7 @@ The pipeline half — what a hit restores and what a key is made of — is in
|
||||
import base64
|
||||
import csv
|
||||
import json
|
||||
import logging
|
||||
import threading
|
||||
from datetime import UTC, datetime, timedelta
|
||||
|
||||
@@ -41,6 +42,10 @@ def test_a_run_cache_finds_what_an_earlier_run_recorded(tmp_path):
|
||||
reference = store.put([b"payload"], name="data.bin")
|
||||
plain, with_artifact, collected = "k-plain", "k-artifact", "k-collected"
|
||||
with Session(db_engine) as session:
|
||||
for run_id in ("cache-1", "cache-2", "cache-3"):
|
||||
session.add(
|
||||
Run(id=run_id, flow="study", status="ok", created_at=datetime.now(UTC))
|
||||
)
|
||||
session.add(
|
||||
RunNode(
|
||||
run_id="cache-1",
|
||||
@@ -891,7 +896,7 @@ def test_overview_is_not_read_as_a_run_id(client, superuser_token_headers):
|
||||
# -----------------------------------------------------------------------------
|
||||
# Deleting a run
|
||||
#
|
||||
# The route owns the four statements; what these guard is that it takes the
|
||||
# The foreign keys cascade; what these guard is that deleting a run takes the
|
||||
# children with it and refuses a run the driver is still writing to.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -928,7 +933,7 @@ def deletable_run():
|
||||
def test_deleting_a_run_takes_its_children_with_it(
|
||||
client, superuser_token_headers, deletable_run
|
||||
):
|
||||
"""No foreign key cascades here, so the route has to do it itself."""
|
||||
"""The foreign keys cascade, which needs them on for every connection."""
|
||||
answer = client.delete(
|
||||
f"{settings.API_V1_STR}/runs/{deletable_run}", headers=superuser_token_headers
|
||||
)
|
||||
@@ -943,6 +948,18 @@ def test_deleting_a_run_takes_its_children_with_it(
|
||||
assert left == [], f"{table.__name__} rows outlived the run"
|
||||
|
||||
|
||||
def test_a_node_finishing_after_its_run_was_deleted_is_dropped_quietly(caplog):
|
||||
"""The constraint refuses its row, and that is a no-op rather than an error."""
|
||||
service = RunService(controller=_OneFlow(FlowDef(name="gone")), queue=_Collect())
|
||||
with caplog.at_level(logging.INFO, logger="fluksio.flow.runs"):
|
||||
service._record_node("del-gone", NodeOutcome(node="gone.a", ok=True))
|
||||
|
||||
with Session(db_engine) as session:
|
||||
assert session.get(RunNode, {"run_id": "del-gone", "node": "gone.a"}) is None
|
||||
assert "is gone" in caplog.text
|
||||
assert not [record for record in caplog.records if record.levelno >= logging.ERROR]
|
||||
|
||||
|
||||
def test_deleting_a_run_that_is_not_there_is_a_404(client, superuser_token_headers):
|
||||
answer = client.delete(
|
||||
f"{settings.API_V1_STR}/runs/nope-1", headers=superuser_token_headers
|
||||
|
||||
Reference in New Issue
Block a user