Skip to main content

lmflow/graph/
poller.rs

1//! 拉模式输出 Poller:有界队列、溢出策略、宿主等待与背压统计。
2//!
3//! Poller 会在图的派发线程上接收输出,因此其 `Block` 策略与图调度存在直接交互;
4//! 相关实现集中在这里,`mod.rs` 只保留边派发和生命周期核心。
5
6use std::collections::VecDeque;
7use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
8use std::sync::{Arc, Mutex};
9
10use crate::packet::Packet;
11use crate::runtime;
12use crate::status::{Error, Result};
13use crate::timestamp::Timestamp;
14
15use super::{BackpressureStats, EdgeId, Graph, GraphInner, State};
16
17pub(super) struct PollerInner {
18    pub(super) edge: EdgeId,
19    pub(super) edge_name: String,
20    pub(super) queue: Mutex<VecDeque<Packet>>,
21    pub(super) closed: AtomicBool,
22    pub(super) capacity: Option<usize>,
23    pub(super) overflow: PollerOverflow,
24    /// `Block` 等宿主腾位的上界;`None` = 无上界。
25    pub(super) block_timeout: Option<std::time::Duration>,
26    pub(super) dropped: AtomicU64,
27    pub(super) block_backpressure: BackpressureStats,
28    pub(super) active: AtomicBool,
29    pub(super) observe_timestamp_bounds: bool,
30}
31
32impl PollerInner {
33    pub(super) fn push(&self, graph: &GraphInner, packet: Packet) -> bool {
34        let mut deadline: Option<std::time::Instant> = None;
35        let mut blocked = false;
36        loop {
37            if !self.active.load(Ordering::SeqCst) {
38                if blocked {
39                    self.finish_block(graph, false);
40                }
41                return false;
42            }
43            let before = graph.activity_gen();
44            let mut queue = self.queue.lock().expect("poller lock poisoned");
45            if !self.active.load(Ordering::SeqCst) {
46                drop(queue);
47                if blocked {
48                    self.finish_block(graph, false);
49                }
50                return false;
51            }
52            let full = self
53                .capacity
54                .is_some_and(|capacity| queue.len() >= capacity);
55            if !full {
56                graph.shared.on_enqueue(packet.byte_size());
57                queue.push_back(packet);
58                drop(queue);
59                if blocked {
60                    self.finish_block(graph, true);
61                }
62                return true;
63            }
64            match self.overflow {
65                PollerOverflow::Block => {
66                    let queued = queue.len();
67                    drop(queue);
68                    if !blocked {
69                        blocked = true;
70                        self.begin_block(graph, queued);
71                    }
72                    if !self.active.load(Ordering::SeqCst)
73                        || graph.shared.is_cancelled()
74                        || graph.shared.has_error()
75                    {
76                        self.finish_block(graph, false);
77                        return false;
78                    }
79                    if let Some(limit) = self.block_timeout {
80                        if deadline.is_none() {
81                            deadline = Some(std::time::Instant::now() + limit);
82                        }
83                        if let Some(at) = deadline {
84                            if std::time::Instant::now() >= at {
85                                self.finish_block(graph, false);
86                                graph.shared.record_error(Error::Kernel(format!(
87                                    "poller on output port `{}`: blocked for {:?} waiting for the \
88                                     host to free a slot (capacity {}, queued {}). `PollerOverflow::Block` \
89                                     requires a host that drains the poller *concurrently*; a host \
90                                     that sends/waits first and drains afterwards will deadlock, \
91                                     because the producer parks inside dispatch and never reaches \
92                                     `poller.next()`. Use `Latest`/`DropOldest`/`DropNewest`, raise \
93                                     the capacity, drain from another thread, or set \
94                                     `PollerOptions::with_block_timeout(None)` if you are certain a \
95                                     concurrent drainer exists.",
96                                    self.edge_name,
97                                    limit,
98                                    self.capacity.unwrap_or(0),
99                                    queued,
100                                )));
101                                return false;
102                            }
103                        }
104                    }
105                    graph.wait_activity_since(before, std::time::Duration::from_millis(100));
106                }
107                PollerOverflow::DropOldest => {
108                    let dropped = if let Some(old) = queue.pop_front() {
109                        graph.shared.on_dequeue(old.byte_size());
110                        1
111                    } else {
112                        0
113                    };
114                    graph.shared.on_enqueue(packet.byte_size());
115                    queue.push_back(packet);
116                    let queued = queue.len();
117                    drop(queue);
118                    if dropped != 0 {
119                        let total = self.record_dropped(dropped);
120                        self.log_dropped(total, queued);
121                    }
122                    return true;
123                }
124                PollerOverflow::DropNewest => {
125                    let queued = queue.len();
126                    drop(queue);
127                    let total = self.record_dropped(1);
128                    self.log_dropped(total, queued);
129                    return false;
130                }
131                PollerOverflow::Latest => {
132                    let dropped = queue.len() as u64;
133                    while let Some(old) = queue.pop_front() {
134                        graph.shared.on_dequeue(old.byte_size());
135                    }
136                    graph.shared.on_enqueue(packet.byte_size());
137                    queue.push_back(packet);
138                    let queued = queue.len();
139                    drop(queue);
140                    if dropped != 0 {
141                        let total = self.record_dropped(dropped);
142                        self.log_dropped(total, queued);
143                    }
144                    return true;
145                }
146            }
147        }
148    }
149
150    fn pop(&self, graph: &GraphInner) -> Option<Packet> {
151        let packet = self.queue.lock().expect("poller lock poisoned").pop_front();
152        if let Some(packet) = &packet {
153            graph.shared.on_dequeue(packet.byte_size());
154            graph.notify_activity();
155        }
156        packet
157    }
158
159    fn is_empty(&self) -> bool {
160        self.queue.lock().expect("poller lock poisoned").is_empty()
161    }
162
163    pub(super) fn clear(&self, graph: &GraphInner) {
164        let mut queue = self.queue.lock().expect("poller lock poisoned");
165        while let Some(packet) = queue.pop_front() {
166            graph.shared.on_dequeue(packet.byte_size());
167        }
168        drop(queue);
169        graph.notify_activity();
170    }
171
172    fn begin_block(&self, graph: &GraphInner, queued: usize) {
173        let Some(event) = self.block_backpressure.enter(graph.epoch_us()) else {
174            return;
175        };
176        if event.is_power_of_two() {
177            runtime::log_warn(&format!(
178                "poller backpressure #{event}: output `{}` blocked (policy=Block, capacity={}, \
179                 queued={queued}, timeout={}); waiting for the host to drain concurrently",
180                self.edge_name,
181                self.capacity.unwrap_or(0),
182                self.block_timeout.map_or_else(
183                    || "none".to_string(),
184                    |timeout| format!("{}ms", timeout.as_millis())
185                ),
186            ));
187        }
188    }
189
190    fn finish_block(&self, graph: &GraphInner, log_recovery: bool) {
191        let Some((event, elapsed)) = self.block_backpressure.leave(graph.epoch_us()) else {
192            return;
193        };
194        if log_recovery && event.is_power_of_two() {
195            runtime::log_info(&format!(
196                "poller backpressure #{event} cleared: output `{}` resumed after {elapsed}us",
197                self.edge_name,
198            ));
199        }
200    }
201
202    fn record_dropped(&self, count: u64) -> u64 {
203        let before = self.dropped.fetch_add(count, Ordering::Relaxed);
204        before + count
205    }
206
207    fn log_dropped(&self, total: u64, queued: usize) {
208        if total == 1 || total.is_power_of_two() {
209            runtime::log_warn(&format!(
210                "poller overflow: output `{}` policy={:?} capacity={} queued={queued} \
211                 dropped_total={total}",
212                self.edge_name,
213                self.overflow,
214                self.capacity.unwrap_or(0),
215            ));
216        }
217    }
218}
219
220/// A bounded poller's behavior when its queue reaches capacity.
221#[derive(Debug, Clone, Copy, PartialEq, Eq)]
222pub enum PollerOverflow {
223    Block,
224    DropOldest,
225    DropNewest,
226    Latest,
227}
228
229/// Options for [`Graph::add_poller_with_options`].
230#[derive(Debug, Clone, Copy, PartialEq, Eq)]
231pub struct PollerOptions {
232    pub capacity: usize,
233    pub overflow: PollerOverflow,
234    /// `Block` 溢出策略下,等宿主腾出一个槽位的上界。`None` 表示无上界,
235    /// 仅适用于宿主确定会在另一个线程持续排水的场景。
236    pub block_timeout: Option<std::time::Duration>,
237}
238
239impl PollerOptions {
240    /// `Block` 默认带 5 秒上界;有损策略下该字段无意义。
241    pub fn new(capacity: usize, overflow: PollerOverflow) -> Self {
242        Self {
243            capacity,
244            overflow,
245            block_timeout: Some(std::time::Duration::from_secs(5)),
246        }
247    }
248
249    /// 覆盖 `Block` 的等待上界。
250    pub fn with_block_timeout(mut self, timeout: Option<std::time::Duration>) -> Self {
251        self.block_timeout = timeout;
252        self
253    }
254}
255
256/// 拉模式输出句柄。
257pub struct Poller {
258    graph: Arc<GraphInner>,
259    inner: Arc<PollerInner>,
260}
261
262/// A typed event from an output poller that observes timestamp bounds.
263#[derive(Clone, Debug)]
264pub enum OutputEvent {
265    Packet(Packet),
266    TimestampBound(Timestamp),
267    Done,
268}
269
270impl Graph {
271    pub fn add_poller(&self, port: &str) -> Result<Poller> {
272        self.add_poller_inner(port, None, false)
273    }
274
275    /// Add an unbounded poller that also receives timestamp-bound events as empty packets.
276    pub fn add_poller_with_timestamp_bounds(&self, port: &str) -> Result<Poller> {
277        self.add_poller_inner(port, None, true)
278    }
279
280    pub fn add_poller_with_options(&self, port: &str, options: PollerOptions) -> Result<Poller> {
281        if options.capacity == 0 {
282            return Err(Error::InvalidArg(
283                "poller capacity must be at least 1".into(),
284            ));
285        }
286        if options.overflow == PollerOverflow::Latest && options.capacity != 1 {
287            return Err(Error::InvalidArg(
288                "poller overflow=latest requires capacity 1".into(),
289            ));
290        }
291        self.add_poller_inner(port, Some(options), false)
292    }
293
294    fn add_poller_inner(
295        &self,
296        port: &str,
297        options: Option<PollerOptions>,
298        observe_timestamp_bounds: bool,
299    ) -> Result<Poller> {
300        let state = self.state();
301        if state != State::Initialized {
302            return Err(Error::State(format!(
303                "add_poller must be called before start (current state {state:?})"
304            )));
305        }
306        let edge =
307            *self.inner.output_by_name.get(port).ok_or_else(|| {
308                Error::NotFound(format!("graph output port `{port}` does not exist"))
309            })?;
310        let inner = Arc::new(PollerInner {
311            edge,
312            edge_name: port.to_string(),
313            queue: Mutex::new(VecDeque::new()),
314            closed: AtomicBool::new(false),
315            capacity: options.map(|options| options.capacity),
316            overflow: options.map_or(PollerOverflow::Block, |options| options.overflow),
317            block_timeout: options.and_then(|options| options.block_timeout),
318            dropped: AtomicU64::new(0),
319            block_backpressure: BackpressureStats::default(),
320            active: AtomicBool::new(true),
321            observe_timestamp_bounds,
322        });
323        if observe_timestamp_bounds {
324            self.inner.edges[edge]
325                .has_timestamp_bound_subscriber
326                .store(true, Ordering::Relaxed);
327        }
328        self.inner.edges[edge]
329            .pollers
330            .lock()
331            .expect("poller list lock poisoned")
332            .push(inner.clone());
333        Ok(Poller {
334            graph: self.inner.clone(),
335            inner,
336        })
337    }
338}
339
340impl Poller {
341    fn classify_event(packet: Packet) -> OutputEvent {
342        if !packet.is_empty() {
343            return OutputEvent::Packet(packet);
344        }
345        if packet.timestamp() == Timestamp::done() {
346            OutputEvent::Done
347        } else {
348            OutputEvent::TimestampBound(packet.timestamp())
349        }
350    }
351
352    /// Get the next output as a typed packet, timestamp-bound, or done event.
353    ///
354    /// Use a poller created by [`Graph::add_poller_with_timestamp_bounds`] to receive all three
355    /// variants. A normal poller only yields [`OutputEvent::Packet`] and ends with `None`.
356    pub fn next_event(&self) -> Option<OutputEvent> {
357        self.next().map(Self::classify_event)
358    }
359
360    /// Timed form of [`next_event`](Self::next_event).
361    pub fn next_event_timeout(&self, timeout: std::time::Duration) -> Result<Option<OutputEvent>> {
362        self.next_timeout(timeout)
363            .map(|packet| packet.map(Self::classify_event))
364    }
365
366    /// Non-blocking form of [`next_event`](Self::next_event).
367    pub fn try_next_event(&self) -> Option<OutputEvent> {
368        self.try_next().map(Self::classify_event)
369    }
370
371    /// 取下一个包。等待期间会抽取并执行主线程任务。
372    pub fn next(&self) -> Option<Packet> {
373        self.next_deadline(None).ok().flatten()
374    }
375
376    pub fn next_result(&self) -> Result<Option<Packet>> {
377        self.next_deadline(None)
378    }
379
380    /// 带超时:`Ok(Some)` 取到,`Ok(None)` 图已结束,`Err(Timeout)` 超时。
381    pub fn next_timeout(&self, timeout: std::time::Duration) -> Result<Option<Packet>> {
382        self.next_deadline(Some(std::time::Instant::now() + timeout))
383    }
384
385    fn next_deadline(&self, deadline: Option<std::time::Instant>) -> Result<Option<Packet>> {
386        loop {
387            // Capture before checking the queue, closure, or delegated work so a
388            // concurrent producer cannot notify just before we begin waiting.
389            let before = self.graph.activity_gen_pub();
390            if let Some(packet) = self.inner.pop(&self.graph) {
391                return Ok(Some(packet));
392            }
393            if let Some(error) = self.graph.shared.first_error() {
394                return Err(error);
395            }
396            if self.inner.closed.load(Ordering::SeqCst) {
397                // `closed` is a latch meaning "no more packets will be *enqueued*" — NOT
398                // "the queue is empty". A producer can enqueue the final packet(s) and then
399                // close the edge in the window between the `pop` at the top of this loop and
400                // this check (e.g. this thread is preempted right here on a busy machine).
401                // Drain with a fresh `pop` so end-of-stream
402                // never abandons a packet that landed in that window. Returning `None` here
403                // unconditionally silently drops those packets.
404                return Ok(self.inner.pop(&self.graph));
405            }
406            if self.graph.shared.is_cancelled() {
407                return Err(Error::Cancelled);
408            }
409            // Do not let a continuously ready delegated executor bypass timeout.
410            if self.graph.remaining_for_poller(deadline).is_none() {
411                return Err(Error::Timeout);
412            }
413            if self.graph.pump_step() {
414                continue;
415            }
416            match self.graph.remaining_for_poller(deadline) {
417                Some(duration) => self.graph.wait_activity_since_pub(before, duration),
418                None => return Err(Error::Timeout),
419            }
420        }
421    }
422
423    /// 非阻塞:仅看现有队列。
424    pub fn try_next(&self) -> Option<Packet> {
425        self.inner.pop(&self.graph)
426    }
427
428    pub fn try_next_result(&self) -> Result<Option<Packet>> {
429        if let Some(packet) = self.inner.pop(&self.graph) {
430            return Ok(Some(packet));
431        }
432        if let Some(error) = self.graph.shared.first_error() {
433            return Err(error);
434        }
435        if self.inner.closed.load(Ordering::SeqCst) {
436            // `closed` latches "no more enqueues", not "queue empty": a producer may enqueue
437            // and close between the `pop` above and this load. Re-pop so end-of-stream never
438            // drops that packet (see `next_deadline`).
439            return Ok(self.inner.pop(&self.graph));
440        }
441        Ok(None)
442    }
443
444    /// 边是否已关闭 —— 即「**不会再有入队**」,**不等于**队列已排空。
445    ///
446    /// 单独用它判流结束会丢包:生产者可以在你读到 `false` 之后入队并关边。判流结束请用
447    /// `next()` / `next_result()` / `try_next_result()` 返回的 `Ok(None)`(它们已经包含了
448    /// 最终排空),或者显式写成 `is_closed() && is_empty()` —— 顺序不能反,先观察到 closed
449    /// 为真才能保证随后的 `is_empty()` 是权威的。
450    pub fn is_closed(&self) -> bool {
451        self.inner.closed.load(Ordering::SeqCst)
452    }
453
454    /// 队列当前是否为空。**不代表流结束** —— 见 [`is_closed`](Self::is_closed)。
455    pub fn is_empty(&self) -> bool {
456        self.inner.is_empty()
457    }
458
459    pub fn dropped_count(&self) -> u64 {
460        self.inner.dropped.load(Ordering::Relaxed)
461    }
462
463    pub fn backpressure_stats(&self) -> PollerBackpressureStatsSnapshot {
464        let stats = self
465            .inner
466            .block_backpressure
467            .snapshot(self.graph.epoch_us());
468        PollerBackpressureStatsSnapshot {
469            port_name: self.inner.edge_name.clone(),
470            capacity: self.inner.capacity,
471            overflow: self.inner.overflow,
472            queued_packets: self.inner.queue.lock().expect("poller lock poisoned").len(),
473            dropped_packets: self.inner.dropped.load(Ordering::Relaxed),
474            blocked: stats.blocked,
475            active_waiters: stats.active_waiters,
476            blocked_for_us: stats.blocked_for_us,
477            block_events: stats.block_events,
478            total_blocked_us: stats.total_blocked_us,
479        }
480    }
481}
482
483impl Drop for Poller {
484    fn drop(&mut self) {
485        self.inner.active.store(false, Ordering::SeqCst);
486        let edge = &self.graph.edges[self.inner.edge];
487        edge.pollers
488            .lock()
489            .expect("poller list lock poisoned")
490            .retain(|poller| !Arc::ptr_eq(poller, &self.inner));
491        self.inner.clear(&self.graph);
492        self.graph.notify_activity();
493    }
494}
495
496impl std::fmt::Debug for Poller {
497    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
498        write!(
499            formatter,
500            "Poller{{port:`{}`, pending:{}}}",
501            self.graph.edges[self.inner.edge].name,
502            self.inner
503                .queue
504                .lock()
505                .map(|queue| queue.len())
506                .unwrap_or(0)
507        )
508    }
509}
510
511#[derive(Debug, Clone)]
512pub struct PollerBackpressureStatsSnapshot {
513    pub port_name: String,
514    pub capacity: Option<usize>,
515    pub overflow: PollerOverflow,
516    pub queued_packets: usize,
517    pub dropped_packets: u64,
518    pub blocked: bool,
519    pub active_waiters: usize,
520    pub blocked_for_us: u64,
521    pub block_events: u64,
522    pub total_blocked_us: u64,
523}