nx_daemon_reap_gate.nx source
↩ module page · 149 lines · 8637 B
1// nx_daemon_reap_gate.nx -- referee for nx_daemon_reap_lib, the ONE child reaper every forking daemon
2// in the estate should compose.
3//
4// It gates the SAME functions a daemon would run, not a reimplementation, and it reaps REAL children it
5// forks itself -- a reaper proven only on synthetic inputs has never met wait4.
6//
7// THE TEETH THAT MATTER:
8// T1 is the empty-set case, and it is not decoration: a drain called with no children must RETURN, not
9// block. Getting WNOHANG wrong turns every idle accept loop in the estate into a hang.
10// T3 proves reaping actually happens against children this gate created, so a pass cannot come from a
11// function that quietly does nothing -- the exact vacuity that let sixteen daemons leak for weeks.
12// T6/T7 are the counter-underflow guard. A live-child counter that goes NEGATIVE turns a concurrency
13// cap into a permanent grant, and that failure direction is silent: the daemon keeps serving, keeps
14// forking, and nothing looks wrong until the box is the thing that reports it.
15// T5 neg-control: below the cap the throttle must NOT wait. A throttle that always blocks would pass
16// every "did it reap?" tooth while serialising the whole server.
17// T8 bite: the drain FIRES on an exited child and stays SILENT when there is none, in one cell.
18// license_tier: ORIGINAL expect_exit: 0
19import "nx_gate_verdict.nx"
20import "nx_daemon_reap_lib.nx"
21
22// Bound on the reap-observation spin. A forked child that calls sys_exit immediately is normally reaped
23// within a handful of iterations; this bound exists so a scheduling stall on a loaded box FAILS THE
24// FIXTURE TOOTH LOUDLY instead of hanging the gate forever. Each iteration is one wait4, so the worst
25// case is roughly a second of syscalls, not an unbounded wait.
26const DRG_SPIN_MAX: i64 = 1000000
27const DRG_KIDS: i64 = 3
28// How long the T9 child stays ALIVE before exiting on its own. Long enough that the parent's drain is
29// certainly called while the child is still running, short enough that a BLOCKING mutant merely returns
30// late instead of hanging the roster.
31const DRG_ALIVE_MS: i64 = 300
32
33func main(argc: i64, argv: *i64) -> i64 {
34 let ctr: *i64 = gv_ctr()
35 gv_head("nx_daemon_reap_gate -- the shared child reaper (nx_daemon_reap_lib)" as *u8)
36
37 let status: *i64 = sys_mmap(8) as *i64
38
39 // T1 -- EMPTY SET: with no children at all, the non-blocking drain returns 0 and RETURNS.
40 // If WNOHANG were passed wrong this call would block here and the gate would never print again,
41 // which is itself the signal -- but the tooth states the contract explicitly.
42 var t1: i64 = 0
43 if dr_reap_nonblocking(status) == 0 { t1 = 1 }
44 gv_check("T1-drain-with-no-children-returns-zero-and-does-not-block" as *u8, t1, ctr)
45
46 // T2 -- FIXTURE REACHED: fork DRG_KIDS children that exit at once. Assert every fork SUCCEEDED before
47 // asserting anything about reaping -- a fork that failed would make T3 pass for the wrong reason.
48 var forked: i64 = 0
49 var i: i64 = 0
50 while i < DRG_KIDS {
51 let pid: i64 = sys_fork()
52 if pid == 0 { sys_exit(0) }
53 if pid > 0 { forked = forked + 1 }
54 i = i + 1
55 }
56 var t2: i64 = 0
57 if forked == DRG_KIDS { t2 = 1 }
58 gv_check("T2-fixture-reached-all-three-children-forked" as *u8, t2, ctr)
59 gv_puts(" forked=" as *u8); gv_num(forked); gv_puts(" of " as *u8); gv_num(DRG_KIDS); gv_puts("\n" as *u8)
60
61 // T3 -- the blocking reap collects exactly one child per call, three times over.
62 var reaped: i64 = 0
63 var j: i64 = 0
64 while j < DRG_KIDS { reaped = reaped + dr_reap_one(status); j = j + 1 }
65 var t3: i64 = 0
66 if reaped == DRG_KIDS { t3 = 1 }
67 gv_check("T3-blocking-reap-collects-one-real-child-per-call" as *u8, t3, ctr)
68 gv_puts(" reaped=" as *u8); gv_num(reaped); gv_puts("\n" as *u8)
69
70 // T4 -- after the children are gone the drain is back to the empty-set answer.
71 var t4: i64 = 0
72 if dr_reap_nonblocking(status) == 0 { t4 = 1 }
73 gv_check("T4-drain-returns-to-zero-once-every-child-is-reaped" as *u8, t4, ctr)
74
75 // T5 -- NEG-CONTROL: below the cap the throttle must not wait and must not touch the counter.
76 // A throttle that always blocks would still satisfy every reaping tooth above while serialising a
77 // fork-per-connection server down to one request at a time.
78 let live: *i64 = sys_mmap(8) as *i64
79 live[0] = 2
80 var t5: i64 = 0
81 if dr_throttle(status, live, 8) == 0 { if live[0] == 2 { t5 = 1 } }
82 gv_check("neg-control-T5-throttle-below-cap-does-not-wait-and-leaves-counter-alone" as *u8, t5, ctr)
83
84 // T6 -- at the cap with NO children the throttle reaps nothing and the counter does not underflow.
85 live[0] = 0
86 var t6: i64 = 0
87 if dr_throttle(status, live, 0) == 0 { if live[0] == 0 { t6 = 1 } }
88 gv_check("T6-throttle-at-cap-with-no-children-leaves-counter-at-zero" as *u8, t6, ctr)
89
90 // T7 -- THE UNDERFLOW GUARD, against a real child. live starts at 0 while one child actually exists,
91 // so an unclamped drain would drive the counter to -1. Bounded spin; the fixture tooth reports
92 // honestly if the child was never observed rather than letting the outcome tooth pass vacuously.
93 let live2: *i64 = sys_mmap(8) as *i64
94 live2[0] = 0
95 var kid: i64 = sys_fork()
96 if kid == 0 { sys_exit(0) }
97 var got: i64 = 0
98 var spins: i64 = 0
99 while spins < DRG_SPIN_MAX {
100 got = got + dr_drain(status, live2)
101 if got >= 1 { spins = DRG_SPIN_MAX } else { spins = spins + 1 }
102 }
103 var t7a: i64 = 0
104 if kid > 0 { if got == 1 { t7a = 1 } }
105 gv_check("T7a-fixture-reached-the-real-child-was-observed-and-drained" as *u8, t7a, ctr)
106 var t7b: i64 = 0
107 if live2[0] == 0 { t7b = 1 }
108 gv_check("T7b-live-counter-clamps-at-zero-and-never-goes-negative" as *u8, t7b, ctr)
109 gv_puts(" got=" as *u8); gv_num(got); gv_puts(" live_after=" as *u8); gv_num(live2[0]); gv_puts("\n" as *u8)
110
111 // T8 -- BITE, in one cell: the drain must FIRE on an exited child and stay SILENT with none.
112 // Two teeth that each see only one condition cannot show the function discriminates between them.
113 var bad: i64 = 0
114 let kid2: i64 = sys_fork()
115 if kid2 == 0 { sys_exit(0) }
116 var s2: i64 = 0
117 while s2 < DRG_SPIN_MAX {
118 if dr_reap_nonblocking(status) > 0 { bad = 1; s2 = DRG_SPIN_MAX } else { s2 = s2 + 1 }
119 }
120 var good: i64 = 0
121 if dr_reap_nonblocking(status) > 0 { good = 1 }
122 gv_bite("T8-bite-drain-fires-on-an-exited-child-and-is-silent-with-none" as *u8, bad, good, ctr)
123
124 // T9 -- THE WNOHANG DISCRIMINATOR, the tooth this gate was missing and the reason a mutation run over
125 // it kept coming back INCONCLUSIVE. NO EXISTING TOOTH SEPARATES NON-BLOCKING FROM BLOCKING: T1 tests
126 // the EMPTY SET and passes either way, and T3 exercises the blocking path on purpose. So a mutant that
127 // turned WNOHANG into a blocking wait changed no verdict, and the gate could not be shown able to fail.
128 // This forks a child that is ALIVE AND NOT YET EXITED while the drain is called: a correct drain
129 // returns 0 at once, a blocking one waits for the child and comes back with 1.
130 // THE DEADLOCK HAZARD IS SOLVED RATHER THAN ACCEPTED. An earlier design had the child wait for the
131 // PARENT to release it, which meant a blocking mutant would HANG the roster instead of going RED -- a
132 // harness timeout is a far weaker signal than a kill, and shipping that would have traded a known-weak
133 // gate for one that can wedge the beat. The child now sleeps a BOUNDED time and exits on its own, so a
134 // blocking mutant merely returns late with the wrong answer and this tooth fails cleanly.
135 // sys_poll with no descriptors is the sleep: sys_nanosleep is ABSENT-PROVEN in this estate.
136 let kid3: i64 = sys_fork()
137 if kid3 == 0 { sys_poll(0 as *u8, 0, DRG_ALIVE_MS); sys_exit(0) }
138 var t9a: i64 = 0
139 if kid3 > 0 { t9a = 1 }
140 gv_check("T9a-fixture-reached-a-child-was-forked-and-is-still-running" as *u8, t9a, ctr)
141 var t9b: i64 = 0
142 if dr_reap_nonblocking(status) == 0 { t9b = 1 }
143 gv_check("T9b-drain-does-NOT-block-while-a-live-unexited-child-exists" as *u8, t9b, ctr)
144 var t9c: i64 = 0
145 if dr_reap_one(status) == 1 { t9c = 1 }
146 gv_check("T9c-and-that-same-child-is-still-collected-once-it-exits" as *u8, t9c, ctr)
147
148 return gv_verdict("DAEMON-REAP-GATE" as *u8, ctr, "the reaper is proven against real forked children, the drain distinguishes an exited child from none, the throttle waits only at the cap, and the live counter cannot underflow" as *u8)
149}