paho delivers on one goroutine, so bills were committed strictly one after another. Each is a full Postgres transaction — advisory lock, dedup, stock row locks, availability check, four inserts, commit — which is 10-30ms, so the ceiling was roughly 30-100 bills a second and a shop's backlog draining after an outage took minutes to land. A fixed pool behind a bounded queue, rather than a goroutine per message. Unbounded concurrency would open a transaction per message and exhaust the connection pool under a storm, stalling every one of them at once — a slow minute turned into a dead one. When the queue fills, submit blocks: paho stops acknowledging, the broker's in-flight window fills, it stops sending, and the backpressure reaches the till, which holds its bills and retries. Slow, but nothing is dropped. Heartbeats get their own pool. Sharing one would let a backlog of bills delay presence, so every till would appear to go dark at exactly the moment the system was busiest — the worst time to be blind to which counters are alive. Payloads are copied on the way in. paho reuses its buffer once a handler returns and the work now happens after that, so a queued bill would otherwise be read as whatever message arrived next — silently, and as valid JSON often enough to commit the wrong sale. One bug found by its own test: submit-after-stop selected between a done-channel and the job channel, and once both were ready Go picks at random. Picking the send panics on a closed channel. It would have shown up in production as an occasional crash during shutdown and nowhere else. Now guarded by an RWMutex held across the send, so the queue cannot be closed under one in progress. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
5.9 KiB
5.9 KiB