Skip to content

v2.5.2

Choose a tag to compare

@bpacholek bpacholek released this 12 Jul 08:41

A correctness-focused release resolving 23 issues from a full review of everything shipped since v2.4.0 (memory-loss risk, NATS/JetStream spec conformance, reconnect/drain semantics, and performance). Every fix landed one-by-one with a falsifiability-checked regression test and two independent adversarial review passes; the whole tree is green on PHPStan level 8, 1545 unit tests, integration + behat against NATS 2.12, 97.8% 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.

Message-loss and drain correctness

  • flush()/drain() now use FIFO ping/pong correlation instead of a shared flag, so a stale heartbeat PONG or a sibling flush's timeout can no longer end a drain early and drop in-flight messages on the lossless path (#117).
  • drain() delivers a suspended handler's remaining backlog before closing, lets a handler publish (a JetStream ack / reply) during draining, and reports any deadline-exceeded discard loudly instead of silently (#149, #150).
  • A mid-chunk parse failure no longer discards the valid frames already parsed from the same TCP segment, on every read path, and surfaces the error instead of vanishing (#147). Frames coalesced behind the handshake PONG are likewise retained (#157).

Reconnect / connection lifecycle

  • Recovery stays Connecting until the subscription replay and buffered-publish flush complete, fixing publish-order inversion, writes to a dead socket, and replay/read collisions (#148).
  • connect() joins an in-flight recovery instead of racing it with a second dial chain that could drop new-epoch subscriptions (#145).
  • A handler exception during post-recovery delivery no longer closes a healthy connection (#144); the reconnect-disabled terminal path now releases state and closes the socket (#146); dropped-frame and discarded-inbound-backlog errors are now observable (#158); the first successful connect emits Connected (#161).

JetStream

  • Ordered / push / KV-watch consumers now detect idle heartbeats stopping and recreate a reaped consumer (or surface a "not active" error) rather than dying silently forever (#113).
  • Ordered-consumer recreate tolerates any delete-leg failure (#151); atomic-batch publish is guarded by a server-version pre-flight so nothing is stored on a pre-2.12 server (#152); JetStream errors are discriminated by err_code, not message substrings (#154); $JS.ACK metadata parsing tolerates extra trailing tokens (#155); pull-consumer 409 handling, no_wait pacing, and idle-heartbeat validation/fail-fast are fixed (#153); KV/ObjectStore/Batch header requests surface no-responders uniformly as JetStreamException(503) (#161).

Delivery accounting, WebSocket, and performance

  • Auto-unsubscribe no longer over-delivers one message past max (#156); SlowConsumerPolicy::Error drops are observable without corrupting auto-unsub accounting (#159); requestMany() respects maxResponses when replies coalesce (#160).
  • WebSocket: large frames spill to a join-once buffer (~3x faster on 10 MB frames, parity elsewhere) (#164); a server-initiated Close is echoed per RFC 6455 (#161).
  • The per-chunk pending-drain scan is O(subscriptions-with-backlog) instead of O(all subscriptions) — idle-chunk cost is now flat regardless of subscription count (#162); the inbound control-line split and per-chunk fiber overhead from #140 are restored (#163).

Two narrow follow-ups found during review are tracked as #165 (flush-loop liveness under sustained publish pressure) and #166 (lame-duck INFO during reconnect replay).