Skip to content

Commit cc624f7

Browse files
chriswaynehclaude
andcommitted
Recover detection runs interrupted by a restart
A detection run completes inside its request, so one still marked active at startup did not finish. Leaving it that way would have been worse than untidy: an overlapping run is refused while one looks active, so a run left behind by a crash would have blocked that Dockyard's detection permanently. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent f379489 commit cc624f7

3 files changed

Lines changed: 44 additions & 1 deletion

File tree

backend/app/detection/runner.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,25 @@ def get_run(session: Session, dockyard_id: int, run_id: int) -> DetectionRun | N
123123
)
124124

125125

126+
def recover_interrupted_runs(session: Session) -> int:
127+
"""Mark detection runs that a restart interrupted.
128+
129+
A detection run completes inside its request, so one still marked active at
130+
startup did not finish. Saying so matters twice over: the record is honest,
131+
and an overlapping run is refused while one looks active, so a run left
132+
behind by a crash would otherwise block this Dockyard's detection for good.
133+
"""
134+
interrupted = list(
135+
session.scalars(select(DetectionRun).where(DetectionRun.status.in_(ACTIVE_STATUSES)))
136+
)
137+
for run in interrupted:
138+
run.status = str(DetectionRunStatus.FAILED)
139+
run.error = "Interrupted by a RedDock restart"
140+
run.completed_at = datetime.now(UTC)
141+
session.commit()
142+
return len(interrupted)
143+
144+
126145
def execute_run(session: Session, run: DetectionRun) -> DetectionRun:
127146
"""Run every registered detector over the Dockyard snapshot. Never raises."""
128147
run.status = str(DetectionRunStatus.RUNNING)

backend/app/main.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from app.api import router
1010
from app.config import get_settings
1111
from app.database import SessionLocal, initialize_database
12+
from app.detection.runner import recover_interrupted_runs as recover_interrupted_detections
1213
from app.discovery.runner import recover_interrupted_runs
1314

1415
STATIC_DIRECTORY = Path(__file__).resolve().parents[2] / "static"
@@ -21,10 +22,14 @@ async def lifespan(_: FastAPI):
2122
initialize_database()
2223
with SessionLocal() as session:
2324
# A run that was in flight when the process stopped did not finish.
24-
# Saying so is more useful than leaving it looking active forever.
25+
# Saying so is more useful than leaving it looking active forever, and a
26+
# detection run left active would keep refusing the next one.
2527
interrupted = recover_interrupted_runs(session)
28+
detections = recover_interrupted_detections(session)
2629
if interrupted:
2730
logger.warning("Marked %s discovery run(s) as interrupted by restart", interrupted)
31+
if detections:
32+
logger.warning("Marked %s detection run(s) as interrupted by restart", detections)
2833
yield
2934

3035

backend/tests/test_detection_runner.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -672,3 +672,22 @@ def test_detection_uses_the_snapshot_time_for_seen_timestamps(
672672
stored = finding.first_seen.replace(tzinfo=UTC)
673673
assert finding.first_seen == finding.last_seen
674674
assert stored >= before.replace(microsecond=0)
675+
676+
677+
def test_a_detection_run_interrupted_by_a_restart_is_marked_and_unblocks_the_next(
678+
endpoint: Recorder, monkeypatch: pytest.MonkeyPatch
679+
):
680+
"""An overlapping run is refused, so a stale active run must not be permanent."""
681+
from app.models import DetectionRun
682+
683+
stale = DetectionRun(dockyard_id=endpoint.dockyard_id, status="running")
684+
endpoint.session.add(stale)
685+
endpoint.session.commit()
686+
687+
assert detection_runner.recover_interrupted_runs(endpoint.session) == 1
688+
endpoint.session.refresh(stale)
689+
assert stale.status == "failed"
690+
assert "restart" in stale.error
691+
692+
install(monkeypatch, StubDetector())
693+
assert detect(endpoint).status == "completed"

0 commit comments

Comments
 (0)