1use 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
222pub enum PollerOverflow {
223 Block,
224 DropOldest,
225 DropNewest,
226 Latest,
227}
228
229#[derive(Debug, Clone, Copy, PartialEq, Eq)]
231pub struct PollerOptions {
232 pub capacity: usize,
233 pub overflow: PollerOverflow,
234 pub block_timeout: Option<std::time::Duration>,
237}
238
239impl PollerOptions {
240 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 pub fn with_block_timeout(mut self, timeout: Option<std::time::Duration>) -> Self {
251 self.block_timeout = timeout;
252 self
253 }
254}
255
256pub struct Poller {
258 graph: Arc<GraphInner>,
259 inner: Arc<PollerInner>,
260}
261
262#[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 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 pub fn next_event(&self) -> Option<OutputEvent> {
357 self.next().map(Self::classify_event)
358 }
359
360 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 pub fn try_next_event(&self) -> Option<OutputEvent> {
368 self.try_next().map(Self::classify_event)
369 }
370
371 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 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 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 return Ok(self.inner.pop(&self.graph));
405 }
406 if self.graph.shared.is_cancelled() {
407 return Err(Error::Cancelled);
408 }
409 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 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 return Ok(self.inner.pop(&self.graph));
440 }
441 Ok(None)
442 }
443
444 pub fn is_closed(&self) -> bool {
451 self.inner.closed.load(Ordering::SeqCst)
452 }
453
454 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}