Skip to main content

lmflow/graph/
lifecycle.rs

1use super::scheduler::KernelPhase;
2use super::*;
3
4// Graph start, shutdown, reset, and wait state machine.
5
6impl Graph {
7    /// Start execution. A fatal Open error is retained and returned on subsequent
8    /// attempts; missing required side packets may be supplied before retrying.
9    pub fn start(&self) -> Result<()> {
10        self.inner.start()
11    }
12
13    pub fn reset(&self) -> Result<()> {
14        self.inner.reset()
15    }
16
17    pub fn wait_done(&self) -> Result<()> {
18        self.inner.wait_done(None)
19    }
20
21    pub fn wait_until_idle(&self) -> Result<()> {
22        self.inner.wait_until_idle(None)
23    }
24}
25
26impl GraphInner {
27    /// 一次调用完成后:尽力再填满容量(并行调度多个 in-flight),并尝试关闭。
28    pub(super) fn finish(&self, n: NodeId) {
29        self.schedule_source_resumption(n);
30        self.schedule_node(n);
31        self.maybe_close(n);
32    }
33
34    /// 关流推进:所有输入已关且排空 → close 算子 → 关自己的输出边 → 递归下游。
35    pub(super) fn maybe_close(&self, n: NodeId) -> bool {
36        let node = &self.nodes[n];
37        let force = self.shared.has_error() || self.shared.is_cancelled();
38
39        // 在锁下**认领**关流:并发时(宿主线程与工作线程可能同时到这里)
40        // 只有一个线程能置位 close_started,从而保证算子的 Close 只被调用一次。
41        // in_flight != 0 表示还有并行调用在跑或等待刷新 —— 必须全部落地才能关。
42        {
43            let mut s = node.sched.lock().expect("scheduler lock poisoned");
44            if s.close_started || s.in_flight != 0 || !s.opened {
45                return false;
46            }
47            if !force && !node.all_inputs_closed_and_drained() {
48                return false;
49            }
50            s.close_started = true;
51        }
52
53        if force {
54            self.discard_queued_inputs(n);
55        }
56
57        // 此刻 in_flight==0,所有槽空闲;Close 是串行的,用槽 0。
58        {
59            let ctx = unsafe { node.ctx_slot(0) };
60            ctx.reset();
61            ctx.close_reason = self.shared.close_reason();
62            ctx.input_ts = Timestamp::done();
63        }
64        let rc = self.call_kernel(n, 0, KernelPhase::Close);
65        if rc != 0 {
66            let ctx = unsafe { node.ctx_slot(0) };
67            let e = ctx.take_error(rc);
68            ctx.discard_staging();
69            self.shared.record_error(e);
70        } else if let Err(e) = self.check_output_types(n, 0) {
71            unsafe { node.ctx_slot(0) }.discard_staging();
72            self.shared.record_error(e);
73        } else {
74            match self.flush_staging(n, 0) {
75                Ok(true) => {}
76                Ok(false) => {
77                    let mut sched = node.sched.lock().expect("scheduler lock poisoned");
78                    sched.blocked_flush = Some(BlockedFlush::Close);
79                    sched.flushing = false;
80                    self.blocked_flush_nodes
81                        .lock()
82                        .expect("blocked flush lock poisoned")
83                        .insert(n);
84                    return false;
85                }
86                Err(error) => {
87                    unsafe { node.ctx_slot(0) }.discard_staging();
88                    self.shared.record_error(error);
89                }
90            }
91        }
92        unsafe { node.ctx_slot(0) }.clear_inputs();
93        node.sched.lock().expect("scheduler lock poisoned").closed = true;
94
95        for &e in &node.outputs {
96            self.close_edge(e);
97        }
98        self.notify_activity();
99        true
100    }
101
102    /// Release unclaimed input packets on forced shutdown, preserving already
103    /// published poller output and its contribution to the global queue counters.
104    fn discard_queued_inputs(&self, n: NodeId) {
105        let node = &self.nodes[n];
106        for (port, queue) in node.input_queues.iter().enumerate() {
107            let discarded = {
108                let mut queue = queue.lock().expect("queue lock poisoned");
109                let discarded = std::mem::take(&mut *queue);
110                node.input_queue_bytes[port].store(0, Ordering::SeqCst);
111                for packet in &discarded {
112                    self.shared.on_dequeue(packet.byte_size());
113                }
114                discarded
115            };
116            // Payload destructors may call host code; never run them under a queue lock.
117            drop(discarded);
118        }
119        self.notify_activity();
120    }
121
122    pub(super) fn close_edge(&self, edge: EdgeId) {
123        let e = &self.edges[edge];
124        let _ingress = if e.is_graph_input {
125            // Callbacks dispatching different inputs must not wait on each
126            // other's ingress ownership. Each sender commits the requested
127            // close after its accepted packet has reached every consumer.
128            let defer_if_busy = self
129                .graph_inputs
130                .iter()
131                .any(|&input| self.edges[input].is_ingress_owner());
132            match e.enter_ingress_close(defer_if_busy) {
133                Some(guard) => Some(guard),
134                None => return, // The current dispatch completes this close.
135            }
136        } else {
137            None
138        };
139        self.close_edge_committed(edge);
140    }
141
142    pub(super) fn close_edge_committed(&self, edge: EdgeId) {
143        let e = &self.edges[edge];
144        if e.closed.swap(true, Ordering::SeqCst) {
145            return; // 已关
146        }
147        self.publish_bound(edge, Timestamp::done());
148        for &(node, port) in &e.consumers {
149            self.nodes[node].input_closed[port].store(true, Ordering::SeqCst);
150            // 关闭即「永远不会再有数据」,边界直接到 Done,让下游不必再等这一路
151            self.nodes[node].advance_bound(port, Timestamp::done());
152            // 关流会改变就绪判定(空口不再阻塞对齐),必须重扫
153            self.schedule_node(node);
154        }
155        // 该边的 poller 在队列排空后即视为结束
156        for p in e.pollers.lock().expect("poller list lock poisoned").iter() {
157            p.closed.store(true, Ordering::SeqCst);
158        }
159        // Closing an idle input/output may enqueue no executor task. Wake both
160        // blocking readers and event-loop hosts so closure can still be observed.
161        self.notify_activity();
162        self.request_wakeup();
163    }
164
165    pub(super) fn set_state_draining_if_all_inputs_closed(&self) {
166        let all = self.graph_inputs.iter().all(|&e| self.edges[e].is_closed());
167        if all {
168            let mut st = self.state.lock().expect("state lock poisoned");
169            if *st == State::Running {
170                *st = State::Draining;
171            }
172        }
173    }
174
175    /// 尝试推进任一节点的关流;返回是否有进展。
176    pub(super) fn try_advance_closing(&self) -> bool {
177        let mut progressed = false;
178        for n in 0..self.nodes.len() {
179            if self.maybe_close(n) {
180                progressed = true;
181            }
182        }
183        if !progressed && self.all_nodes_closed() {
184            self.set_state(State::Terminated);
185        }
186        progressed
187    }
188
189    pub(super) fn all_nodes_closed(&self) -> bool {
190        self.nodes
191            .iter()
192            .all(|n| n.sched.lock().expect("scheduler lock poisoned").closed)
193    }
194
195    /// 距 deadline 还剩多久;`None` 表示已超时。无 deadline 时返回一个固定的
196    /// 轮询上限,以免因通知丢失而永久挂住。
197    pub(super) fn remaining(
198        &self,
199        deadline: Option<std::time::Instant>,
200    ) -> Option<std::time::Duration> {
201        const POLL_CAP: std::time::Duration = std::time::Duration::from_millis(50);
202        match deadline {
203            None => Some(POLL_CAP),
204            Some(d) => {
205                let now = std::time::Instant::now();
206                if now >= d {
207                    None
208                } else {
209                    Some((d - now).min(POLL_CAP))
210                }
211            }
212        }
213    }
214    pub(super) fn start(self: &Arc<Self>) -> Result<()> {
215        let st = self.state();
216        if st != State::Initialized {
217            return Err(Error::State(format!(
218                "start can only be called in Initialized (current {st:?})"
219            )));
220        }
221
222        // A failed Open leaves the graph Initialized, but its fatal error is
223        // retained. Do not rerun callbacks or report Running for an unusable graph.
224        if let Some(error) = self.shared.first_error() {
225            return Err(error);
226        }
227        if self.shared.is_cancelled() {
228            return Err(Error::Cancelled);
229        }
230
231        // 校验算子声明的必需 side packet
232        let provided = self.side_packets.lock().expect("side packet lock poisoned");
233        for (need, who) in &self.required_side_packets {
234            if !provided.contains_key(need) {
235                return Err(Error::InvalidArg(format!(
236                    "missing required side packet `{need}` -- node `{who}` declared it in GetContract; \
237                     inject it with set_side_packet before start"
238                )));
239            }
240        }
241        let sp = Arc::new(provided.clone());
242        drop(provided);
243
244        // 把 side packets 灌进各节点的**所有** context 槽,然后 open(用槽 0,串行)。
245        // reset 重跑时算子实例被保留、`opened` 仍为 true —— 那种情况下**跳过 open**
246        // (不重跑 open 正是 reset 省重载模型的价值),只重灌 side packet + 复位槽。
247        let mut open_outputs = Vec::new();
248        for (i, node) in self.nodes.iter().enumerate() {
249            let already_open = node.sched.lock().expect("scheduler lock poisoned").opened;
250            for slot in 0..node.max_in_flight {
251                // 安全性:尚未开始调度,所有槽空闲,可独占写入。
252                let ctx = unsafe { node.ctx_slot(slot) };
253                ctx.side_packets = sp.clone();
254                ctx.reset();
255                ctx.input_ts = Timestamp::unstarted();
256            }
257            if already_open {
258                continue; // reset 后重跑:算子实例与其 open 状态都保留,不再 open
259            }
260            let rc = self.call_kernel(i, 0, KernelPhase::Open);
261            if rc != 0 {
262                let e = unsafe { node.ctx_slot(0) }.take_error(rc);
263                for node in &self.nodes {
264                    unsafe { node.ctx_slot(0) }.discard_staging();
265                }
266                self.shared.record_error(e.clone());
267                return Err(e);
268            }
269            node.sched.lock().expect("scheduler lock poisoned").opened = true;
270            if let Err(e) = self.check_output_types(i, 0) {
271                for node in &self.nodes {
272                    unsafe { node.ctx_slot(0) }.discard_staging();
273                }
274                self.shared.record_error(e.clone());
275                return Err(e);
276            }
277            let ctx = unsafe { node.ctx_slot(0) };
278            if ctx.staging.iter().any(|packets| !packets.is_empty())
279                || ctx.next_bounds.iter().any(Option::is_some)
280            {
281                open_outputs.push(i);
282            }
283        }
284
285        // Finish all Open callbacks before publishing initialization output.
286        // Reserve slot 0 so Process/Close cannot reset staging under backpressure.
287        for &i in &open_outputs {
288            let mut sched = self.nodes[i].sched.lock().expect("scheduler lock poisoned");
289            sched.free_slots.retain(|&slot| slot != 0);
290            sched.in_flight += 1;
291            sched.blocked_flush = Some(BlockedFlush::Open);
292            self.blocked_flush_nodes
293                .lock()
294                .expect("blocked flush lock poisoned")
295                .insert(i);
296        }
297        self.set_state(State::Running);
298        self.run_started_us
299            .store(self.epoch_us().saturating_add(1), Ordering::Relaxed);
300        *self
301            .dot_intervals
302            .lock()
303            .expect("DOT interval lock poisoned") = dot::DotIntervalBaselines::default();
304
305        // 拉起真正有节点归属的执行器。必须在 Arc 存在之后:工作线程持 Weak,避免 Arc 环。
306        let weak = Arc::downgrade(self);
307        for (executor_id, executor) in self.executors.iter().enumerate() {
308            if self.nodes.iter().any(|node| node.executor == executor_id) {
309                executor.start(weak.clone());
310            }
311        }
312        self.resume_blocked_flushes();
313        // 源节点(0 输入)无输入触发,须在此显式起调度。
314        // 之后由 finish→schedule_node 自我续产,直到内核 source_done() 或图被 cancel。
315        for i in 0..self.nodes.len() {
316            if self.nodes[i].is_source() {
317                self.schedule_node(i);
318            }
319        }
320        Ok(())
321    }
322
323    /// 复位为可再次 `start` 的状态,**保留已 open 的算子实例**(省掉每会话重载模型的
324    /// 开销)。字段的「保留 / 复位」分类见 docs/design.md §7.13。
325    ///
326    /// 前提:图必须**已静止** —— `Terminated` 且 `is_idle()`(没有 worker 还在算子里)。
327    /// 否则返回 `Error::State`。宿主通常先 `wait_done()` 再 `reset()`。
328    ///
329    /// **不碰线程池**:worker 随图存活、此刻都 park 在 condvar 上、`stop` 仍为 false,
330    /// 下一轮 `start` 直接复用(见 executor.rs 模块头);shutdown+join 只发生在 Drop。
331    pub(super) fn reset(&self) -> Result<()> {
332        // 1. 校验静止。in_flight==0 且委托队列为空 ⇒ 没有 worker 在 run_node 中途,
333        //    故下面所有「无并发」的复位都成立(与 Drop / start 用同一条静止依据)。
334        {
335            let st = *self.state.lock().expect("state lock poisoned");
336            if st != State::Terminated || !self.is_idle() {
337                return Err(Error::State(
338                    "reset requires the graph to be Terminated and idle; call wait_done() first"
339                        .into(),
340                ));
341            }
342        }
343
344        // 2. 清 GraphShared:先清 error/cancelled,否则下一轮 start 的 try_claim 会被旧
345        //    has_error 挡回(mod.rs try_claim 首判)。
346        self.shared.reset_run_state();
347        self.blocked_flush_nodes
348            .lock()
349            .expect("blocked flush lock poisoned")
350            .clear();
351
352        // 3. 逐 Edge 复位。last_sent 必须回 unset() —— 否则单调性校验会拒掉下一轮
353        //    从图输入口发的第一个包(时间戳通常又从小开始)。
354        for e in &self.edges {
355            e.reset_ingress();
356            e.closed.store(false, Ordering::SeqCst);
357            e.dropped.store(0, Ordering::Relaxed);
358            e.watermark_backpressure.reset();
359            *e.last_sent.lock().expect("last_sent lock poisoned") = Timestamp::unset();
360            *e.last_published_bound
361                .lock()
362                .expect("published-bound lock poisoned") = Timestamp::unstarted();
363            // poller / observer 是宿主持有、engine 存 Arc —— **保留**列表,只复位内容,
364            // 让宿主复用同一个 Poller 句柄再取下一轮输出。
365            for pl in e.pollers.lock().expect("poller list lock poisoned").iter() {
366                pl.clear(self);
367                pl.closed.store(false, Ordering::SeqCst);
368                pl.dropped.store(0, Ordering::Relaxed);
369                pl.block_backpressure.reset();
370            }
371        }
372
373        // 4. 逐 Node 复位。
374        for node in &self.nodes {
375            // sched 整体重建(一把覆盖 next_seq / free_slots / ready / pending_flush 等全部
376            // 运行态,不会漏),再单独把 opened 置回 true —— **保留 open 是 reset 的价值**。
377            {
378                let mut sc = node.sched.lock().expect("scheduler lock poisoned");
379                let opened = sc.opened;
380                *sc = NodeSched::new(node.max_in_flight);
381                sc.opened = opened;
382            }
383            node.stats.reset();
384            node.kernel.reset_stats();
385            for q in &node.input_queues {
386                q.lock().expect("queue lock poisoned").clear();
387            }
388            for bytes in &node.input_queue_bytes {
389                bytes.store(0, Ordering::SeqCst);
390            }
391            for reserved in &node.input_queue_reserved {
392                reserved.store(0, Ordering::SeqCst);
393            }
394            for stats in &node.input_queue_stats {
395                stats.reset();
396            }
397            for c in &node.input_closed {
398                c.store(false, Ordering::SeqCst);
399            }
400            // input_bounds 必须回 pre_stream()(不是上一轮 close 推到的 done())——
401            // 否则 readiness/对齐会认为每个空口「已到流尾」,语义崩坏。
402            for b in &node.input_bounds {
403                *b.lock().expect("bound lock poisoned") = Timestamp::pre_stream();
404            }
405            node.source_done.store(false, Ordering::SeqCst);
406            node.source_waiting.store(false, Ordering::SeqCst);
407            node.source_wait_reason.store(0, Ordering::Relaxed);
408            node.source_wake_deadline_us.store(0, Ordering::Relaxed);
409            node.source_yield_count.store(0, Ordering::Relaxed);
410            node.source_wake_generation.fetch_add(1, Ordering::SeqCst);
411            *node.last_fire.lock().expect("last_fire lock poisoned") = None;
412            // 逐槽复位 Context:此刻 in_flight==0,与 start/Drop 同为「独占相」,无并发。
413            for slot in 0..node.ctxs.len() {
414                unsafe { node.ctx_slot(slot) }.reset();
415            }
416        }
417
418        // 5. GraphInner 顶层。side_packets 保留(下一轮 start 会自动 clone 进各 ctx)。
419        //    epoch 不动:它只是各诊断时间戳的单调基准。
420        for executor in &self.executors {
421            executor.reset_run_state();
422        }
423        self.in_flight.store(0, Ordering::SeqCst);
424        self.delegated_cursor.store(0, Ordering::Relaxed);
425        self.delegated_running.store(false, Ordering::Release);
426        self.wakeup_pending.store(false, Ordering::Release);
427        self.wakeup_generation.store(0, Ordering::Release);
428        {
429            let mut a = self.activity.0.lock().unwrap_or_else(|e| e.into_inner());
430            a.waiters = 0;
431        }
432        self.paused.store(false, Ordering::SeqCst);
433        self.run_started_us.store(0, Ordering::Relaxed);
434        self.e2e_stats.reset();
435        *self
436            .dot_intervals
437            .lock()
438            .expect("DOT interval lock poisoned") = dot::DotIntervalBaselines::default();
439
440        // 6. 最后置 state —— 前面的清理对「下一次 start」全部可见后,才对外表现为可 start。
441        *self.state.lock().expect("state lock poisoned") = State::Initialized;
442        Ok(())
443    }
444
445    /// 等待图跑完。`deadline` 为 `None` 表示不限时。
446    ///
447    /// 期间会**借用宿主线程**执行主线程任务(默认执行器,§7.9),
448    /// 同时等待线程池里的任务完成。
449    pub(super) fn wait_done(&self, deadline: Option<std::time::Instant>) -> Result<()> {
450        loop {
451            // 先把能自己干的干完
452            // A synchronous callback cannot be preempted, but check the deadline
453            // between tasks even when delegated work remains continuously ready.
454            while self.remaining(deadline).is_some() && self.pump_step() {}
455            if self.all_nodes_closed() && self.workers_idle() {
456                break;
457            }
458            if self.remaining(deadline).is_none() {
459                return Err(Error::Timeout);
460            }
461            // 在判断是否空闲**之前**捕获活动代数,再据此等待 —— 否则会丢唤醒。
462            let before = self.activity_gen();
463            if self.workers_idle() {
464                self.resume_blocked_flushes();
465                // An open graph input is the primary reason `wait_done` cannot finish. Report it
466                // before any blocked-flush diagnostic so callers do not mistake a normal producer
467                // pause for an impossible downstream deadlock.
468                let inputs_open: Vec<&str> = self
469                    .graph_inputs
470                    .iter()
471                    .filter(|&&e| !self.edges[e].is_closed())
472                    .map(|&e| self.edges[e].name.as_str())
473                    .collect();
474                if !inputs_open.is_empty() {
475                    return Err(Error::State(format!(
476                        "wait_done: graph input ports [{}] still open, the graph won't finish on its own -- \
477                         call close_input/close_all_inputs first",
478                        inputs_open.join(", ")
479                    )));
480                }
481                let blocked: Vec<NodeId> = self
482                    .blocked_flush_nodes
483                    .lock()
484                    .expect("blocked flush lock poisoned")
485                    .iter()
486                    .copied()
487                    .collect();
488                if !blocked.is_empty() {
489                    if self.retry_idle_progress(deadline)? {
490                        continue;
491                    }
492                    let details = self.backpressure_stall_details(&blocked);
493                    return Err(Error::Kernel(format!(
494                        "wait_done: internal backpressure cannot make progress; blocked queues: [{}]. \
495                         increase the input queue packet capacity or inspect downstream alignment",
496                        details.join("; ")
497                    )));
498                }
499                if self.retry_idle_progress(deadline)? {
500                    continue;
501                }
502                // 空闲且未全关:再推一轮关流
503                if self.try_advance_closing() {
504                    continue;
505                }
506                // try_advance_closing 把最后一个节点关掉并置 Terminated 时会返回 false
507                // (它不把“到达终态”算作推进)。此时图其实已跑完 —— 常见触发是:工作线程
508                // 在本轮 all_nodes_closed() 判定与这里之间关掉了最后一个节点。必须重判,
509                // 否则会把已完成的图误报成“卡住”(症状:未能关闭的节点列表为空 [])。
510                if self.all_nodes_closed() {
511                    break;
512                }
513                // 推不动了。这时**不能返回 Ok** —— 图并没有跑完。
514                // 区分两种成因,给出可操作的报错而不是静默成功或永久挂住:
515                match self.remaining(deadline) {
516                    Some(duration) => self.wait_activity_since(before, duration),
517                    None => return Err(Error::Timeout),
518                }
519                if self.activity_gen() != before || !self.workers_idle() {
520                    continue;
521                }
522                if self.remaining(deadline).is_none() {
523                    return Err(Error::Timeout);
524                }
525                let stuck: Vec<&str> = (0..self.nodes.len())
526                    .filter(|&n| {
527                        !self.nodes[n]
528                            .sched
529                            .lock()
530                            .expect("scheduler lock poisoned")
531                            .closed
532                    })
533                    .map(|n| self.nodes[n].name.as_str())
534                    .collect();
535                return Err(Error::Kernel(format!(
536                    "wait_done: all inputs closed but the graph is still idle, nodes not closed: [{}]. \
537                     usually some kernel's output/close condition is unmet (use dump to inspect queue backlog)",
538                    stuck.join(", ")
539                )));
540            }
541            // 线程池还在跑:等它有进展(相对刚才捕获的 before)
542            match self.remaining(deadline) {
543                Some(d) => {
544                    self.wait_activity_since(before, d);
545                }
546                None => return Err(Error::Timeout),
547            }
548        }
549        if self.all_nodes_closed() {
550            self.set_state(State::Terminated);
551        }
552        if self.shared.is_cancelled() {
553            return Err(Error::Cancelled);
554        }
555        match self.shared.first_error() {
556            Some(e) => Err(e),
557            None => Ok(()),
558        }
559    }
560
561    /// 等到在途任务都处理完(但不结束图)。
562    pub(super) fn wait_until_idle(&self, deadline: Option<std::time::Instant>) -> Result<()> {
563        loop {
564            while self.remaining(deadline).is_some() && self.run_one_main_task() {}
565            self.resume_blocked_flushes();
566            // 判断空闲**之前**捕获代数,防止丢唤醒(见 activity_gen)。
567            let before = self.activity_gen();
568            if self.is_idle() {
569                break;
570            }
571            if self.remaining(deadline).is_none() {
572                return Err(Error::Timeout);
573            }
574            if self.workers_idle() {
575                let blocked: Vec<NodeId> = self
576                    .blocked_flush_nodes
577                    .lock()
578                    .expect("blocked flush lock poisoned")
579                    .iter()
580                    .copied()
581                    .collect();
582                if !blocked.is_empty() && self.retry_idle_progress(deadline)? {
583                    continue;
584                }
585                if self.is_idle() {
586                    break;
587                }
588                if !blocked.is_empty() {
589                    let details = self.backpressure_stall_details(&blocked);
590                    return Err(Error::Kernel(format!(
591                        "wait_until_idle: internal backpressure cannot make progress; blocked queues: [{}]",
592                        details.join("; ")
593                    )));
594                }
595            }
596            match self.remaining(deadline) {
597                Some(d) => {
598                    self.wait_activity_since(before, d);
599                }
600                None => return Err(Error::Timeout),
601            }
602        }
603        match self.shared.first_error() {
604            Some(e) => Err(e),
605            None => Ok(()),
606        }
607    }
608}
609
610impl Drop for GraphInner {
611    /// 兜底关流:图被直接丢弃(没走 wait_done)时,已 open 的算子仍必须收到 Close,
612    /// 否则算子里申请的资源(文件、连接、GPU 上下文)不会被释放。
613    fn drop(&mut self) {
614        // 先关停线程池并 join:否则工作线程可能触碰正在析构的节点。
615        self.shutdown_executors();
616
617        for n in 0..self.nodes.len() {
618            let need_close = {
619                let s = self.nodes[n].sched.lock().expect("scheduler lock poisoned");
620                s.opened && !s.close_started
621            };
622            if !need_close {
623                continue;
624            }
625            {
626                // 安全性:线程池已 join,此刻只有 drop 这一条执行流,独占成立。用槽 0。
627                let ctx = unsafe { self.nodes[n].ctx_slot(0) };
628                ctx.reset();
629                ctx.close_reason = self.shared.close_reason();
630                ctx.input_ts = Timestamp::done();
631            }
632            let rc = self.call_kernel(n, 0, KernelPhase::Close);
633            if rc != 0 {
634                runtime::log_warn(&format!(
635                    "node `{}`: close returned {rc} during graph destruction (ignored)",
636                    self.nodes[n].name
637                ));
638            }
639            unsafe { self.nodes[n].ctx_slot(0) }.clear_inputs();
640            self.nodes[n]
641                .sched
642                .lock()
643                .expect("scheduler lock poisoned")
644                .closed = true;
645        }
646    }
647}