Consolidated on 2026-10-02 from the per-PR 'Follow-ups from #N' bundles, by maintainer decision. From now on, review nits are fixed inside the PR before it merges, so no new bundles are filed. Each line names the bundle and item it came from; the bundle's thread keeps the full context. Everything here stays in v1.6.0.
Fix the EPOLLOUT comments' premise: armEpollOut (loop.go:2978 on main) and loop.go:645 say a flush 'stops at EAGAIN', but flushWrites/flushWritesV make one write and return on a short count; suggested 'a partial flush (short write or EAGAIN) leaves the send buffer full'. Also driver.go:256 says 'EPOLLIN stays edge-triggered' though that MOD drops EPOLLET for EPOLLIN. (from Follow-ups from #698: shutdown leaves a deferred transplant's descriptor open (pre-existing), EPOLLOUT comment premise, CI visibility of the new test, stale comments #727 2)
Update TestATransplantWaitsForARelink's comments (async_handler_stall_linux_test.go:311-331) that still give the round-1 reason (a transplant returning cs to the pool); say relinkPending is now defence in depth. (from Follow-ups from #698: shutdown leaves a deferred transplant's descriptor open (pre-existing), EPOLLOUT comment premise, CI visibility of the new test, stale comments #727 4)
Add a nil-by-default or build-tagged test hook between driverRead's dc.closed check and unix.Read (engine/epoll/driver.go) and a test that unregisters, closes and reuses the number from inside it, so fix(epoll): never read a driver conn's descriptor number after UnregisterConn has returned (celeris#710) #772 's surviving mutant m1-toctou fails; measure the hook's cost or build-tag it. (fix(eventloop): never read a driver conn's descriptor number after UnregisterConn has returned (celeris#784) #843 did the same for the standalone loop only.) (from Follow-ups from #772: the standalone driver event loop reads a reused descriptor number after UnregisterConn (#710 there), the WorkerLoop contract sentence, a test for the check-and-read critical section, UnregisterConn doc #784 3)
Add one sentence to epoll UnregisterConn's doc (engine/epoll/driver.go:163-165): a read that completed before UnregisterConn took dc.mu is still delivered, so onRecv can run once more, concurrently with or after onClose. (from Follow-ups from #772: the standalone driver event loop reads a reused descriptor number after UnregisterConn (#710 there), the WorkerLoop contract sentence, a test for the check-and-read critical section, UnregisterConn doc #784 4)
Add a Config{Engine: Epoll, AsyncHandlers: true} arm with no Async route to TestHijackKeepsRequestViews (hijack_keeps_request_views_linux_test.go) and to CI's exact arm-PASS tally step, so Context.Hijack pools the receive buffer while the hijacker still holds request views: kept strings read another connection's bytes (epoll 7/20, io_uring 10/20) #733 is pinned on the AsyncHandlers-inline path now that fix: keep a hijacked request's receive buffer out of the pool on epoll and io_uring, and copy the request values at Hijack (celeris#733) #773 and fix(epoll): do not touch a connection's state after a handler that ran inline on an async loop hijacked it (celeris#769) #774 have both merged. (from Follow-ups from #773: an AsyncHandlers arm after #774, io_uring multishot strings read before Hijack, the copy on std, the engine-side cost, doc and witness nits #785 1)
Add a test that forces the connState's reuse between releaseConnState and the reset in drainRead's inline branch (a test hook running another conn's accept on the same P), so the nil-check mutant (cs.h1State != nil instead of the ErrHijacked check) fails. (from Follow-ups from #774: epoll drops a first segment shorter than protocol detection needs, a test for the connState-reuse face, an inline witness, the cost sentence #786 2)
engine/epoll/hijack_inline_async_loop_linux_test.go checks only hijacked.Load() == n; assert /hj ran inline (loops' asyncPromoted stays 0 in the async-loop-no-async-routes arm, and an equivalent observable in TestHijackWithAsyncHandlersOnEpoll), and cite fix(epoll): do not touch a connection's state after a handler that ran inline on an async loop hijacked it (celeris#769) #774 's failing-first numbers as the unfixed-code control. (from Follow-ups from #774: epoll drops a first segment shorter than protocol detection needs, a test for the connState-reuse face, an inline witness, the cost sentence #786 3)
Commit the review's ~30-line test of RegisterConn's rollback in ./engine/epoll (register a regular file's fd, which EPOLL_CTL_ADD refuses with EPERM; assert hasDriverConns reset, fd absent from driverConns, Write returns ErrUnknownFD), with the no-rollback mutant as failing-first control. (from Follow-ups from #776: a test for RegisterConn's rollback, an amd64 failing-first run, the 200 ms bound, the Coverage linger flake on pre-#744 branches #787 1)
Raise the 200 ms 'lost' bound in engine/epoll/driver_register_first_edge_linux_test.go:71 to 1-5 s (keep the count) or to seconds with a stop at the first loss (CodeRabbit's design), then re-run the failing-first control on aa2331d . (from Follow-ups from #776: a test for RegisterConn's rollback, an amd64 failing-first run, the 200 ms bound, the Coverage linger flake on pre-#744 branches #787 3)
Add an epoll arm to engine/epoll/park_retracts_residue_test.go that reaches the post-sweep close through a production path (e.g. a detached conn closed via the detach queue on a paused loop) instead of disarming the timerfd from the test goroutine (still done at :129 on main 24cdb7b ); it must fail with loop.go's park retraction removed and pass on main. (from Follow-ups from #766: an epoll park-retraction arm through a production post-sweep close, and the always-true liveConns guard #794 2)
In parkResidue, give the pre-sweep control arm its own long ReadHeaderTimeout (e.g. 10 s), keep 400 ms for the post-sweep arm, and close the client right after PauseAccept with no 50 ms sleep (still at :177 on main), so the control cannot fall onto the post-sweep path; re-run the negative control and apply the same check to the io_uring pair (CodeRabbit on fix(engine): retract a loop's or worker's residual gauges where it parks (celeris#711) #766 ). (from Follow-ups from #766: an epoll park-retraction arm through a production post-sweep close, and the always-true liveConns guard #794 3)
Add a test of the cancel path's race: release the handler before StartWithContext's watcher stores the budget (or delay the watcher with a hook) and check the epoll drain still extends to the budget, so a drain that reads the budget once at start would fail. (from Follow-ups from #807: shutdown-drain bytes missing from BytesWritten, no test of the cancel path's budget race #819 2)
TestShutdownSendsTheWholeResponse orders the client after the drain with fixed sleeps (shutdown_send_drain_linux_test.go:79-82); add a test-only hook in Loop.shutdown (drain entered) and wait on it (CodeRabbit on fix(epoll): shutdown sends what the sockets have not taken yet before it closes the conns (celeris#760) #807 ). (from Follow-ups from #807: shutdown-drain bytes missing from BytesWritten, no test of the cancel path's budget race #819 4)
h2PoolSettled (epoll and io_uring) ignores writeRefused after GoAway: since fix(epoll, iouring): send a large response whole, keep pipelined responses in order, never send a body the handler has given back (celeris#761, celeris#802, celeris#817) #805 the shutdown GOAWAY can be refused when the conn backlog is over its cap (64 MiB for HTTP/2), so it is lost and the conn closes only at its next write-queue drain. Review calls it harmless; check writeRefused after GoAway and close explicitly, plus a test. (from Follow-ups from #808: std h2c conns get no GOAWAY and outlive the drain, a pool-wait bound test that passes on main, cost wording #820 7)
Added 2026-10-03 (#907 's round-2 reviews, lane L905):
Loop.run, engine/epoll/loop.go (loop.go:405-408 at fix(epoll, iouring): a stopped engine no longer hands its loop threads back to the scheduler pinned to one CPU (celeris#905) #907 's head 242d88a ): when platform.SaveThreadAffinity fails, the loop still calls PinToCPU and registers no restore, so a main thread the loop ran on is parked still pinned (the epoll, io_uring: a stopped engine hands its loop threads back to Go still pinned to one CPU (PinToCPU is never undone before UnlockOSThread) #905 shape). sched_getaffinity into unix.CPUSet's 1024 bits fails with EINVAL on a kernel with more than 1024 possible CPUs (nr_cpu_ids), while sched_setaffinity still takes the short mask. Either pin only after a successful save (a behaviour change: no pin on such hosts), or read the mask into a buffer as large as the kernel's (the Go runtime uses 8 KiB, runtime/os_linux.go getCPUCount, golang.org/issue/11823). One change in internal/platform serves both engines (io_uring: the same item on v1.6.0 polish: io_uring engine #884 ). fix(epoll, iouring): a stopped engine no longer hands its loop threads back to the scheduler pinned to one CPU (celeris#905) #907 states the gap in a comment at both sites. Code reading only, not reproduced. (from fix(epoll, iouring): a stopped engine no longer hands its loop threads back to the scheduler pinned to one CPU (celeris#905) #907 )
Consolidated on 2026-10-02 from the per-PR 'Follow-ups from #N' bundles, by maintainer decision. From now on, review nits are fixed inside the PR before it merges, so no new bundles are filed. Each line names the bundle and item it came from; the bundle's thread keeps the full context. Everything here stays in v1.6.0.
Added 2026-10-03 (#907's round-2 reviews, lane L905):
Loop.run, engine/epoll/loop.go (loop.go:405-408 at fix(epoll, iouring): a stopped engine no longer hands its loop threads back to the scheduler pinned to one CPU (celeris#905) #907's head 242d88a): whenplatform.SaveThreadAffinityfails, the loop still callsPinToCPUand registers no restore, so a main thread the loop ran on is parked still pinned (the epoll, io_uring: a stopped engine hands its loop threads back to Go still pinned to one CPU (PinToCPU is never undone before UnlockOSThread) #905 shape).sched_getaffinityinto unix.CPUSet's 1024 bits fails with EINVAL on a kernel with more than 1024 possible CPUs (nr_cpu_ids), whilesched_setaffinitystill takes the short mask. Either pin only after a successful save (a behaviour change: no pin on such hosts), or read the mask into a buffer as large as the kernel's (the Go runtime uses 8 KiB, runtime/os_linux.go getCPUCount, golang.org/issue/11823). One change in internal/platform serves both engines (io_uring: the same item on v1.6.0 polish: io_uring engine #884). fix(epoll, iouring): a stopped engine no longer hands its loop threads back to the scheduler pinned to one CPU (celeris#905) #907 states the gap in a comment at both sites. Code reading only, not reproduced. (from fix(epoll, iouring): a stopped engine no longer hands its loop threads back to the scheduler pinned to one CPU (celeris#905) #907)