code wiki / (root) / nx_ts_drain_lib.nx

nx_ts_drain_lib.nx source

↩ module page · 199 lines · 10029 B

1// nx_ts_drain_lib.nx -- TS2: COOPERATIVE DRAIN ON TERM, with a DERIVED deadline. 2// 3// /compare/trafficsafety rung TS2. Accept rule taken VERBATIM from trafficsafety.plan and not 4// re-invented here: "On TERM the daemon STOPS ACCEPTING and KEEPS SERVING in-flight requests until 5// a declared deadline, then exits; the deadline is a named constant, never a literal. ACCEPT RULE: 6// with a request in flight at the moment TERM lands, the client receives a complete response and 7// not a 5xx or a reset, and a daemon that exits early FAILS the gate." 8// 9// THE PROCESS MUST KEEP SERVING AFTER TERM. Kubernetes states the race precisely: endpoint removal 10// happens AT THE SAME TIME AS shutdown begins, not before it, so a process that exits on TERM is 11// exactly what produces the client-visible 5xx. AWS names the cost from the other side: a target 12// that terminates the connection before the deregistration delay elapses makes the client receive a 13// 500-level error. 14// 15// WHY THIS BLOCKS THE SIGNAL INSTEAD OF INSTALLING A HANDLER, and it is not a style choice: 16// (1) A handler that RETURNS needs SA_RESTORER on x86-64 or the return frame is undefined. 17// nx_syscalls.nx's own sys_ignore_sigpipe comment records that it may skip SA_RESTORER ONLY 18// because SIG_IGN never delivers a handler frame -- a drain handler must return, so that 19// exemption does not extend to it. 20// (2) A delivered signal interrupts a blocking syscall with EINTR. A read or write cut mid-response 21// IS the truncated reply this rung exists to prevent, so the mechanism that notices TERM must 22// not be the mechanism that can corrupt the in-flight response. 23// Blocking TERM gives both properties for free: the signal can never kill this process, and it can 24// never interrupt a syscall, so the in-flight request completes on its own natural path. The signal 25// is then consumed as a FILE DESCRIPTOR READ, which is pollable -- so an idle daemon exits the 26// instant TERM arrives rather than sleeping out its budget. 27// 28// SYSCALL NUMBERING, MEASURED FROM THE ARTIFACT RATHER THAN RECALLED. TARGET_X86_64 is hard-pinned 29// undefined, so the x86 backend emits through x86ctx_rv64_to_x86_64_syscall (nx_x86_64_ctx.nx:1916) 30// whose default branch is `return num` -- an unmapped number is NOT an error, it is a DIFFERENT 31// syscall. That table was read in full: 14, 289 and 13 are NOT keys, so a raw x86-64 number passes 32// through UNCHANGED and lands on the intended call. This is the same convention the proven code in 33// this tree already uses -- SYS_SENDMSG=46 (x86 sendmsg, proven by nx_scm_rights_gate 14/14) and the 34// literal 13 in sys_ignore_sigpipe (x86 rt_sigaction, proven by nx_sigpipe_gate). The RV64 spellings 35// would be WRONG here for the same reason: rv64 135 is not a key either, so it would land on x86 36// personality, and rv64 134 would land on x86 uselib -- which is why nx_signal.nx's 37// nx_signal_action_install cannot work on this target and why this rung does not live in that file. 38// nx_ts_drain_gate proves the number empirically: a child that survives TERM can only have blocked it. 39// license_tier: ORIGINAL No hw writes (Rule 26). 40 41import "nx_syscalls.nx" 42import "nx_resmon_lib.nx" // rm_conf: the estate's line-anchored conf reader -- NOT a second one 43 44const TSD_SIGTERM: i64 = 15 45 46// Raw x86-64 numbers -- see the numbering note in the header. Named so a reader can check them 47// against the translator table rather than meet a bare integer at the call site. 48const TSD_SYS_RT_SIGPROCMASK: i64 = 14 49const TSD_SYS_SIGNALFD4: i64 = 289 50 51const TSD_SIG_BLOCK: i64 = 0 52const TSD_SFD_NONBLOCK: i64 = 0x800 53const TSD_SFD_CLOEXEC: i64 = 0x80000 54const TSD_SIGSET_BYTES: i64 = 8 55const TSD_SSI_BYTES: i64 = 128 56const TSD_SIGNALFD_NEW: i64 = 0 - 1 57 58const TSD_POLLFD_BYTES: i64 = 8 59const TSD_POLLIN: i64 = 1 60const TSD_PF_EV_OFF: i64 = 4 61const TSD_PF_RE_OFF: i64 = 6 62const TSD_NFDS_BOTH: i64 = 2 63const TSD_NFDS_ONE: i64 = 1 64const TSD_BYTE_MASK: i64 = 0xff 65// struct pollfd is { i32 fd; i16 events; i16 revents }. These are the FIELD WIDTHS, and the byte 66// lanes below are DERIVED from them rather than hand-written one line per byte: a hand-counted offset 67// sitting beside a width is a second copy of that width's shape, and the two drift silently. 68const TSD_FD_BYTES: i64 = 4 69const TSD_I16_BYTES: i64 = 2 70const TSD_BITS_PER_BYTE: i64 = 8 71 72const TSD_CONF: *u8 = "knowledge/trafficsafety_drain.conf" as *u8 73const TSD_KEY: *u8 = "drain_deadline_s" as *u8 74const TSD_CONF_ABSENT: i64 = 0 - 1 75 76// Arm verdicts -- each NAMES which step failed, so "no drain" and "no signalfd" are different 77// numbers rather than one indistinguishable -1. 78const TSD_OK: i64 = 0 79const TSD_ERR_BLOCK: i64 = 1 80const TSD_ERR_FD: i64 = 2 81 82// ts_drain_wait results. 83const TSD_W_DRAIN: i64 = 1 84const TSD_W_CONN: i64 = 0 85const TSD_W_NONE: i64 = 0 - 1 86const TSD_WAIT_BLOCK: i64 = 0 - 1 87 88// Deadline provenance. An unapplied config must be VISIBLE, so the caller is told which source it got. 89const TSD_SRC_DERIVED: i64 = 0 90const TSD_SRC_CONF: i64 = 1 91 92// THE DEADLINE, AND ITS DERIVATION -- this is not a number somebody picked. 93// 94// A drain deadline is a bound on "how long may one in-flight request still be served after TERM". 95// Every accepted connection in this tree already carries exactly that bound: the serving loops call 96// sys_set_socket_timeout(cfd, ACCEPT_TMO_S), so a single in-flight request CANNOT outlive 97// ACCEPT_TMO_S by construction. The drain deadline is therefore the SAME named constant as the 98// resource it waits on -- which is rung TS9's principle applied one rung early, and it means changing 99// ACCEPT_TMO_S changes this deadline with no second edit and no second place to forget. 100// 101// The conf exists to let an operator RAISE it for a daemon whose requests are legitimately longer, 102// never to invent the default. An absent conf, an unreadable conf, or a non-positive value all fall 103// back to the derived value and say so through out_src. 104func ts_drain_deadline_s(out_src: *i64) -> i64 { 105 *out_src = TSD_SRC_DERIVED 106 let nl: *i64 = (sys_mmap(TSD_SIGSET_BYTES * TSD_NFDS_BOTH)) as *i64 107 nl[0] = 0 108 let buf: *u8 = sys_read_file(TSD_CONF, nl) 109 if buf == (0 as *u8) { return ACCEPT_TMO_S } 110 if nl[0] <= 0 { return ACCEPT_TMO_S } 111 let v: i64 = rm_conf(buf, nl[0], TSD_KEY, TSD_CONF_ABSENT) 112 if v <= 0 { return ACCEPT_TMO_S } 113 *out_src = TSD_SRC_CONF 114 return v 115} 116 117// ARM THE DRAIN. Block TERM (so it can neither kill this process nor interrupt a syscall 118// mid-response) and return a non-blocking, close-on-exec signalfd that becomes readable when TERM 119// arrives. Returns the fd, or -1 with a named verdict. 120// 121// CALL THIS BEFORE THE LISTENER EXISTS. A daemon that arms the drain only after it starts serving 122// has a window in which TERM still kills it outright -- and that window is exactly a deploy. 123func ts_drain_on_term(out_verdict: *i64) -> i64 { 124 let mask: *u8 = sys_mmap(TSD_SIGSET_BYTES) 125 let m64: *i64 = mask as *i64 126 m64[0] = 1 << (TSD_SIGTERM - 1) 127 let br: i64 = __syscall(TSD_SYS_RT_SIGPROCMASK, TSD_SIG_BLOCK, mask as i64, 0, 128 TSD_SIGSET_BYTES, 0, 0) 129 if br < 0 { *out_verdict = TSD_ERR_BLOCK; return 0 - 1 } 130 let fd: i64 = __syscall(TSD_SYS_SIGNALFD4, TSD_SIGNALFD_NEW, mask as i64, TSD_SIGSET_BYTES, 131 TSD_SFD_NONBLOCK | TSD_SFD_CLOEXEC, 0, 0) 132 if fd < 0 { *out_verdict = TSD_ERR_FD; return 0 - 1 } 133 *out_verdict = TSD_OK 134 return fd 135} 136 137// Has TERM arrived? A non-blocking read of the signalfd: one 128-byte siginfo record means yes. 138// Consuming the record is deliberate -- the drain is a latch the caller acts on once. 139func ts_drain_requested(drain_fd: i64) -> i64 { 140 if drain_fd < 0 { return 0 } 141 let b: *u8 = sys_mmap(TSD_SSI_BYTES) 142 let n: i64 = sys_read(drain_fd, b, TSD_SSI_BYTES) 143 if n == TSD_SSI_BYTES { return 1 } 144 return 0 145} 146 147func tsd_pollfd_set(p: *u8, idx: i64, fd: i64) -> i64 { 148 let o: i64 = idx * TSD_POLLFD_BYTES 149 var k: i64 = 0 150 while k < TSD_FD_BYTES { 151 p[o + k] = ((fd >> (k * TSD_BITS_PER_BYTE)) & TSD_BYTE_MASK) as u8 152 k = k + 1 153 } 154 var e: i64 = 0 155 while e < TSD_I16_BYTES { 156 p[o + TSD_PF_EV_OFF + e] = 0 as u8 157 p[o + TSD_PF_RE_OFF + e] = 0 as u8 158 e = e + 1 159 } 160 p[o + TSD_PF_EV_OFF] = TSD_POLLIN as u8 161 return 0 162} 163 164func tsd_pollfd_ready(p: *u8, idx: i64) -> i64 { 165 let o: i64 = idx * TSD_POLLFD_BYTES 166 var e: i64 = 0 167 while e < TSD_I16_BYTES { 168 if (p[o + TSD_PF_RE_OFF + e] as i64) != 0 { return 1 } 169 e = e + 1 170 } 171 return 0 172} 173 174// WAIT for whichever comes first: a drain request, or a connection to accept. 175// 176// THE DRAIN IS TESTED FIRST, DELIBERATELY. When both are ready in the same wakeup the correct answer 177// is STOP ACCEPTING -- a loop that took the connection first would serve one more request after it 178// had already been told to stop, which is the opposite of draining. Two organs that must agree on an 179// ordering should not agree by discipline, so the ordering lives here once and every caller inherits it. 180// 181// A caller that could not arm the drain degrades to polling the listener alone rather than failing: 182// an arming failure must not turn a working daemon into a dead one. 183func ts_drain_wait(listen_fd: i64, drain_fd: i64, timeout_ms: i64) -> i64 { 184 let p: *u8 = sys_mmap(TSD_POLLFD_BYTES * TSD_NFDS_BOTH) 185 if drain_fd < 0 { 186 tsd_pollfd_set(p, 0, listen_fd) 187 let r0: i64 = sys_poll(p, TSD_NFDS_ONE, timeout_ms) 188 if r0 <= 0 { return TSD_W_NONE } 189 if tsd_pollfd_ready(p, 0) == 1 { return TSD_W_CONN } 190 return TSD_W_NONE 191 } 192 tsd_pollfd_set(p, 0, drain_fd) 193 tsd_pollfd_set(p, 1, listen_fd) 194 let r: i64 = sys_poll(p, TSD_NFDS_BOTH, timeout_ms) 195 if r <= 0 { return TSD_W_NONE } 196 if tsd_pollfd_ready(p, 0) == 1 { return TSD_W_DRAIN } 197 if tsd_pollfd_ready(p, 1) == 1 { return TSD_W_CONN } 198 return TSD_W_NONE 199}