Skip to content

v2.5.3

Choose a tag to compare

@bpacholek bpacholek released this 12 Jul 20:50

A correctness release resolving the remaining reconnect/JetStream/WebSocket findings from the review lineage (issues #115, #121, #122, #165, #166). Every fix landed with a falsifiability-checked regression test and adversarial review; the tree is green on PHPStan level 8, 1610 unit tests, integration + behat against NATS 2.12, 97.9% coverage, and the >= 90% MSI mutation gate. All changes are backward-compatible bug fixes (patch release). See CHANGELOG.md for the full per-issue detail.

JetStream ordered consumers

  • The ordered-consumer recreate now rotates its deliver inbox (#122). Reusing one inbox across recreates meant a consumer created after a lost CONSUMER.CREATE reply (a transient the retry survives) could orphan a live server-side ephemeral, and its plain idle heartbeats could drive a recreate storm. Rotating to a fresh inbox on each recreate means an orphan's data and heartbeats never reach the new subscription, and dropping interest on the old inbox lets the server reap the orphan via its inactive_threshold - closing both the leak and the storm. On terminal recreate failure the subscription is torn down so "dead" is actually dead, and the watchdog timer is cancelled explicitly on rotation/teardown.

Reconnect liveness

  • The reconnect flush loop is now bounded (#165). Under sustained publish pressure a fiber that re-filled the reconnect buffer during each flush write could defer the Open transition indefinitely, stranding the connection in Connecting. After a bounded number of drain passes the buffer is sealed and late publishers park on a flush gate (writing directly once Open, failing loudly once Closed), so recovery reaches Open in bounded time with per-publisher and buffered-before-direct wire order preserved.

WebSocket data safety

  • A WebSocket close or fragmentation error no longer discards messages decoded from the same read chunk (#115). A close frame (or an RFC 6455 fragmentation violation) that shared a TCP read with data frames used to throw mid-batch, dropping the already-decoded messages. Terminal conditions now defer: the buffered data is returned from readLine() and the close/protocol error surfaces on the next call. Permessage-deflate inflation is also capped to guard against a decompression-bomb OOM.

Silent-degradation roundup (#121)

  • Direct Get batch and KV history() use progress-based deadlines - a healthy-but-slow replay completes, a genuinely stalled server throws instead of silently returning a truncated prefix.
  • A PubAck missing stream is rejected as invalid rather than accepted as a bogus success.
  • Terminal (4xx/5xx) push status frames are no longer forwarded to the handler as data - an ordered consumer recreates, a caller-owned push consumer surfaces the status through the errorListener.
  • publish()'s at-least-once reconnect semantics are documented.

Also included: #166 (lame-duck INFO during the reconnect replay poll) was verified already fixed by the v2.5.2 same-fiber recovery guard and is now pinned by a regression test.