nx_ts_drain_gate.nx
buildroot/runtime/nx_ts_drain_gate.nx
about
nx_ts_drain_gate.nx -- TS2: THE PROCESS MUST KEEP SERVING AFTER TERM.
/compare/trafficsafety rung TS2, accept rule taken VERBATIM from trafficsafety.plan and not
re-invented here: "with a request in flight at the moment TERM lands, the client receives a
complete response and not a 5xx or a reset, and a daemon that exits early FAILS the gate.
Anti-vacuity tooth required -- a run in which no request was in flight when TERM landed must be
reported as UNOBSERVED and must not score as a pass."
THE MEASUREMENT IS WHAT THE CLIENT RECEIVED, NOT WHAT THE SERVER LOGGED. A drain that logs
"draining" and still cuts the response is the defect, so nothing here reads the subject's output:
a real client drives a real TCP connection and the verdict is whether the completion byte arrived.
THE IN-FLIGHT WINDOW IS CONSTRUCTED, NOT HOPED FOR, and that is what makes this a gate rather than
a race. The subject answers a request in TWO parts: it writes ACK (the response has begun and is
therefore incomplete), then BLOCKS reading GO before writing DONE. The parent sends TERM and only
THEN sends GO. So at the instant TERM lands the subject is provably mid-request, every time:
- drain armed -> TERM is blocked, the blocking read is NOT interrupted, GO arrives, DONE is
written. The client gets a complete response AFTER TERM.
- neg-control -> TERM is delivered to a process blocked in read, default action TERMINATE, so
the subject dies BEFORE GO is ever written and the client gets nothing.
The ordering (TERM strictly before GO) is what removes the race; a sleep would have re-introduced
it and would have been a magic number besides.
TEST THE STATE, NOT THE MESSAGE. The neg-control's tooth asserts the subject died BY SIGNAL 15 --
read out of the wait status, not out of any text -- because "the client got nothing" has several
possible causes and only one of them is the one this rung is about.
IT ALSO VERIFIES THE SYSCALL NUMBER EMPIRICALLY. A subject that survives TERM can only have
blocked it, so T04 is a live check that raw x86-64 14 (rt_sigprocmask) really passes through
x86ctx_rv64_to_x86_64_syscall unchanged. Reasoning about that table is not the same as running it.
SAFETY: loopback only; every port is FOUND by a bind that would have failed had it been occupied,
because a probe port you did not verify free is not a control, it is a second instance. The
listening socket is created by the PARENT and inherited across fork, so a child is never racing to
bind and the client can connect the instant the subject exists. No serving daemon is touched.
expect_exit: 0 license_tier: ORIGINAL No hw writes (Rule 26).
dependencies 4 imports · 0 importers
imports: nx_syscalls.nxnx_ts_drain_lib.nxnx_gate_emit_lib.nxnx_gate_verdict.nx
imported by: nobody (leaf or entry point)
call flow from main pre-order; caps 40 nodes / depth 6 declared; ↻ = already shown
structs
| none |
consts
| 42 | const TD_NUM_SCRATCH: i64 = 24 |
| 43 | const TD_ASCII_ZERO: i64 = 48 |
| 44 | const TD_B10: i64 = 10 |
| 45 | const TD_ONE: i64 = 1 |
| 46 | const TD_SA_BYTES: i64 = 16 |
| 49 | const TD_SA_PORT_OFF: i64 = 2 |
| 50 | const TD_SA_ADDR_OFF: i64 = 4 |
| 51 | const TD_ADDR_BYTES: i64 = 4 |
| 52 | const TD_BYTE_RADIX: i64 = 256 |
| 53 | const TD_OUT_SLOTS: i64 = 8 |
| 54 | const TD_OUT_BYTES: i64 = 64 |
| 55 | const TD_LOOPBACK_A: i64 = 127 |
| 56 | const TD_LOOPBACK_D: i64 = 1 |
| 57 | const TD_PORT_BASE: i64 = 39300 |
| 58 | const TD_PORT_TRIES: i64 = 64 |
| 59 | const TD_PORT_NONE: i64 = 0 - 1 |
| 60 | const TD_BACKLOG: i64 = 8 |
| 61 | const TD_TRUE: i64 = 1 |
| 62 | const TD_FALSE: i64 = 0 |
| 65 | const TD_PING: i64 = 80 |
| 66 | const TD_ACK: i64 = 65 |
| 67 | const TD_GO: i64 = 71 |
| 68 | const TD_DONE: i64 = 68 |
| 69 | const TD_READY: i64 = 82 |
| 73 | const TD_DEADLINE_S: i64 = ACCEPT_TMO_S |
| 76 | const TD_C_OK: i64 = 0 |
| 77 | const TD_C_ARM: i64 = 2 |
| 78 | const TD_C_ACCEPT: i64 = 3 |
| 79 | const TD_C_BADREQ: i64 = 4 |
| 80 | const TD_C_WRITE: i64 = 5 |
| 81 | const TD_C_NODRAIN: i64 = 6 |
| 82 | const TD_C_IDLE_DRAIN: i64 = 7 |
| 84 | const TD_SIG_TERM: i64 = 15 |
| 85 | const TD_SIG_NONE: i64 = 0 |
| 88 | const TD_O_ACK: i64 = 0 |
| 89 | const TD_O_DONE: i64 = 1 |
| 90 | const TD_O_EXIT: i64 = 2 |
| 91 | const TD_O_TSIG: i64 = 3 |
| 92 | const TD_O_INFLIGHT: i64 = 4 |
| 93 | const TD_O_POST: i64 = 5 |
| 95 | const TD_DIR: *u8 = "/tmp/nx_ts_drain_gate" as *u8 |
functions
| 97 | func g_putn(v: i64) -> i64 |
| 108 | func gq(label: *u8, got: i64, want: i64, ctr: *i64) -> i64 |
| 120 | func td_sa(sa: *u8, port: i64) -> i64 |
| 132 | func td_put1(fd: i64, v: i64) -> i64 |
| 137 | func td_get1(fd: i64) -> i64 |
| 147 | func td_listen(port: i64, reuse: i64) -> i64 |
| 164 | func td_find_port(from: i64) -> i64 |
| 175 | func td_connect(port: i64) -> i64 called by 2: td_run_inflightmain calls 6: sys_mmaptd_sasys_socketsys_set_socket_timeoutsys_connectsys_close |
| 190 | func td_subject_inflight(armed: i64, lfd: i64) -> i64 called by 1: td_run_inflight calls 9: sys_alarmsys_mmapts_drain_on_termsys_acceptsys_set_socket_timeouttd_get1+3 |
| 222 | func td_subject_idle(lfd: i64) -> i64 |
| 242 | func td_run_inflight(armed: i64, lfd: i64, port: i64, out: *i64) -> i64 |
| 269 | func td_report(tag: *u8, out: *i64) -> i64 |
| 280 | func main() -> i64 |