Skip to content

Follow-ups from #801: an end-to-end held-completion witness, the replay's place at every hand-back entry, two untested defensive paths, a 15 s wait without #800 #814

Description

@FumingPower3925

The review of #801 (celeris#750) left these minor findings and nits. Its blocking finding is fixed on the branch at f8f51fd: a held completion kept its conn on the dirty list, so the worker waited with a zero timeout, a spin, for as long as the handler ran (about 375 ms CPU per second, measured). The items below were left for later, and each says what to do. Line numbers are at f8f51fd.

1. The end-to-end test has no witness that a completion was held (minor)

TestIouringSendCompletionDuringASlowAsyncHandlerDoesNotStallItsWorker checks only the fast conn's error (send_completion_stall_linux_test.go:530), its worst latency against stallBudget704 (:533) and the /big read error (:538). It does not check that a ring SEND of /big's tail existed and completed while /slow's handler ran. If the server's send buffer takes the whole direct write, which can happen with a larger tcp_wmem, for example on bare metal or on a tree with #800, no ring SEND is made. Nothing is then held, and max_ms < 300 passes without exercising the fix. #811 lists this as an open candidate for its own single-request arm. On the laptop and in CI the window is entered: main fails 3 of 3 at 742 to 758 ms.

Cluster row 52 (evidence/_queue/cluster.tsv) judges this test. Until it has a witness, the row requires big_ok=false on every celeris750 SENDSTALL line. On this branch, which does not have #800, that shows /big's tail was still queued when /slow's handler wrote. This interim witness stops working once #800 is merged, because /big then arrives intact.

To do:

  • Read EngineMetrics.RingBytes (engine/engine.go:359, filled at engine/iouring/engine.go:507) before and after /slow, and fail the arm's apparatus check unless it grew. Alternatively, add a test hook with a high-water count of held completions and assert it is greater than 0.
  • Make cluster row 52's PASS criterion require that witness.

2. Nothing pins the replay at the top of every hand-back entry (minor)

The replay sits at the top of drainDetachQueue's entry (worker.go:4886-4888), before the asyncClosed, transplantPending (:4909) and asyncH2Promoted branches. It must stay there. finishAsyncTransplant refuses a conn with cs.sending set (transplant_source.go:345), and a held completion keeps cs.sending set. endDispatch has already cleared relinkOwed. So a replay moved below the claim branch would leave the held completion (for example a partial SEND's remainder) unapplied until the conn's next request or close, and the client would wait for the rest of its response. handBack750 exercises only the loop-top hand-back, and the round-1 mutant M2 deletes the replay outright.

To do: add an arm that holds a completion and lets the goroutine exit through the park claim (with a drain set) and through the h2c exit. Assert that the entry applies the completion.

3. Two defensive parts have no test (minor)

Two changes survive every test when removed:

  • releaseConnState clearing heldSends (conn.go:550). Mutant Mrelease removes it.
  • replayHeldSends dropping completions for a conn that left its slot or changed generation (worker.go:3461). Mutant Mgen keeps only the bounds check.

The review ran both with -race in the m8 shape, and both passed.

To do: add a test that holds a completion, then releases and reuses the connState (or the fd number), and asserts that nothing is applied to the new owner.

4. The end-to-end test waits 15 s per run on a tree without #800 (nit)

Without #800, /slow's response is interleaved into /big's, and the /slow read waits for its 15 s socket deadline (send_completion_stall_linux_test.go:486). CI Unit job 108811856764 logs the test at 15.09 s, which adds about 17 s to the engine/iouring step, and that step runs with -timeout=300s. With #800 merged the wait goes away.

To do: set a short read deadline before the /slow read, so the test is cheap in either merge order.

5. For the record: fixed in round 2

  • The blocking finding. flushDirty now gives up a conn with held completions, and the hand-back lists it again. The check is in the pass, not only where the completion is held. The reason: when the goroutine has already entered its next handler, the hand-back's drain entry holds the completion again and then lists the conn. The review's one-line control (unlink in holdOrLockSend) misses that case. The new held_again arm fails on it 3 of 3 in both shapes. In a CPU probe with a second /slow sent in its own recv, that control spun at about 375 ms per second in 5 of 6 runs, and this head used 5.2 to 7.6 ms in every run.
  • The cost wording (nit). The PR body no longer credits the removed defer with pass 2's conn=sync "-11.63%": that comparison is against a main column with a level shift. The body now says only "no regression detected".

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/engineEngine interface or implementationarea/testTesting infrastructurebugSomething isn't workingengine/iouringio_uring engine specificsplatform/linuxLinux-specific (io_uring, epoll)

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions