Skip to content

v2.12.0

Choose a tag to compare

@bpacholek bpacholek released this 06 Oct 21:25
· 43 commits to main since this release

Minor release. Every operation that reads the socket while it waits for a result of its own now gets that result as soon as it is delivered, by whichever fiber's read (#174). A SubscriptionQueue poll looks at its queue before every read, and every such read now carries a wake-up: a delivery to the operation's subscription, its reply or its PONG ends the read wherever it waits, and the operation looks again. Before, a result that reached the operation while its read waited came back only with the server's next bytes or at the operation's deadline: the whole deadline on a real server, which pings only every two minutes.

Upgrading is recommended if any of your code reads the connection from more than one fiber at a time, such as a processIncoming() loop beside polls, requests, fetches or a pull consumer, or if your subscription handlers await HTTP or database calls. There are no API changes. Custom transports have one rule to check, below. symfony-nats-messenger 5.4.1 works with this release unchanged.

Upgrade notes

  • Custom transports. TransportInterface::readLine() must keep every byte it has not returned when its cancellation ends it, a partial frame included. The connection now cancels a read not only at a deadline but also to wake an operation whose result another fiber delivered meanwhile, and then reads on. The built-in transports do this, and the rule always held in practice, since every non-blocking poll and the heartbeat cancel their reads after a short bound. A transport that lost bytes on cancellation would now corrupt the stream in normal operation.
  • flush() fails earlier after a drop. A flush(), rtt() or drain flush whose PING went out before the connection dropped fails with Connection lost before the server answered the PING as soon as the reconnect's first attempt clears the pong slots, rather than once the reconnect is over or at the flush's deadline.

Fixed

  • Queue polls between reads (#174). SubscriptionQueue::next() with a timeout, and fetchAll() with a limit and a timeout, read, found nothing, paused 1 ms and read again, and looked at the queue only right after their own read. A message that another fiber's read delivered during the pause stayed in the queue while the poll's next read waited on the socket until the timeout. They now look at the queue before every further read. next() has worked this way since 1.0.0, fetchAll() since 2.0.0.
  • fetchAll() and a full queue. fetchAll() also takes such messages when they do not complete the call. Its next read used to find the queue as that delivery left it; when the delivery had filled the queue, the messages that read brought overflowed it, so messages were dropped, or under SlowConsumerPolicy::Error the call failed with a SlowConsumerException.
  • The pipelined pull engine. A batch that another fiber's read delivered while the engine waited for the write of a pull request reached the handler only with the server's next bytes or at that pull's deadline, its expiry plus 1 s. The engine looked at its oldest pull only before it issued pulls; it now looks again before it reads. The engine has worked this way since 2.7.0.
  • A result delivered while the operation's read waited. SubscriptionQueue polls, request()/requestMany(), fetchBatch()/fetchNext(), directGetBatch(), the pull consumer and Key/Value keys()/history() got such a result only with the server's next bytes or at their deadline. It happened in three ways: the delivery was still under way when the read started, held up in another subscription's handler that awaits an HTTP or database call, or in the write of the PONG a server PING ahead of the message is owed; it came in the few event-loop hops between the operation's check and the start of its read; or it came with the reconnect the read waited for. On a real server the first case costs the whole deadline and the third about two seconds. An operation's read now ends, without reading, as soon as anything is delivered to its subscription, or its reply comes, wherever the read waits: on the socket, behind another fiber's read, or for a reconnect. The operation then looks again.
  • Flushes. flush(), rtt(), the drain flushes and the confirmation of the reply inbox end their read as soon as their PONG is in, also when the read that took it was held up before it reached the PONG.
  • After a reconnect. An operation's read that waited for a reconnect looks again before it reads the new socket. The pull consumer re-issues the pulls the old server forgot right after the reconnect, instead of at the lost pull's deadline.

Unchanged on purpose

  • Application reads, processIncoming() and readIncoming(), have no wake-up: they read until the server sends something or their cancellation fires.
  • Delivery order. An operation gets its message once the in-order delivery reaches it, so a handler of another subscription ahead of it that outlasts the operation's deadline still makes it time out. A message left queued by a handler that threw in another fiber's delivery still waits for the next read that receives bytes; that and five smaller cases the review found are filed as issues.

Quality gates

  • PHPStan level 8.
  • 2682 unit tests (with data sets) on PHP 8.2 to 8.5, 149 live integration tests and 48 Behat scenarios. Every CI job passed on the first attempt.
  • All 45 runnable examples executed against a live server.
  • Statement coverage 99.08% from the unit suite (95% floor enforced in CI) and 99.14% combined (97% floor).
  • Infection covered MSI 93.6% over the 3891 mutants it ran (90% floor enforced in CI). It skipped the other 3828 of the 7719 it generated, because the tests covering them take longer than its time limit, so the score does not include them.
  • Every fix has tests that fail on the old code. The queue polls returned at their 5 s timeout and the pull engine handed over at its 6 s deadline. For the wake-up, 46 scenarios per operation and way of delivery, hop sweeps of 0 to 32 event-loop hops in both orders, and lifecycle cases (auto-unsubscribe, drains, drop reports, two pollers, concurrent requests) all returned at their deadline on the old code and within milliseconds now. A cancelled read is proven to keep every byte on plain TCP, TLS and WebSocket and on a real loopback socket.
  • The wake-up design was chosen between two prototypes built against shared probes and reviewed for interleavings and semantics by four independent reviewers and a judge. The alternative, which counted deliveries, missed its wake-up when a subscription was removed in the pass that delivered its last message.
  • symfony-nats-messenger 5.4.1's suites pass against this release: PHPStan level max, 528 unit tests, 52 functional scenarios against live NATS and 5 examples.

The full record is in the CHANGELOG.