5.8 KiB
Executable File
X2 — the InitialCapsStartup unit-test flake under load
Wave 2, package X2. Base 9ae1e6c0. Branch p7/x2-caps-flake.
1. What actually failed
InitialCapsStartup.ANullInitialSnapshotFailsInsteadOfStartingWithAPlaceholder failed in 3 of
the last 4 integration gates (ctest -L '^unit$' -j 8 on a host where other agents build) and
passed 3/3 alone. Reproduced on the same host with 20 busy loops plus a repeating -j 20 clean
rebuild in a second build directory (load average 40-60), 8 test processes at a time:
[ RUN ] InitialCapsStartup.ANullInitialSnapshotFailsInsteadOfStartingWithAPlaceholder
MGPipe: Fatal{ProtocolCorruption, "StreamLink"} invalid data frame header
### rc=134 (SIGABRT) wall=262ms
It is not an EXPECT and it is not the caps path: the abort is StreamLink.cpp:229, the DATA
link's reader thread failing FrameReader::Feed on the first 8 bytes it ever reads. The same
line kills DelayedTcpSnapshotArmsRunAheadAndLaterSnapshotsCanOnlyDemote (the case project
memory recorded flaking before), and AHealthyPeerWithoutInitialCapsFailsStartupWithoutDeviceLoss
never fails only because its peer sends nothing.
2. The mechanism
The fixture rendezvoused over loopback TCP with SocketTransport::ConnectTo (two back-to-back
connects: control, then aux) against AcceptPair on a thread (two accepts: "first connection is
control, second is aux. The order IS the protocol"). Under load the kernel does not deliver two
loopback connects to the listener's accept queue in connect order: the final ACK of each
handshake is processed by the loopback softirq, and once the connecting thread migrates or
ksoftirqd is starved the second connection's ACK can land first. Measured with a raw-socket
probe that does exactly ConnectTcp's dance (~/w7/x2bin/swapprobe.c): 0 of 2000 on an idle
host, 5 of 3000 (0.17 %) with 20 CPU hogs.
When the pair is swapped the peer's control transport is the client's data socket. The
test's first control frame (CapsSnapshot, or the LogLine of the delayed case) then arrives
on the client's StreamLink reader, whose framing magic is MGLD, and the reader aborts by
name. No timing assumption in the tests, no 250 ms window, no fixed sleep: an ordering the
fixture assumed and TCP never promised.
Verdict: TEST. The product does not pair by arrival order on TCP. PH-7 (4) / ID-P7-3 took
that out (SocketTransport.h: "never paired by arrival order"; ClientSession.cpp:780:
ConnectControl then ConnectDataConnection with Welcome.dataNonce; ServerMain.cpp:
AcceptOne + ReceiveOneFrame + DecodeDataBind). ConnectTo/AcceptPair survive for AF_UNIX
only, where unix_stream_connect enqueues on the listener synchronously and the order is real.
Window 1b's device notes show the same defect in the pre-F product over the LAN ("only 1 of 2
connections within 250 ms", connections reordered under adb forward) - the fixture was simply
left behind on the old rendezvous. CapsMirror::ServerConsumes, AwaitFirstCapsSnapshot and
FinishStartup step 7 were read end to end and are not implicated; no product bytes change.
3. The fix
MG_Test/Wire/InitialCapsStartupTest.cpp (9f7e8013): the fixture does what
ServerMain::ListenerSource does. ConnectControl is accepted with AcceptOne and wrapped as
SocketTransport(fd, -1, Server) BEFORE the data connection is opened; the data connection is
ConnectDataConnection carrying EncodeDataBind(MintNonce()), accepted with AcceptOne, read
with ReceiveOneFrame, decoded with DecodeDataBind and matched with ConstantTimeNonceMatch.
No accept thread: a loopback connect completes against the backlog without a concurrent accept.
The three cases are untouched.
MG_Test/Wire/SocketTransportTest.cpp (d0284455):
TcpCarriesControlAndIndependentDataWithKeepalive had the same ConnectTo/AcceptPair shape
and, swapped, hung in recv(serverData, MSG_WAITALL) until the 60 s kill (rc=124). Same
rendezvous now; it keeps the NODELAY / keepalive / ShareFd checks on both control transports,
the independent-data check on the bound data sockets, and asserts a control-only TCP transport
owns no data descriptor (TakeDataFd() == -1).
4. Red-once, under the same load (20 hogs + -j 20 rebuild loop, 8 processes at a time)
| case | before | after |
|---|---|---|
InitialCapsStartup.ANullInitialSnapshotFailsInsteadOfStartingWithAPlaceholder |
2 / 200, then 2 / 500 (SIGABRT, text above) | 0 / 500 |
InitialCapsStartup.DelayedTcpSnapshotArmsRunAheadAndLaterSnapshotsCanOnlyDemote |
5 / 500 (same abort) | 0 / 500 |
SocketTransportTest.TcpCarriesControlAndIndependentDataWithKeepalive |
3 / 500 (60 s hang) | 0 / 500 |
Pass wall time did not move (median ~250 ms in both columns).
5. Gates
ctest -L '^unit$' -j 8: 2435 / 2435, rc 0.scripts/ci/fatal_census.py: rc 0, 79 abort sites / 20 files, 44 family words, 3 refusal words, 0 unmarked (unchanged).scripts/link_ratchet.py --build-dir build-split --baseline scripts/data/link_ratchet_baseline.txt --assert-monotone: 173, unchanged.- G1 pull build (
build-linux, the p5_g1base flags):.text0xa52203,nm --defined-onlyagainst~/w7/p7-before/pull-syms.txt+0 / −0 (30570 lines). integration-splitnot re-run: no product file changed.
6. Open
AcceptPairover atcp://listener is still callable and still pairs by order. The product never calls it that way, but nothing refuses it by name; aMOBILEGL_ERR_UNSUPPORTEDfor TCP listeners inAcceptPair(andConnectTofortcp://) would close the door the fixture walked through. Not in X2 - it is a product change and needs its own red-once.- The
::alarm(10)in the fixture is the only remaining time bound;AHealthyPeer...spends 5 s of it inkHandshakeTimeoutMsby design. Not observed to fire.