You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(record-surrealdb-resolver): a stuck or malformed message can no longer take a NATS subject down
Closes the defect behind `domain.list: timeout`, which made reach-edu's corpora
look missing. They were never missing — all nine were in SurrealDB, reachable in
about a second. The service had lost the ability to answer anything on that
subject, for every client, and only a restart recovered it.
Three compounding defects in the consumer idiom, all closed here.
A subscription consumed with `for await` is strictly sequential, so one awaited
call that never settles halts the subject permanently — later messages are
delivered by NATS and dropped, silently. `msg.json()` sat outside the try, so a
single malformed payload threw out of the loop and killed the consumer while the
NATS subscription stayed registered and monitoring still reported the subject
healthy. And the loop lived in a bare `void (async () => {})()`, so its death
was an unhandled rejection with nothing in the logs.
New nats-loop.ts owns the parse, a per-message deadline, the always-answer
guarantee, and the loop's own death. It takes any AsyncIterable of reply-shaped
messages, which is what makes these failure modes testable — the old bug was
unreachable from the existing suite because every test went through a real
SurrealDB and none exercised the loop.
Deliberately still sequential. Processing concurrently would also fix
head-of-line blocking, but it would reorder writes on subjects like
domain.create.requested, and that semantic change is not needed: bounded
sequential turns "dead forever" into "one slow message", which is the actual bug.
getDb() is now bounded and shares one in-flight handshake. surrealdb's connect()
against a WSS endpoint has no deadline of its own, so a stalled handshake never
settled, `db` was never assigned, and the loop parked on it. The 10s ceiling is a
wide margin over the ~1.1s measured against Surreal Cloud from inside the
container. Because every consumer in the file begins with `await getDb()`, this
one change closes the observed hang for all ten of them.
Five paths now route through serveSubject and get all four protections:
domain.list, domain.assemble, tag.suggest, domain.create, and both
source.fetch/source.retry — the last of which had no try block at all, so any
throw killed both subjects instantly. The other five keep their hand-written
bodies with the parse moved inside the try and a .catch() on the consumer; they
have no per-message deadline, which the issue records rather than glosses.
Verified live against the running stack after rebuilding the container:
reach-edu returns its 9 domains, humain-vc 7, unfiltered 16. A malformed payload
answers ok:false in 3ms and the subject keeps serving — the exact regression.
9 new unit tests, and 87 tests across 7 suites all passing.
Only this service changed. The same idiom appears across the other NATS
services; nats-loop.ts was written to be liftable and that rollout is tracked
separately.
Fixes#83
Files changed:
- services/record-surrealdb-resolver/src/nats-loop.ts (new)
- services/record-surrealdb-resolver/test/nats-loop.test.ts (new)
- services/record-surrealdb-resolver/src/surreal.ts
- services/record-surrealdb-resolver/src/domains.ts
- context-v/issues/One-Stuck-Message-Kills-A-NATS-Subject-Until-Restart.md
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019a8tSPbFdvF1pKtADnWyDg
0 commit comments