Files

5.8 KiB
Executable File
Raw Permalink Blame History

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): .text 0xa52203, nm --defined-only against ~/w7/p7-before/pull-syms.txt +0 / −0 (30570 lines).
  • integration-split not re-run: no product file changed.

6. Open

  1. AcceptPair over a tcp:// listener is still callable and still pairs by order. The product never calls it that way, but nothing refuses it by name; a MOBILEGL_ERR_UNSUPPORTED for TCP listeners in AcceptPair (and ConnectTo for tcp://) would close the door the fixture walked through. Not in X2 - it is a product change and needs its own red-once.
  2. The ::alarm(10) in the fixture is the only remaining time bound; AHealthyPeer... spends 5 s of it in kHandshakeTimeoutMs by design. Not observed to fire.