code wiki / (root) / nx_ts_slot_lib.nx

nx_ts_slot_lib.nx source

↩ module page · 215 lines · 9468 B

1// nx_ts_slot_lib.nx -- TS3: THE SLOT-FLIP DECISION CORE (importable, no main). 2// 3// /compare/trafficsafety rung TS3. Two backend SLOTS on two ports behind one small front door that 4// owns the upstream choice and is independent of both. Accept rule taken VERBATIM from 5// trafficsafety.plan: "a flip changes which slot answers with zero failed requests across the flip, 6// a slot that fails its warm-up probe is NEVER flipped to, and the front door's own uptime is 7// unbroken across the whole exercise." 8// 9// IT DOES NOT TOUCH sites.elf OR nx_proxy_route, AND THAT IS A DESIGN DECISION, NOT AN OMISSION. 10// The live front door is what all 69 route rows cross; editing it to gain a deploy-safety property is 11// the one change whose blast radius is every request in the estate. So this ships as a STANDALONE 12// router that fronts one non-critical backend. The pattern is identical and provable; adopting it for 13// the estate's real front door is an operator decision that should be taken against this evidence, 14// not smuggled in beneath it. 15// 16// THE FRONT DOOR IS THE ONLY THING THAT MUST NEVER RESTART, so it is also the only thing that must be 17// replaceable without a gap -- and that is precisely why TS3 depends on TS1. This router takes its 18// own listener from ts_handoff_nodrop, so the component whose restart would be an outage is itself 19// hot-replaceable. A front door that cannot be replaced hitlessly is refused rather than deployed; 20// this one closes that dependency in its own first line of work rather than deferring it. 21// 22// THE UPSTREAM CHOICE IS DATA, NOT A RECOMPILED BRANCH. The active slot lives in a conf file read 23// through rm_conf -- the estate's line-anchored reader, not a second one -- and it is re-read PER 24// CONNECTION. So a flip takes effect on the very next request with no signal, no restart and no 25// reload race, and the router holds no cached routing state that could disagree with the file. 26// 27// A FLIP IS GUARDED BY A WARM-UP PROBE AND THE GUARD FAILS TOWARD DOING NOTHING. ts_slot_flip probes 28// the CANDIDATE before it writes, and on a cold candidate it leaves the active slot exactly where it 29// was and returns a NAMED refusal. A heuristic that gates a destructive action must be wrong in the 30// direction of doing nothing, and flipping traffic onto a dead slot is the destructive action here. 31// SCOPE OF THE PROBE, STATED RATHER THAN IMPLIED: it establishes that the candidate ACCEPTS A 32// CONNECTION AND ANSWERS BYTES. That is liveness, not health. The estate already owns the deeper 33// instrument -- nx_http_probe_lib's hp_run, with thirteen distinct exit codes so a compound health 34// assertion cannot fail without naming WHICH half failed -- and the honest upgrade path is to compose 35// that, never to re-implement a second probe here. 36// license_tier: ORIGINAL No hw writes (Rule 26). 37 38import "nx_syscalls.nx" 39import "nx_http_server.nx" // ts_handoff_nodrop -- TS1, so the front door is itself replaceable 40import "nx_ts_drain_lib.nx" // ts_drain_on_term / ts_drain_wait -- TS2, so it drains rather than cuts 41import "nx_resmon_lib.nx" // rm_conf: the estate's line-anchored conf reader -- NOT a second one 42 43const TSR_SA_BYTES: i64 = 16 44// sockaddr_in FIELD OFFSETS, named so a reader can check them against the platform layout instead of 45// meeting bare indices at the call site: sin_family at 0, sin_port at 2 (big-endian), sin_addr at 4. 46const TSR_SA_PORT_OFF: i64 = 2 47const TSR_SA_ADDR_OFF: i64 = 4 48const TSR_ADDR_BYTES: i64 = 4 49const TSR_BYTE_RADIX: i64 = 256 50const TSR_LOOPBACK_A: i64 = 127 51const TSR_LOOPBACK_D: i64 = 1 52const TSR_BACKLOG: i64 = 16 53const TSR_OUT_BYTES: i64 = 64 54const TSR_NUM_SCRATCH: i64 = 24 55const TSR_ASCII_0: i64 = 48 56const TSR_ASCII_9: i64 = 57 57const TSR_B10: i64 = 10 58const TSR_BUF_CAP: i64 = 65536 59const TSR_PROBE_BYTE: i64 = 80 60 61// Slot identity. Two slots is the whole point -- blue and green -- and the count is named so a reader 62// does not meet a bare 2 at a call site. 63const TSR_SLOT_A: i64 = 0 64const TSR_SLOT_B: i64 = 1 65const TSR_SLOT_N: i64 = 2 66 67// ts_slot_flip verdicts -- each NAMES why, because a refusal that cannot say which rule fired sends 68// the reader at the wrong cause. 69const TSR_FLIP_OK: i64 = 0 70const TSR_FLIP_NOOP: i64 = 1 71const TSR_FLIP_COLD: i64 = 2 72const TSR_FLIP_BAD_SLOT: i64 = 3 73const TSR_FLIP_WRITE_ERR: i64 = 4 74 75const TSR_KEY_ACTIVE: *u8 = "active_slot" as *u8 76const TSR_ARGC_MIN: i64 = 5 77const TSR_A_FRONT: i64 = 1 78const TSR_A_SLOTA: i64 = 2 79const TSR_A_SLOTB: i64 = 3 80const TSR_A_STATE: i64 = 4 81const TSR_A_SOCK: i64 = 5 82 83func tsr_atoi(s: *u8) -> i64 { 84 var v: i64 = 0 85 var i: i64 = 0 86 while s[i] != (0 as u8) { 87 if s[i] >= (TSR_ASCII_0 as u8) { if s[i] <= (TSR_ASCII_9 as u8) { v = v * TSR_B10 + ((s[i] as i64) - TSR_ASCII_0) } } 88 i = i + 1 89 } 90 return v 91} 92 93func tsr_itoa(v: i64, out: *u8) -> i64 { 94 if v == 0 { out[0] = TSR_ASCII_0 as u8; out[1] = 0 as u8; return 1 } 95 var m: i64 = v 96 let t: *u8 = sys_mmap(TSR_NUM_SCRATCH) 97 var k: i64 = 0 98 while m > 0 { t[k] = ((TSR_ASCII_0 + (m - (m / TSR_B10) * TSR_B10)) as u8); m = m / TSR_B10; k = k + 1 } 99 var j: i64 = 0 100 while j < k { out[j] = t[k - 1 - j]; j = j + 1 } 101 out[k] = 0 as u8 102 return k 103} 104 105func tsr_sa(sa: *u8, port: i64) -> i64 { 106 var i: i64 = 0 107 while i < TSR_SA_BYTES { sa[i] = 0; i = i + 1 } 108 sa[0] = AF_INET 109 // The zero-fill above already cleared every other octet, so only the non-zero lanes are written. 110 sa[TSR_SA_PORT_OFF] = port / TSR_BYTE_RADIX 111 sa[TSR_SA_PORT_OFF + 1] = port % TSR_BYTE_RADIX 112 sa[TSR_SA_ADDR_OFF] = TSR_LOOPBACK_A 113 sa[TSR_SA_ADDR_OFF + TSR_ADDR_BYTES - 1] = TSR_LOOPBACK_D 114 return 0 115} 116 117func tsr_dial(port: i64) -> i64 { 118 let sa: *u8 = sys_mmap(TSR_SA_BYTES) 119 tsr_sa(sa, port) 120 let fd: i64 = sys_socket(AF_INET, SOCK_STREAM, 0) 121 if fd < 0 { return 0 - 1 } 122 sys_set_socket_timeout(fd, ACCEPT_TMO_S) 123 if sys_connect(fd, sa, TSR_SA_BYTES) < 0 { sys_close(fd); return 0 - 1 } 124 return fd 125} 126 127// THE ACTIVE SLOT IS READ FROM DATA, EVERY TIME. An absent or unparsable state file yields slot A, 128// which is the fail-safe direction for a front door: serve from the known-first slot rather than 129// refuse every request because a config read went wrong. 130func ts_slot_active(state_path: *u8) -> i64 { 131 let nl: *i64 = (sys_mmap(TSR_OUT_BYTES)) as *i64 132 nl[0] = 0 133 let buf: *u8 = sys_read_file(state_path, nl) 134 if buf == (0 as *u8) { return TSR_SLOT_A } 135 if nl[0] <= 0 { return TSR_SLOT_A } 136 let v: i64 = rm_conf(buf, nl[0], TSR_KEY_ACTIVE, TSR_SLOT_A) 137 if v < TSR_SLOT_A { return TSR_SLOT_A } 138 if v >= TSR_SLOT_N { return TSR_SLOT_A } 139 return v 140} 141 142// THE WARM-UP PROBE. Liveness, not health -- see the scope note in the header. It connects and 143// requires the candidate to answer bytes, so a port that is bound but wedged before its first write 144// does NOT read as warm. 145func ts_slot_probe(port: i64) -> i64 { 146 let fd: i64 = tsr_dial(port) 147 if fd < 0 { return 0 } 148 let b: *u8 = sys_mmap(TSR_BUF_CAP) 149 b[0] = TSR_PROBE_BYTE 150 if sys_write(fd, b, 1) != 1 { sys_close(fd); return 0 } 151 let r: i64 = sys_read(fd, b, TSR_BUF_CAP) 152 sys_close(fd) 153 if r > 0 { return 1 } 154 return 0 155} 156 157// THE FLIP. Probe the CANDIDATE first and refuse by name on a cold one, leaving the active slot 158// exactly where it was -- wrong in the direction of doing nothing. Writes the state file, which the 159// router re-reads per connection, so the change takes effect on the next request with no restart. 160func ts_slot_flip(state_path: *u8, ports: *i64, want: i64) -> i64 { 161 if want < TSR_SLOT_A { return TSR_FLIP_BAD_SLOT } 162 if want >= TSR_SLOT_N { return TSR_FLIP_BAD_SLOT } 163 if ts_slot_active(state_path) == want { return TSR_FLIP_NOOP } 164 if ts_slot_probe(ports[want]) == 0 { return TSR_FLIP_COLD } 165 let buf: *u8 = sys_mmap(TSR_NUM_SCRATCH * TSR_SLOT_N) 166 var o: i64 = 0 167 var i: i64 = 0 168 while TSR_KEY_ACTIVE[i] != (0 as u8) { buf[o] = TSR_KEY_ACTIVE[i]; o = o + 1; i = i + 1 } 169 buf[o] = 32 as u8 170 o = o + 1 171 let nb: *u8 = sys_mmap(TSR_NUM_SCRATCH) 172 let nlen: i64 = tsr_itoa(want, nb) 173 var j: i64 = 0 174 while j < nlen { buf[o] = nb[j]; o = o + 1; j = j + 1 } 175 buf[o] = 10 as u8 176 o = o + 1 177 let fd: i64 = sys_openat_wr(state_path, MODE_0644) 178 if fd < 0 { return TSR_FLIP_WRITE_ERR } 179 let w: i64 = sys_write(fd, buf, o) 180 sys_fsync(fd) 181 sys_close(fd) 182 if w != o { return TSR_FLIP_WRITE_ERR } 183 return TSR_FLIP_OK 184} 185 186// Forward one client exchange to the currently-active slot. Returns 1 when the client got bytes. 187func tsr_forward(cfd: i64, upstream_port: i64, buf: *u8) -> i64 { 188 let n: i64 = sys_read(cfd, buf, TSR_BUF_CAP) 189 if n <= 0 { return 0 } 190 let ufd: i64 = tsr_dial(upstream_port) 191 if ufd < 0 { return 0 } 192 if sys_write(ufd, buf, n) != n { sys_close(ufd); return 0 } 193 let r: i64 = sys_read(ufd, buf, TSR_BUF_CAP) 194 sys_close(ufd) 195 if r <= 0 { return 0 } 196 var off: i64 = 0 197 while off < r { 198 let w: i64 = sys_write(cfd, ((buf as i64) + off) as *u8, r - off) 199 if w <= 0 { return 0 } 200 off = off + w 201 } 202 return 1 203} 204 205func tsr_puts(s: *u8) -> i64 { 206 var n: i64 = 0 207 while s[n] != (0 as u8) { n = n + 1 } 208 return sys_write(1, s, n) 209} 210func tsr_putn(v: i64) -> i64 { 211 let b: *u8 = sys_mmap(TSR_NUM_SCRATCH) 212 let n: i64 = tsr_itoa(v, b) 213 return sys_write(1, b, n) 214} 215