nx_ts_drain_lib.nx
buildroot/runtime/nx_ts_drain_lib.nx
about
nx_ts_drain_lib.nx -- TS2: COOPERATIVE DRAIN ON TERM, with a DERIVED deadline.
/compare/trafficsafety rung TS2. Accept rule taken VERBATIM from trafficsafety.plan and not
re-invented here: "On TERM the daemon STOPS ACCEPTING and KEEPS SERVING in-flight requests until
a declared deadline, then exits; the deadline is a named constant, never a literal. ACCEPT RULE:
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."
THE PROCESS MUST KEEP SERVING AFTER TERM. Kubernetes states the race precisely: endpoint removal
happens AT THE SAME TIME AS shutdown begins, not before it, so a process that exits on TERM is
exactly what produces the client-visible 5xx. AWS names the cost from the other side: a target
that terminates the connection before the deregistration delay elapses makes the client receive a
500-level error.
WHY THIS BLOCKS THE SIGNAL INSTEAD OF INSTALLING A HANDLER, and it is not a style choice:
(1) A handler that RETURNS needs SA_RESTORER on x86-64 or the return frame is undefined.
nx_syscalls.nx's own sys_ignore_sigpipe comment records that it may skip SA_RESTORER ONLY
because SIG_IGN never delivers a handler frame -- a drain handler must return, so that
exemption does not extend to it.
(2) A delivered signal interrupts a blocking syscall with EINTR. A read or write cut mid-response
IS the truncated reply this rung exists to prevent, so the mechanism that notices TERM must
not be the mechanism that can corrupt the in-flight response.
Blocking TERM gives both properties for free: the signal can never kill this process, and it can
never interrupt a syscall, so the in-flight request completes on its own natural path. The signal
is then consumed as a FILE DESCRIPTOR READ, which is pollable -- so an idle daemon exits the
instant TERM arrives rather than sleeping out its budget.
SYSCALL NUMBERING, MEASURED FROM THE ARTIFACT RATHER THAN RECALLED. TARGET_X86_64 is hard-pinned
undefined, so the x86 backend emits through x86ctx_rv64_to_x86_64_syscall (nx_x86_64_ctx.nx:1916)
whose default branch is `return num` -- an unmapped number is NOT an error, it is a DIFFERENT
syscall. That table was read in full: 14, 289 and 13 are NOT keys, so a raw x86-64 number passes
through UNCHANGED and lands on the intended call. This is the same convention the proven code in
this tree already uses -- SYS_SENDMSG=46 (x86 sendmsg, proven by nx_scm_rights_gate 14/14) and the
literal 13 in sys_ignore_sigpipe (x86 rt_sigaction, proven by nx_sigpipe_gate). The RV64 spellings
would be WRONG here for the same reason: rv64 135 is not a key either, so it would land on x86
personality, and rv64 134 would land on x86 uselib -- which is why nx_signal.nx's
nx_signal_action_install cannot work on this target and why this rung does not live in that file.
nx_ts_drain_gate proves the number empirically: a child that survives TERM can only have blocked it.
license_tier: ORIGINAL No hw writes (Rule 26).
dependencies 2 imports · 3 importers
imports: nx_syscalls.nxnx_resmon_lib.nx
imported by: nx_survey_daemon.nxnx_ts_drain_gate.nxnx_ts_slot_lib.nx
structs
| none |
consts
| 44 | const TSD_SIGTERM: i64 = 15 |
| 48 | const TSD_SYS_RT_SIGPROCMASK: i64 = 14 |
| 49 | const TSD_SYS_SIGNALFD4: i64 = 289 |
| 51 | const TSD_SIG_BLOCK: i64 = 0 |
| 52 | const TSD_SFD_NONBLOCK: i64 = 0x800 |
| 53 | const TSD_SFD_CLOEXEC: i64 = 0x80000 |
| 54 | const TSD_SIGSET_BYTES: i64 = 8 |
| 55 | const TSD_SSI_BYTES: i64 = 128 |
| 56 | const TSD_SIGNALFD_NEW: i64 = 0 - 1 |
| 58 | const TSD_POLLFD_BYTES: i64 = 8 |
| 59 | const TSD_POLLIN: i64 = 1 |
| 60 | const TSD_PF_EV_OFF: i64 = 4 |
| 61 | const TSD_PF_RE_OFF: i64 = 6 |
| 62 | const TSD_NFDS_BOTH: i64 = 2 |
| 63 | const TSD_NFDS_ONE: i64 = 1 |
| 64 | const TSD_BYTE_MASK: i64 = 0xff |
| 68 | const TSD_FD_BYTES: i64 = 4 |
| 69 | const TSD_I16_BYTES: i64 = 2 |
| 70 | const TSD_BITS_PER_BYTE: i64 = 8 |
| 72 | const TSD_CONF: *u8 = "knowledge/trafficsafety_drain.conf" as *u8 |
| 73 | const TSD_KEY: *u8 = "drain_deadline_s" as *u8 |
| 74 | const TSD_CONF_ABSENT: i64 = 0 - 1 |
| 78 | const TSD_OK: i64 = 0 |
| 79 | const TSD_ERR_BLOCK: i64 = 1 |
| 80 | const TSD_ERR_FD: i64 = 2 |
| 83 | const TSD_W_DRAIN: i64 = 1 |
| 84 | const TSD_W_CONN: i64 = 0 |
| 85 | const TSD_W_NONE: i64 = 0 - 1 |
| 86 | const TSD_WAIT_BLOCK: i64 = 0 - 1 |
| 89 | const TSD_SRC_DERIVED: i64 = 0 |
| 90 | const TSD_SRC_CONF: i64 = 1 |
functions
| 104 | func ts_drain_deadline_s(out_src: *i64) -> i64 |
| 123 | func ts_drain_on_term(out_verdict: *i64) -> i64 |
| 139 | func ts_drain_requested(drain_fd: i64) -> i64 |
| 147 | func tsd_pollfd_set(p: *u8, idx: i64, fd: i64) -> i64 called by 1: ts_drain_wait |
| 164 | func tsd_pollfd_ready(p: *u8, idx: i64) -> i64 called by 1: ts_drain_wait |
| 183 | func ts_drain_wait(listen_fd: i64, drain_fd: i64, timeout_ms: i64) -> i64 called by 4: maintd_subject_inflighttd_subject_idlemain calls 4: sys_mmaptsd_pollfd_setsys_polltsd_pollfd_ready |