Skip to content

test(iouring): void a linger attempt whose close did not linger, and retry it (celeris#763) - #810

Merged
FumingPower3925 merged 1 commit into
mainfrom
test/celeris-763-linger-void
Sep 28, 2026
Merged

FumingPower3925 merged 1 commit into
mainfrom
test/celeris-763-linger-void

Conversation

@FumingPower3925

Copy link
Copy Markdown
Contributor

Summary

TestDriverLingeringCloseDoesNotStallTheWorker/linger failed in 3 of 30 linger runs on main since #744, which is 3 of 6 runs of the job "io_uring init-failure regression (./engine/iouring)" (not required). It also failed once on #746 and once on #768. Every failure was "L's onClose fired while its close still lingered", and in every one the close had not lingered at all: onclose_after_finalize_ms was 100.5 to 100.6, the same as the no-linger control, and V's byte arrived in under 0.2 ms. The engine was right; the test was wrong.

Refs #763 (item 5).

The defect in the rig

The kernel ends a close's linger wait early when a signal is pending (sk_stream_wait_close stops on signal_pending). A Go test process gets signals. When one arrives, the engine's close(2) of its duplicate returns at once, and the fixed engine then fires onClose at once, as it should. The rig read "onClose fired 100 ms after finalize" as "onClose fired while the close lingered". It had no way to tell "this close did not linger" from "onClose ran before a lingering close returned", which is the defect #744 guards against.

Reproduced. The control OLDSIG1 is main's test, unchanged, plus one step: it sends SIGURG to the thread blocked in close(opFD), which it finds through /proc/self/task/*/syscall. The Go runtime ignores a SIGURG it did not ask for. Main's test then fails 5 of 5 with CI's signature: onclose_before_v=true, onclose_after_finalize_ms 100.6 to 102.2, V's byte in 0.04 to 0.52 ms, and the subtest over in 0.12 s.

The fix (test only)

  • Detect the linger. closeInLingerWait looks in /proc/net/tcp for a row that still carries the socket's inode. tcp_close orphans the socket, which sets that inode to 0, only when its linger wait ends, however it ends: the FIN is ACKed, the linger time runs out, or a signal arrives. Each attempt waits for the engine's close to begin (its number no longer names the socket), serves V, looks for onClose, and only then reads the probe. A close still waiting at that point was waiting while V was served and while onClose was looked for.
  • Void and retry. An attempt whose close has already returned by then proves nothing, so it is VOID. The arm retries on a new engine and a new socket, up to 5 times. It logs celeris735 LINGER arm=linger attempt=N ... close_lingering=... verdict=valid|void|fail for every attempt, then one line celeris735 LINGER arm=linger attempts=N void=M result=lingered|fail.
  • Still fails on the defect. onClose fired while the close is still in its linger wait is a FAIL. A V held past the 1 s budget is a FAIL at once, whether the close lingered or not.
  • Fails loudly when every attempt is void. Then the arm fails as apparatus: "L's close did not linger in any of 5 attempts".
  • Checks the probe. The no-linger control now also requires the probe to read a close with SO_LINGER off as already returned. If it reads that close as lingering, the probe is broken, and the control fails as apparatus.

TestDriverShutdownWaitsForAHandedOffClose (#763 item 4) and the CI step's list are unchanged.

Numbers

All runs are at 19106c1, in one container: linux/arm64, 4 CPUs, memlock 8 MiB (the step's shape), its own uid, kernel 7.0.12.

The fixed test, in CI's shape. The CI step "celeris#691 io_uring driver tests" ran exactly as ci.yml runs it (29 names, -race -count=5 -v), 20 times:

  • 20 of 20 tallies read "want 145 PASS, passed 145, FAIL lines 0, SKIP lines 0", with 0 data races.
  • TestDriverLingeringCloseDoesNotStallTheWorker: 100 PASS, 0 FAIL. All 100 linger arms lingered at their first attempt (attempts=1 void=0).
  • In the linger arm, V's byte arrived in 0.009 to 0.265 ms, and L's onClose came 2999.4 to 3062.9 ms after finalize.
  • The no-linger control read its close as returned in 100 of 100 runs.

Controls. Each is a go test -overlay, so the source is never edited. Each runs -race -count=5 on this test alone:

control what it does result
M3 #744's own mutant: finalizeDriver fires onClose at once, and a goroutine only closes (re-cut on this driver.go) FAIL 20/20, "L's onClose fired while its close still lingered". In one run, attempt 1 did not linger on its own (void), and attempt 2 failed.
FF the new test on #744's failing-first commit 3c930ee (the close runs on the worker) FAIL 20/20, "V's byte reached onRecv after" 2.9 s. In one run, attempt 1 was void on its own, and attempt 2 failed.
M3SIG1 M3, with attempt 1's close cut short by the signal FAIL 5/5: attempt 1 void, attempt 2 FAIL
SIG1 the new test, with attempt 1's lingering close cut short by the signal PASS 10/10: attempt 1 void (onclose_before_v=true close_lingering=false, onClose 101.7 to 104.8 ms after finalize), attempt 2 lingered
OLDSIG1 main's old test, with the same signal FAIL 5/5, CI's signature (above)
VALL the linger arm's socket has SO_LINGER off, so no attempt can linger FAIL 5/5, "apparatus: L's close did not linger in any of 5 attempts (5 void)"
DET1 the probe always reports a lingering close FAIL 5/5, in the no-linger control: "apparatus: ... cannot tell a returned close from a lingering one"

A first run of the suite, at the same head, ran as root. 8 of its 20 step iterations skipped or failed on ring ENOMEM ("io_uring not available on this system"). io_uring charges ring memory to the uid, and another lane's root container was running io_uring engines at the same time. None of those failures was in the linger check. The whole suite was then run again as a uid of its own; those are the numbers above.

Evidence: evidence/celeris-763-linger/. It has make_mutants.py, ctr.sh, in_container.sh, suite.sh and summary.sh, the logs in logs/19106c1/ with the tally in SUMMARY.txt, and the root run in logs/19106c1-run1-root-enomem/. Every log starts with its worktree, head, porcelain, uid, shape and image.

Test Plan

  • Unit tests added/updated (test only: no production change)
  • golangci-lint v2.13.2: 0 issues on ./engine/iouring/... for GOOS=linux on amd64 and arm64, and go vet is clean
  • Tested on Linux (arm64 container, in the CI step's shape); amd64 in this PR's CI

Tested on: [ ] std [ ] epoll [x] io_uring — [ ] amd64 [x] arm64

Release notes

  • Breaking change? (label breaking)
  • Labeled for release notes (testing: not user-facing)

…retry it (celeris#763)

TestDriverLingeringCloseDoesNotStallTheWorker/linger failed in 3 of 30 linger
runs on main's CI with "L's onClose fired while its close still lingered"
when the close had not lingered at all: the kernel ends a close's linger
wait early on a pending signal, the fixed engine then fires onClose at once,
and the rig could not tell that from onClose firing before a lingering close
returned.

Each attempt now reads /proc/net/tcp, after V was served and after onClose
was looked for, for a row that still carries the socket's inode: tcp_close
orphans the socket only once its linger wait has ended. An attempt whose
close has returned by then is void and is retried on a new engine and
socket, up to 5 times; the arm fails as apparatus if none lingers. The
no-linger control now also checks the probe reads a close with SO_LINGER
off as returned.
@FumingPower3925 FumingPower3925 added this to the v1.6.0 milestone Sep 28, 2026
@FumingPower3925 FumingPower3925 added engine/iouring io_uring engine specifics testing Testing infrastructure and helpers labels Sep 28, 2026
@coderabbitai

coderabbitai Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: goceleris/celeris/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: f22d9fa3-9454-40aa-955b-5fb6f6a05ce2

📥 Commits

Reviewing files that changed from the base of the PR and between 67fdb78 and 19106c1.

📒 Files selected for processing (1)
  • engine/iouring/driver_linger_close_linux_test.go

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.


📝 Walkthrough

Walkthrough

The Linux linger-close test checks whether a socket remains in its linger wait. It adds a no-linger control and retries linger attempts when the close has already returned.

Changes

Linger-close test

Layer / File(s) Summary
Observe socket close state
engine/iouring/driver_linger_close_linux_test.go
The test waits for the duplicate descriptor to stop naming the socket and checks /proc/net/tcp for the socket inode and TCP state.
Run and validate linger attempts
engine/iouring/driver_linger_close_linux_test.go
The test adds a no-linger control and retries attempts that no longer remain in the linger wait. Valid attempts check receive timing, onClose, and duplicate-descriptor state.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Other

Merge Risk: ⚪ Minimal · up to 19106

The Linux linger-close test has no established issue that should block merging after normal checks.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title uses the required Conventional Commit form, identifies the iouring test change, and ends with the issue reference (celeris#763).
Description check ✅ Passed The description directly explains the test defect, the retry and linger-detection changes, validation results, and test scope.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Sep 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@FumingPower3925
FumingPower3925 merged commit 00d985c into main Sep 28, 2026
17 checks passed
@FumingPower3925
FumingPower3925 deleted the test/celeris-763-linger-void branch September 28, 2026 07:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

engine/iouring io_uring engine specifics testing Testing infrastructure and helpers

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant