mirror of
https://github.com/ArchiveBox/ArchiveBox.git
synced 2026-09-14 11:06:13 +05:00
Make per-snapshot crawl seal idempotent against the run_snapshot race
run_snapshot calls load_snapshot_payload which replaces self.crawl with a fresh Crawl model instance every time. When N snapshot tasks finish near-simultaneously their independent self.crawl/SM pairs all see 'STARTED + no open snapshots' and race on sm.seal(). The first task drives the SM to a final state (engine.running becomes False), but the loser's current_state still reads STARTED off its stale model field, so python-statemachine then raises 'Can't Seal when in Started.' and fails the whole snapshot task even though the crawl is already sealed. Refresh the row right before the call so the guard sees the committed status, and treat TransitionNotAllowed as the expected idempotent no-op when another task already drove the transition.
This commit is contained in:
parent
8ba658b4dc
commit
a48ce6871b
@ -1239,17 +1239,36 @@ class CrawlRunner:
|
||||
await sync_to_async(run_snapshot_maintenance, thread_sensitive=True)(snapshot_id, output_dir=output_dir)
|
||||
return
|
||||
await self.enqueue_discovered_snapshots_from_outputs(snapshot)
|
||||
await sync_to_async(
|
||||
lambda: (
|
||||
self.crawl.sm.seal()
|
||||
if self.crawl.status == self.crawl.StatusChoices.STARTED
|
||||
and not self.crawl.snapshot_set.filter(
|
||||
status__in=self.crawl.snapshot_set.model.OPEN_STATES,
|
||||
).exists()
|
||||
else None
|
||||
),
|
||||
thread_sensitive=True,
|
||||
)()
|
||||
|
||||
def _seal_when_last_snapshot_finished() -> None:
|
||||
# run_snapshot replaces self.crawl per-snapshot, so multiple
|
||||
# concurrent tasks each load a fresh Crawl/SM pointing at the
|
||||
# same DB row. The "no open snapshots" check is non-atomic
|
||||
# with sm.seal(), so two tasks racing to finish the last
|
||||
# snapshot can both pass the guard. The first call drives
|
||||
# the SM to a final state (engine.running=False); the loser
|
||||
# then raises TransitionNotAllowed even though current_state
|
||||
# still reads STARTED off its stale model field. Re-read the
|
||||
# row right before the call and swallow the race so the
|
||||
# task that lost the lap doesn't fail the whole snapshot.
|
||||
from statemachine.exceptions import TransitionNotAllowed
|
||||
|
||||
crawl = self.crawl
|
||||
crawl.refresh_from_db(fields=["status"])
|
||||
if crawl.status != crawl.StatusChoices.STARTED:
|
||||
return
|
||||
if crawl.snapshot_set.filter(
|
||||
status__in=crawl.snapshot_set.model.OPEN_STATES,
|
||||
).exists():
|
||||
return
|
||||
try:
|
||||
crawl.sm.seal()
|
||||
except TransitionNotAllowed:
|
||||
# Another task sealed it between our refresh and the
|
||||
# SM call. Idempotent by design.
|
||||
pass
|
||||
|
||||
await sync_to_async(_seal_when_last_snapshot_finished, thread_sensitive=True)()
|
||||
finally:
|
||||
snapshot_service.close()
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user