Every wait in production code (time.Sleep, time.After, time.NewTicker,
time.NewTimer, context.WithTimeout) must go through
domain/clock.Clock and be cancellable via ctx. This document describes
the invariant, the enforcement, and the readiness APIs that replaced
sleep-based test synchronisation.
| Class | Meaning | Action |
|---|---|---|
| REPLACEABLE | Sleep substitutes for a missing signal | Add the missing event/channel/hook; delete the sleep |
| DEADLINE | Upper bound on a bounded operation | Derive from operational config; never hardcode |
| ESSENTIAL | Genuine waiting (backoff, protocol cadence) | Keep, make cancellable via select { ctx; clk.After }, expose in config, route through Clock |
Production: zero unauthorized timing calls. Every wait across
adapters/, runtime/, bridge/, processors/, circuitbreaker/,
and httpapi/ routes through domain/clock.Clock. The Clock
interface deliberately omits Sleep — every wait is
select { case <-ctx.Done(): case <-clk.After(d): }, enforcing
cancellability at the type level.
Tests: audit/test-timing-allowlist.txt tracks the small
irreducible set of time.Sleep calls in tests, each annotated with
its classification comment (e.g. // ESSENTIAL: protocol cadence,
// OTHER: real-time component of clocktest.Fake).
An entry matches on the FILE and the CODE, not on the line number it
carries. The line numbers are provenance, not part of the key: an
allowed sleep that moves down its file because something above it was
edited is the same allowed sleep, and requiring a renumber there
failed the audit on files nobody had touched. Changing the sleep
itself, or moving it to another file, still needs a new entry — the
exemption is for that call, not for a path.
scripts/audit-timing-filter.awk implements the match.
Enforcement: make audit-timings and make audit-test-timings
run on every make test / make check / make test-integration and
fail the build on new unauthorized entries. See the header comments
in audit/timing-allowlist.txt and audit/test-timing-allowlist.txt
for how to justify a new entry.
These are the only legitimate kinds of wait that survive event-signal elimination:
Anything outside these three categories should be driven by an event signal. The readiness API inventory below is the catalogue of those signals.
| Component | API | Signal type | When signaled | File |
|---|---|---|---|---|
ports.Receiver (optional) |
Started() <-chan struct{} |
Channel close | Receive loop live | ports/transport.go |
ports.Session |
Health(ctx) SessionHealth |
Struct poll | Anytime | ports/transport.go |
ports.Session |
Events() <-chan SessionEvent |
Event push | Connect/disconnect/reconcile | ports/transport.go |
ports.SessionHealth |
ActiveTopics []string / HasTopic() |
Struct field | From session Health() |
ports/transport.go |
runtime.Runtime |
DeepHealth(ctx) DeepHealth |
Struct poll | Anytime | runtime/bridge.go |
runtime.Runtime |
WaitRouteReady(ctx, routeID) error |
Event-driven wait | Route runner + receiver Started() closed (sanity fallback 1s) |
runtime/bridge.go |
runtime.Runtime |
WaitQuiescent(ctx, opts) error |
Event-driven wait | Per-route IdleChanged() fan-in; zero in-flight for MinQuiet |
runtime/bridge.go |
runtime.Runtime |
ReadinessLevel(ctx) ReadinessLevel |
Snapshot enum | Down/Live/Running/Connected/Subscribed/Full | runtime/bridge.go |
runtime.Runtime |
AtLeast(ctx, want) bool |
Snapshot bool | Has reached requested level | runtime/bridge.go |
HTTP /api/v1/monitor/ready?level= |
level= query param | HTTP 200/503 | live, running, connected, subscribed, full | httpapi/monitor.go |
HTTP /api/v1/monitor/deephealth |
JSON body | Per-session active_topics, per-route ready + in_flight, current level |
exposed in response | httpapi/monitor.go |
runtime.RouteRunner |
Started() <-chan struct{} |
Channel close | Run() entered |
runtime/route/runner.go |
runtime.RouteRunner |
InFlight() int64 |
Atomic counter | Per delivery enter/exit | runtime/route/runner.go |
runtime.RouteRunner |
IdleChanged() <-chan struct{} |
Channel close + swap | InFlight transitions → 0 | runtime/route/runner.go |
runtime.RouteHealth |
Ready bool / InFlight int |
Struct fields | From DeepHealth() |
runtime/bridge.go |
runtime.OutboxDrainer |
IdleSince() (time.Time, bool) |
Atomic timestamp | Pending → empty transition | runtime/outbox/drainer.go |
runtime.OutboxDrainer |
WaitIdle(ctx, minQuiet) error |
Blocking wait | Continuously idle for minQuiet |
runtime/outbox/drainer.go |
runtime.SessionManager |
LeaseStateChanged() <-chan LeaseStateEvent |
Event push | Acquire/renew/release/stepdown/loss | runtime/session/manager.go |
paho.Receiver |
Started() <-chan struct{} |
Channel close | Handler registered on router | adapters/mqtt/transport/paho/receiver.go |
paho.Session |
Health() / Events() |
Poll + event | Connect/reconcile/disconnect | adapters/mqtt/transport/paho/session.go |
http.SSESender |
WaitClientConnected(ctx, n) error |
Blocking wait | N clients connected | adapters/http/transport/sender_sse.go |
file.Watcher |
Started() <-chan struct{} |
Channel close | fsnotify registered | adapters/native/config/file/acl_watcher.go |
file.Watcher |
LastApplied() time.Time |
Atomic timestamp | Config successfully applied | adapters/native/config/file/acl_watcher.go |
The testutil/testcontent package provides deterministic message
content verification. Every test message is tagged with a unique TID
(test-message-ID) in both a header (x-bridge.test-msg-id) and a
JSON payload field (_tid).
Assertion helpers compare sent vs received sets by TID:
AssertReceivedSet — set(sent.TID) == set(received.TID); reports missing and extraAssertContentMatches — set equality + per-TID payload/header comparisonAssertNoDuplicates — each TID appears at most onceAssertOrdered — received TIDs appear in the same order as sentThe ExtractTID function tries the header first, then falls back to
the JSON _tid field, so it works even when transport headers are
stripped (e.g. SQS → body-only polling).
MQTT Envelope.ID round-trip: PublishFromEnvelope stamps the
Envelope.ID as a mqtt.message-id user property, so a peer bridge’s
producer identity survives the hop. EnvelopeFromPublish recovers the
inbound identity in precedence order:
mqtt.message-id user property (the producer ID a peer stamped);x-bridge.correlation-id);newIngressEnvelopeID). This is not a topic+payload hash: two
legitimate equal-valued publishes with no producer ID get distinct IDs, so
shared_outbox does not silently collapse them. See
MQTT — Envelope identity and no-ID redelivery.This enables countUnique() to work correctly on MQTT collectors for
all throughput, resilience, and backpressure tests.