fix(ops_run): unique triggering_event_id per cron fire
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Build and Publish Container Image / build-and-push (push) Successful in 21s

Cron schedules always passed trigger_key="scheduled", so ops_run
idempotency collapsed every weekday into one key. After the first fire,
create_ops_run was a silent no-op, the claim loop starved, and dual-clock
host timers produced empty FI briefs.

Map scheduled fires to run_id (or scheduled:{iso}) via
emit_triggering_event_id; log duplicate skips; document the contract.
This commit is contained in:
tegwick 2026-08-05 15:30:00 +02:00
parent 00da21e39e
commit 21dc228cb1
6 changed files with 117 additions and 7 deletions

View file

@ -11,6 +11,7 @@ import pytest
from activity_core.ops_run_queue import (
build_idempotency_key,
emit_triggering_event_id,
max_attempts,
ops_run_queue_enabled,
ops_run_to_dict,
@ -39,6 +40,58 @@ def test_build_idempotency_key() -> None:
)
def test_emit_triggering_event_id_scheduled_uses_run_id() -> None:
"""Bare 'scheduled' must not be the ops_run key for every weekday fire."""
day1 = emit_triggering_event_id("scheduled", "run-day-1")
day2 = emit_triggering_event_id("scheduled", "run-day-2")
assert day1 == "run-day-1"
assert day2 == "run-day-2"
assert day1 != day2
def test_emit_triggering_event_id_prefers_scheduled_for() -> None:
got = emit_triggering_event_id(
"scheduled",
"run-fallback",
scheduled_for="2026-08-05T05:30:00+00:00",
)
assert got == "scheduled:2026-08-05T05:30:00+00:00"
def test_emit_triggering_event_id_passes_through_events_and_manual() -> None:
assert emit_triggering_event_id("evt-abc", "run-x") == "evt-abc"
assert (
emit_triggering_event_id("manual-3da4cf06", "run-y") == "manual-3da4cf06"
)
def test_scheduled_fires_produce_distinct_ops_keys() -> None:
"""Regression: two cron days must not share one ops_run idempotency key."""
def_id = "3169ab1f-882b-59c7-9763-d014dc96f4fc"
source = "emit-fi-daily-brief-task"
key_aug4 = build_idempotency_key(
TaskSpec(
title="FI",
activity_definition_id=def_id,
source_id=source,
triggering_event_id=emit_triggering_event_id("scheduled", "run-aug4"),
)
)
key_aug5 = build_idempotency_key(
TaskSpec(
title="FI",
activity_definition_id=def_id,
source_id=source,
triggering_event_id=emit_triggering_event_id("scheduled", "run-aug5"),
)
)
# Old bug: both would be "...:scheduled"
legacy = f"{def_id}:{source}:scheduled"
assert key_aug4 != key_aug5
assert key_aug4 != legacy
assert key_aug5 != legacy
def test_max_attempts(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("OPS_RUN_MAX_ATTEMPTS", raising=False)
assert max_attempts() == 3