1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
//! Ending a node whose `dora run` parent was killed outright.
//!
//! Every cooperative teardown path — the `Stop` event, the daemon's
//! SIGTERM/SIGKILL ladder, the destroy reaper — needs the daemon to still be
//! running. `SIGKILL` to `dora run` leaves none of them: the signal is neither
//! catchable nor blockable, so no CLI- or daemon-side code executes afterwards.
//!
//! A node that polls its event stream still notices, because the daemon
//! connection hits EOF. A node that does not poll — one busy in a long
//! computation, or blocked on a device — notices nothing and runs forever with
//! `ppid 1`, holding whatever it held (dora-rs/dora#2856). Nodes are spawned as
//! process-group *leaders*, on purpose, so that a terminal `Ctrl-C` cannot kill
//! them out from under the daemon; the same property means neither inherited
//! signal delivery nor a group-kill of the CLI can reach the orphan.
//!
//! So the node watches for itself. The daemon hands it
//! [`DORA_RUN_PARENT_PID_ENV`] — and *only* when daemon and node-parent are the
//! same process, which is `dora run` and the other in-process
//! `Daemon::run_dataflow` callers. Once that pid is gone, this guard `SIGKILL`s
//! the node's own process group.
//!
//! # Why the whole group, not just this process
//!
//! The process the daemon tracks is frequently a wrapper: `uv run python
//! node.py`, `sh -c ...`, a console script. The real node is one level down, in
//! the wrapper's process group. Ending only the calling process would leave the
//! interpreter behind — the same orphan, one level down — so the guard signals
//! the group, exactly as the daemon's own reaper does.
//!
//! # Why not `PR_SET_PDEATHSIG`
//!
//! Linux can ask the kernel to signal a child when its parent dies, which
//! needs no thread and covers a node that is killed before it reaches `init`.
//! It is not enough on its own, and it is not free:
//!
//! - It reaches only the *direct* child. Under `--uv` that child is `uv run
//! python ...` and the node is one level down, so the interpreter is left
//! exactly as orphaned as before — the shape this issue was reported for.
//! - It fires when the parent **thread** exits, not the parent process. The
//! daemon spawns from a tokio worker thread, so the guarantee is only as
//! stable as which thread happened to run the spawn — a spawn moved behind
//! `spawn_blocking` would start killing nodes early, silently.
//! - It is Linux-only, and the report is from macOS.
//!
//! It remains a reasonable *complement* for the pre-`init` window (see
//! "Coverage" below); it is not the mechanism.
//!
//! # Why `SIGKILL` rather than a `SIGTERM` grace period first
//!
//! By the time this fires the daemon is already gone: there is nothing to flush
//! outputs to, no coordinator to report to, and no one to answer a cooperative
//! stop. A grace period would also be self-defeating here — `SIGTERM` to the
//! group includes *this* process, so if it dies on the first rung nothing is
//! left to deliver the second one to a sibling that ignores `SIGTERM`.
//!
//! # Coverage
//!
//! A node is guarded from [`DoraNode::init`][crate::DoraNode::init] onwards, so
//! two gaps remain. A process killed *before* it gets there — a Python node
//! still in `import torch`, a `uv run` still resolving dependencies — is
//! orphaned as before. And a node that never calls `init` at all (a `path:
//! shell` command) is never guarded, because nothing of dora's runs in it.
//!
//! Unix only; on Windows this module compiles to a no-op and the gap remains.
//! Windows has no process groups in this sense, so a node cannot contain its
//! own tree the way `killpg` does here. The idiomatic equivalent is the job
//! object the daemon already spawns nodes into (`process_wrap`'s `JobObject`),
//! which ties the tree to the parent's handle only once
//! `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE` is set — a daemon-side change that
//! `process-wrap` couples to its `KillOnDrop` wrapper, and therefore to
//! `Command::kill_on_drop`, altering when every node dies. That is not
//! verifiable from this workspace, so it is left to a follow-up rather than
//! shipped untested (dora-rs/dora#2856).
use DORA_RUN_PARENT_PID_ENV;
use Once;
/// How often the parent is re-checked.
///
/// The parent is already dead when this matters, so the interval is pure
/// latency between the kill and the node's exit; it buys nothing to poll
/// faster, and one sleeping thread per node costs nothing to poll this often.
const POLL_INTERVAL: Duration = from_millis;
/// One guard per process, so initializing several nodes (the operator runtime
/// does) does not accumulate threads all watching the same pid.
static ARMED: Once = new;
/// Arm the guard if this node was spawned by `dora run`.
///
/// A no-op when [`DORA_RUN_PARENT_PID_ENV`] is absent, which is every other
/// way a node starts: `dora up` + `dora start` (where the node is *meant* to
/// outlive daemon restarts — dora-rs/dora#2029), a manually launched dynamic
/// node, interactive mode, and the integration-test harness.
pub
/// What this node will do once `parent` is gone, decided while the parent is
/// still alive and therefore still inspectable.
/// Report a guard problem without depending on a `tracing` subscriber being
/// installed (nodes commonly have none) and without `eprintln!`'s panic on a
/// broken stderr pipe.