diff --git a/src/db.py b/src/db.py index 36590a4..0b77610 100644 --- a/src/db.py +++ b/src/db.py @@ -67,6 +67,17 @@ def init_db(): # (CREATE TABLE IF NOT EXISTS won't add columns to an already-created table). _ensure_column(conn, "jobs", "transient_attempts", "INTEGER NOT NULL DEFAULT 0") _ensure_column(conn, "jobs", "available_at", "TEXT") + # ORCH-5 (M-7): webhook delivery de-dup. Add events.delivery_id and a PARTIAL + # unique index. Partial (WHERE delivery_id IS NOT NULL) so pre-existing rows + # (which have NULL delivery_id) never collide with each other. Restart-safe: + # _ensure_column is a no-op once the column exists, and CREATE INDEX IF NOT + # EXISTS is a no-op once the index exists, so this is safe on the live prod DB. + _ensure_column(conn, "events", "delivery_id", "TEXT") + conn.execute( + "CREATE UNIQUE INDEX IF NOT EXISTS idx_events_delivery " + "ON events(delivery_id) WHERE delivery_id IS NOT NULL" + ) + conn.commit() conn.close() @@ -141,6 +152,33 @@ def get_next_work_item_id(repo: str, prefix: str = "ET") -> str: return f"{prefix}-{next_num:03d}" +# --------------------------------------------------------------------------- +# ORCH-5 (M-7): idempotent webhook event logging +# --------------------------------------------------------------------------- + +def insert_event_dedup( + source: str, event_type: str, payload: str, delivery_id: str +) -> bool: + """Idempotently log a webhook event keyed by delivery_id. + + Returns True if a NEW row was inserted (caller should dispatch the event) and + False if this delivery_id was already present (a duplicate delivery -> caller + must skip dispatch/enqueue). Uses INSERT OR IGNORE against the partial UNIQUE + index idx_events_delivery; rowcount==1 means the row was actually inserted. + """ + conn = get_db() + try: + cur = conn.execute( + "INSERT OR IGNORE INTO events (source, event_type, payload, delivery_id) " + "VALUES (?, ?, ?, ?)", + (source, event_type, payload, delivery_id), + ) + conn.commit() + return cur.rowcount == 1 + finally: + conn.close() + + # --------------------------------------------------------------------------- # ORCH-1 (F-2b): job queue helpers # ---------------------------------------------------------------------------