Skip to content

PR-4: an aired announcement is recorded once (STORY-469) - #836

Merged
genwave-radio merged 1 commit into
mainfrom
host/aired-at-least-once-469
Sep 23, 2026
Merged

genwave-radio merged 1 commit into
mainfrom
host/aired-at-least-once-469

Conversation

@genwave-radio

Copy link
Copy Markdown
Collaborator

🎯 What

PR-4 of the launch-polish epic: STORY-469, an aired announcement is recorded once (SPEC F202). One commit ahead of main.

Fixes #773.

  • T554: the aired queue never refuses a signal. AnnouncementAiredEventSink writes to an unbounded channel (F202.1).
  • T554: the drain retries. AnnouncementAiredDrainService retries MarkAiredAsync at 1 s, 5 s and 30 s on a TimeProvider, then logs one WARN with the id and drops it; the guardian's re-arm sweep remains the fallback (F202.2).
  • T554: a duplicate is a no-op. MarkAiredAsync only stamps rows where aired_at is null, so a second signal returns null silently (F202.3).
  • T554: the booth log can't undo the stamp. A booth-log append failure is logged on its own and no longer affects the aired stamp.
  • T554: the grace stays 6 min (F202.4). Residual: a restart can still re-air once.

🔬 Wire evidence (T555, dev station on the branch image)

Announcement #4 was posted through POST /api/announcements on a settled station. After it was claimed, the db was docker paused for ~3 s across each item change until the announcement was on air:

Check Result
Air Started inside the pause 14:55:10.67 → 14:55:13.78
aired_at 14:55:13.71, stamped as the db came back
announcement-aired booth rows 1
Announcement airs in the next 10 min 1 (no re-air)
Guardian re-arms / drain WARNs none

A paused container stalls the connection rather than refusing it, so this run proves the stamp survives a db stall. The fault-and-retry path itself (AC4, AC6, AC7 and the 1/5/30 s backoff) is pinned by the Story469 facts on a fake clock.

Full solution (dotnet test GenWave.sln --filter "Category!=Integration", Host with MaxParallelThreads=3): 0 failed across all 9 projects (Host 3090 / 40 skipped, MediaLibrary 177). MediaLibrary Integration: 34/34, including the two new real-Postgres idempotency facts.

⚠️ Follow-ups (not fixed here)

  1. An announcement that airs after the 6-min re-arm grace is never recorded as aired #835: when the buffer is deeper than the 6-min grace, for example right after an api restart, the guardian re-arms a still-queued announcement and its air is never stamped. Found by the first smoke run; it predates this PR.
  2. The WaitUntilAsync-style helpers in Story374/375/379 duplicate each other; Story469's CountingFakeTimeProvider is a fourth shape of the same idea.

The aired-confirmation queue is now unbounded, so a signal is never
refused. The drain retries MarkAiredAsync at 1 s, 5 s and 30 s on a
TimeProvider, then logs one WARN with the id and drops it; the
guardian's re-arm sweep stays the fallback. MarkAiredAsync only stamps
rows whose aired_at is null, so a duplicate signal is a silent no-op.
A booth-log append failure no longer undoes the aired stamp.

Story469 AC1-AC7 un-skipped; the idempotency facts run against real
Postgres in MediaLibrary.Tests.
@genwave-radio
genwave-radio merged commit 5cc0f4e into main Sep 23, 2026
11 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 23, 2026
@genwave-radio
genwave-radio deleted the host/aired-at-least-once-469 branch September 23, 2026 16:14
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

announcements: a dropped aired-confirmation re-arms the row and airs it twice — make delivery at-least-once

1 participant