amazon: add a Ready() signal for ProcessEvents' LISTEN registration
Tests (and any other caller) previously had no way to know when the
Postgres LISTEN behind the reactive event-processing loop had actually
registered, forcing a guessed sleep before relying on it. Mocks now
exposes Ready() <-chan struct{}, closed once listenForNotifications
successfully issues LISTEN. Purely additive - ProcessEvents' signature
is unchanged.
Updates the integration tests to wait on Ready() instead of a flat
sleep, which also cut TestProcessEvents_ReactsToNotification's runtime
from ~0.25s to ~0.06s with no flakes across repeated runs.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XEDaCB7C2NEBgyvqEtZuxY
This commit is contained in:
+17
-2
@@ -7,6 +7,7 @@ import (
|
|||||||
"ruben/inventory2/domains/accounts"
|
"ruben/inventory2/domains/accounts"
|
||||||
"ruben/inventory2/domains/raw_events"
|
"ruben/inventory2/domains/raw_events"
|
||||||
"ruben/inventory2/logging"
|
"ruben/inventory2/logging"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
@@ -18,6 +19,9 @@ type (
|
|||||||
log *logging.Logger
|
log *logging.Logger
|
||||||
db *pgxpool.Pool
|
db *pgxpool.Pool
|
||||||
listener MockEventListener
|
listener MockEventListener
|
||||||
|
|
||||||
|
ready chan struct{}
|
||||||
|
readyOnce sync.Once
|
||||||
}
|
}
|
||||||
|
|
||||||
MockEventListener interface {
|
MockEventListener interface {
|
||||||
@@ -31,8 +35,9 @@ const (
|
|||||||
|
|
||||||
func NewMocks(log *logging.Logger, db *pgxpool.Pool) *Mocks {
|
func NewMocks(log *logging.Logger, db *pgxpool.Pool) *Mocks {
|
||||||
return &Mocks{
|
return &Mocks{
|
||||||
log: log,
|
log: log,
|
||||||
db: db,
|
db: db,
|
||||||
|
ready: make(chan struct{}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,6 +46,14 @@ func (m *Mocks) SetListener(l MockEventListener) *Mocks {
|
|||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ready returns a channel that's closed once ProcessEvents has registered
|
||||||
|
// its Postgres LISTEN and is actively watching for notifications. Callers
|
||||||
|
// that need to know the reactive path is live - tests in particular -
|
||||||
|
// should wait on this instead of guessing with a sleep.
|
||||||
|
func (m *Mocks) Ready() <-chan struct{} {
|
||||||
|
return m.ready
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Mocks) ProcessEvents(ctx context.Context) error {
|
func (m *Mocks) ProcessEvents(ctx context.Context) error {
|
||||||
notifCh, errCh := m.listenForNotifications(ctx)
|
notifCh, errCh := m.listenForNotifications(ctx)
|
||||||
|
|
||||||
@@ -83,6 +96,8 @@ func (m *Mocks) listenForNotifications(ctx context.Context) (<-chan struct{}, <-
|
|||||||
return nil, errCh
|
return nil, errCh
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m.readyOnce.Do(func() { close(m.ready) })
|
||||||
|
|
||||||
ch := make(chan struct{})
|
ch := make(chan struct{})
|
||||||
|
|
||||||
go func() (err error) {
|
go func() (err error) {
|
||||||
|
|||||||
@@ -184,14 +184,11 @@ func TestProcessEvents_ReactsToNotification(t *testing.T) {
|
|||||||
errCh <- m.ProcessEvents(ctx)
|
errCh <- m.ProcessEvents(ctx)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// ProcessEvents registers its Postgres LISTEN synchronously before
|
select {
|
||||||
// entering its main loop, but that registration still races against
|
case <-m.Ready():
|
||||||
// this goroutine actually getting scheduled - the production code
|
case <-time.After(5 * time.Second):
|
||||||
// exposes no "listening now" signal a caller (or a test) can wait
|
t.Fatal("ProcessEvents() did not become ready (LISTEN registered) within 5s")
|
||||||
// on. A short sleep is the only lever available from outside; see
|
}
|
||||||
// the accompanying report for why that's a real testability/design
|
|
||||||
// gap, not just a test-flakiness workaround.
|
|
||||||
time.Sleep(200 * time.Millisecond)
|
|
||||||
|
|
||||||
insertRawAmazonEvent(t, pool, shopID, "evt-1")
|
insertRawAmazonEvent(t, pool, shopID, "evt-1")
|
||||||
|
|
||||||
@@ -234,7 +231,11 @@ func TestProcessEvents_ShutsDownOnContextCancel(t *testing.T) {
|
|||||||
errCh <- m.ProcessEvents(ctx)
|
errCh <- m.ProcessEvents(ctx)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
time.Sleep(50 * time.Millisecond) // let it reach its first select
|
select {
|
||||||
|
case <-m.Ready():
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Fatal("ProcessEvents() did not become ready (LISTEN registered) within 5s")
|
||||||
|
}
|
||||||
cancel()
|
cancel()
|
||||||
|
|
||||||
select {
|
select {
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Work Summary — 2026-08-05 19:13
|
||||||
|
|
||||||
|
## Task
|
||||||
|
First of the four `domains/amazon` design insights, addressed one at a time per user request: no "now listening" readiness signal from `(*Mocks).ProcessEvents`.
|
||||||
|
|
||||||
|
## Change
|
||||||
|
`domains/amazon/mock.go`:
|
||||||
|
- `Mocks` gained `ready chan struct{}` (initialized in `NewMocks`) and a `readyOnce sync.Once` guard.
|
||||||
|
- New exported method `Ready() <-chan struct{}` - closed once `listenForNotifications` successfully issues `LISTEN` on Postgres, i.e. the moment the reactive path is actually live.
|
||||||
|
- `listenForNotifications` calls `m.readyOnce.Do(func() { close(m.ready) })` right after the `LISTEN` exec succeeds (and before spawning the `WaitForNotification` goroutine).
|
||||||
|
- `ProcessEvents(ctx) error`'s signature is unchanged - this is purely additive, so `main.go`'s existing call site needed no changes.
|
||||||
|
|
||||||
|
`domains/amazon/mock_test.go`:
|
||||||
|
- `TestProcessEvents_ReactsToNotification` and `TestProcessEvents_ShutsDownOnContextCancel` now `select` on `m.Ready()` (bounded by a 5s timeout as a safety net) instead of a flat `time.Sleep(200 * time.Millisecond)` / `time.Sleep(50 * time.Millisecond)` before proceeding.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
- `go build ./...` / `go vet ./...` clean.
|
||||||
|
- `go test ./domains/amazon/... -v -race`: all 4 pass.
|
||||||
|
- `TestProcessEvents_ReactsToNotification` dropped from ~0.25s to ~0.06-0.07s per run (no longer paying for an arbitrary sleep) - and 10 consecutive runs (`-count=1` each) were all clean, no flakes.
|
||||||
|
- `make test`: full suite still green, no regressions.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
This closes insight #1 from `work-summaries/work-summary-Claude-2026-08-04-2318.md`. The other three (fire-and-forget listener notifications, hardcoded poll interval, LISTEN-connection errors being fatal to the whole app) are still open, to be addressed one at a time per the user's request - not done in this pass.
|
||||||
Reference in New Issue
Block a user