Skip to main content

cumulus3d_cli/
pipeline.rs

1/*
2 * pipeline.rs
3 *
4 * Copyright (c) 2026 ParkSangWoo
5 *
6 * Author(s):
7 *
8 *      ParkSangWoo <dev.parksangwoo@gmail.com>
9 *
10 *
11 * This file is part of cumulus3d.
12 *
13 * Licensed under the Apache License, Version 2.0 (the "License");
14 * you may not use this file except in compliance with the License.
15 * You may obtain a copy of the License at
16 *
17 *      http://www.apache.org/licenses/LICENSE-2.0
18 *
19 * Unless required by applicable law or agreed to in writing, software
20 * distributed under the License is distributed on an "AS IS" BASIS,
21 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22 * See the License for the specific language governing permissions and
23 * limitations under the License.
24 *
25 * SPDX-License-Identifier: Apache-2.0
26 */
27
28//! Closure hook pipeline (`Pipeline`): hook registration, queues, policies, panic isolation, channels.
29//!
30//! It wraps a reducer (`Reducer`), pushes inputs into it, and for the events the reducer emits:
31//! 1) copies all of them to subscription channels (`subscribe`),
32//! 2) passes them to the registered hooks (`on`, `on_any`, ...).
33//!
34//! How hooks run
35//! - Asynchronous (default): hooks run on "lane" worker threads. There is one lane per event kind
36//!   and one lane for `on_any` hooks. The processing loop (`push`) does not wait for hooks.
37//!   Arrival order is preserved within a lane, so order within a kind is always preserved.
38//!   (The `on_any` lane preserves the overall order.) The execution order between different kinds is not guaranteed.
39//! - Synchronous (`sync(true)`): hooks run immediately on the thread that called `push`, in event order
40//!   (for each event, hooks of its kind first, then `on_any` hooks). For tests and file output.
41//!
42//! The queue policy (`policy`) is set per kind. The default is to keep everything (`QueuePolicy::KeepAll`).
43//! For kinds where only the latest matters, such as display updates, earlier unprocessed events can be dropped.
44//!
45//! If a hook panics, it is caught with `catch_unwind`, an `Event::Error` is sent to subscribers, and the pipeline keeps running.
46//! (That `Error` is not delivered to hooks, to prevent an error hook from panicking again and looping forever.)
47
48use crate::events::{Command, Event, EventKind, Frame, Meta, ZoneRange};
49use cumulus3d_core::io::ply::PointCloud;
50use std::collections::{HashMap, VecDeque};
51use std::panic::{catch_unwind, AssertUnwindSafe};
52use std::sync::atomic::{AtomicUsize, Ordering};
53use std::sync::mpsc::{channel, Receiver, Sender};
54use std::sync::{Arc, Condvar, Mutex};
55use std::thread::JoinHandle;
56use std::time::SystemTime;
57
58/// The reducer driven by the pipeline.
59///
60/// The session state of the session module implements this trait. If the session has the immutable-value form
61/// (`step(&Session, FrameSet) -> (Session, Vec<Event>)`), implement it as a thin adapter that holds the current session
62/// and replaces it with the new session in `step`.
63pub trait Reducer {
64    /// Input pushed in one go (e.g. the frame bundle of one position).
65    type Input;
66    /// Process one input and return the resulting events in order.
67    fn step(&mut self, input: Self::Input) -> Vec<Event>;
68    /// Events to emit as soon as the input is received, before `step` processes it (e.g. decoded frames). Default: none.
69    fn prelude(&mut self, _input: &Self::Input) -> Vec<Event> {
70        Vec::new()
71    }
72    /// Collect finished background work (refinement etc.). Does not wait.
73    fn poll(&mut self) -> Vec<Event>;
74    /// Process a command.
75    fn command(&mut self, c: Command) -> Vec<Event>;
76    /// Wait until all remaining background work finishes and collect it.
77    /// The default implementation is a single `poll`. Reducers with background work must override it.
78    fn finish(&mut self) -> Vec<Event> {
79        self.poll()
80    }
81}
82
83/// Per-kind queue policy.
84#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
85pub enum QueuePolicy {
86    /// Keep everything (default).
87    #[default]
88    KeepAll,
89    /// Drop earlier unprocessed events with the same key (zone index / snapshot index).
90    /// Kinds without a key behave like `LatestOnly`.
91    LatestPerKey,
92    /// Drop all earlier unprocessed events of the same kind.
93    LatestOnly,
94}
95
96/// Summary returned by `finish`.
97#[derive(Clone, Debug, Default)]
98pub struct Summary {
99    /// Number of events emitted by the reducer.
100    pub events: usize,
101    /// Number of events per kind.
102    pub by_kind: HashMap<EventKind, usize>,
103    /// Number of hook calls executed (including panics).
104    pub hook_calls: usize,
105    /// Number of hook calls that panicked.
106    pub hook_panics: usize,
107    /// Number of events dropped by the queue policy (counted per lane).
108    pub superseded: usize,
109}
110
111type Hook = Box<dyn FnMut(&Event) + Send + 'static>;
112type HookList = Arc<Mutex<Vec<Hook>>>;
113
114/// Lane key: `Some(kind)` is a per-kind lane, `None` is the `on_any` lane.
115type LaneKey = Option<EventKind>;
116
117/// State shared by the worker threads and the processing loop.
118struct Shared {
119    subscribers: Mutex<Vec<Sender<Event>>>,
120    hook_calls: AtomicUsize,
121    hook_panics: AtomicUsize,
122    superseded: AtomicUsize,
123}
124
125impl Shared {
126    fn broadcast(&self, ev: &Event) {
127        let mut subs = self.subscribers.lock().unwrap_or_else(|p| p.into_inner());
128        subs.retain(|tx| tx.send(ev.clone()).is_ok());
129    }
130
131    /// Run the hook list for one event and isolate panics.
132    fn run_hooks(&self, hooks: &HookList, ev: &Event) {
133        let mut hooks = hooks.lock().unwrap_or_else(|p| p.into_inner());
134        for hook in hooks.iter_mut() {
135            self.hook_calls.fetch_add(1, Ordering::Relaxed);
136            let r = catch_unwind(AssertUnwindSafe(|| hook(ev)));
137            if let Err(payload) = r {
138                self.hook_panics.fetch_add(1, Ordering::Relaxed);
139                let what = if let Some(s) = payload.downcast_ref::<&str>() {
140                    (*s).to_string()
141                } else if let Some(s) = payload.downcast_ref::<String>() {
142                    s.clone()
143                } else {
144                    "알 수 없는 패닉".to_string()
145                };
146                let err = Event::Error {
147                    meta: Meta { version: ev.meta().version, at: SystemTime::now() },
148                    message: format!("{:?} 훅 패닉: {what}", ev.kind()),
149                };
150                self.broadcast(&err);
151            }
152        }
153    }
154}
155
156struct LaneQueue {
157    items: VecDeque<Event>,
158    busy: bool,
159    closed: bool,
160}
161
162struct LaneState {
163    q: Mutex<LaneQueue>,
164    cv: Condvar,
165}
166
167struct Lane {
168    hooks: HookList,
169    state: Arc<LaneState>,
170    worker: Option<JoinHandle<()>>,
171}
172
173impl Lane {
174    fn new() -> Self {
175        Lane {
176            hooks: Arc::new(Mutex::new(Vec::new())),
177            state: Arc::new(LaneState {
178                q: Mutex::new(LaneQueue { items: VecDeque::new(), busy: false, closed: false }),
179                cv: Condvar::new(),
180            }),
181            worker: None,
182        }
183    }
184
185    fn ensure_worker(&mut self, name: String, shared: &Arc<Shared>) {
186        if self.worker.is_some() {
187            return;
188        }
189        let state = Arc::clone(&self.state);
190        let hooks = Arc::clone(&self.hooks);
191        let shared = Arc::clone(shared);
192        let h = std::thread::Builder::new()
193            .name(name)
194            .spawn(move || loop {
195                let ev = {
196                    let mut q = state.q.lock().unwrap_or_else(|p| p.into_inner());
197                    loop {
198                        if let Some(ev) = q.items.pop_front() {
199                            q.busy = true;
200                            break Some(ev);
201                        }
202                        if q.closed {
203                            break None;
204                        }
205                        q = state.cv.wait(q).unwrap_or_else(|p| p.into_inner());
206                    }
207                };
208                let Some(ev) = ev else { return };
209                shared.run_hooks(&hooks, &ev);
210                let mut q = state.q.lock().unwrap_or_else(|p| p.into_inner());
211                q.busy = false;
212                state.cv.notify_all();
213            })
214            .expect("훅 워커 스레드 생성 실패");
215        self.worker = Some(h);
216    }
217
218    /// Drop earlier events according to the policy and enqueue. Returns the number dropped.
219    fn enqueue(&self, ev: Event, policy: QueuePolicy) -> usize {
220        let mut q = self.state.q.lock().unwrap_or_else(|p| p.into_inner());
221        let before = q.items.len();
222        match policy {
223            QueuePolicy::KeepAll => {}
224            QueuePolicy::LatestOnly => {
225                let k = ev.kind();
226                q.items.retain(|e| e.kind() != k);
227            }
228            QueuePolicy::LatestPerKey => {
229                let k = (ev.kind(), supersede_key(&ev));
230                q.items.retain(|e| (e.kind(), supersede_key(e)) != k);
231            }
232        }
233        let dropped = before - q.items.len();
234        q.items.push_back(ev);
235        self.state.cv.notify_all();
236        dropped
237    }
238
239    /// Wait until the queue is empty and no hook is running.
240    fn wait_idle(&self) {
241        if self.worker.is_none() {
242            return;
243        }
244        let mut q = self.state.q.lock().unwrap_or_else(|p| p.into_inner());
245        while !q.items.is_empty() || q.busy {
246            q = self.state.cv.wait(q).unwrap_or_else(|p| p.into_inner());
247        }
248    }
249
250    fn shutdown(&mut self) {
251        {
252            let mut q = self.state.q.lock().unwrap_or_else(|p| p.into_inner());
253            q.closed = true;
254            self.state.cv.notify_all();
255        }
256        if let Some(h) = self.worker.take() {
257            let _ = h.join();
258        }
259    }
260}
261
262/// Same-key test: zone events use the zone index, snapshots the index. Others have no key.
263fn supersede_key(ev: &Event) -> Option<usize> {
264    match ev {
265        Event::ZoneArrived { range, .. }
266        | Event::ZonePreview { range, .. }
267        | Event::ZoneRefinedPose { range, .. }
268        | Event::ZoneRefined { range, .. } => Some(range.zone),
269        Event::BaseAdopted { zone, .. }
270        | Event::Reanchored { zone, .. }
271        | Event::RefineStarted { zone, .. }
272        | Event::ZoneAdjusted { zone, .. }
273        | Event::ZoneInvalidated { zone, .. } => Some(*zone),
274        Event::Snapshot { index, .. } => Some(*index),
275        Event::PositionDone { position, .. } | Event::FrameIngested { position, .. } => Some(*position),
276        _ => None,
277    }
278}
279
280/// Lane set. On drop, it closes the workers, empties the remaining queues and joins.
281struct Lanes {
282    map: HashMap<LaneKey, Lane>,
283}
284
285impl Lanes {
286    fn shutdown(&mut self) {
287        for lane in self.map.values_mut() {
288            lane.shutdown();
289        }
290    }
291}
292
293impl Drop for Lanes {
294    fn drop(&mut self) {
295        self.shutdown();
296    }
297}
298
299/// Derives extra events from an event (see [`Pipeline::derive`]).
300type Deriver = Box<dyn FnMut(&Event) -> Vec<Event> + Send + 'static>;
301
302/// Event driver that wraps a reducer.
303pub struct Pipeline<R: Reducer> {
304    reducer: R,
305    derivers: Vec<Deriver>,
306    lanes: Lanes,
307    policies: HashMap<EventKind, QueuePolicy>,
308    shared: Arc<Shared>,
309    sync: bool,
310    events: usize,
311    by_kind: HashMap<EventKind, usize>,
312}
313
314impl<R: Reducer> Pipeline<R> {
315    /// Create it with asynchronous hook execution and `KeepAll` for every kind.
316    pub fn new(reducer: R) -> Self {
317        Pipeline {
318            reducer,
319            derivers: Vec::new(),
320            lanes: Lanes { map: HashMap::new() },
321            policies: HashMap::new(),
322            shared: Arc::new(Shared {
323                subscribers: Mutex::new(Vec::new()),
324                hook_calls: AtomicUsize::new(0),
325                hook_panics: AtomicUsize::new(0),
326                superseded: AtomicUsize::new(0),
327            }),
328            sync: false,
329            events: 0,
330            by_kind: HashMap::new(),
331        }
332    }
333
334    /// Set synchronous mode. Decide before the first `push`. Switching to synchronous first drains the asynchronous queues accumulated so
335    /// far.
336    pub fn sync(mut self, on: bool) -> Self {
337        if on && !self.sync {
338            self.flush();
339        }
340        self.sync = on;
341        self
342    }
343
344    /// Set the per-kind queue policy (only meaningful in asynchronous mode).
345    pub fn policy(mut self, kind: EventKind, p: QueuePolicy) -> Self {
346        self.policies.insert(kind, p);
347        self
348    }
349
350    /// Register a hook for a specific kind. Hooks of the same kind run in registration order.
351    pub fn on<F>(mut self, kind: EventKind, f: F) -> Self
352    where
353        F: FnMut(&Event) + Send + 'static,
354    {
355        self.add_hook(Some(kind), Box::new(f));
356        self
357    }
358
359    /// Register a hook for all events (preserves the overall order).
360    pub fn on_any<F>(mut self, f: F) -> Self
361    where
362        F: FnMut(&Event) + Send + 'static,
363    {
364        self.add_hook(None, Box::new(f));
365        self
366    }
367
368    /// Frame hook: receives each decoded input frame, one camera at a time, before the position is processed.
369    /// The session emits frames only if `SessionConfig::decode_frames` is on.
370    pub fn on_frame<F>(self, mut f: F) -> Self
371    where
372        F: FnMut(&Arc<Frame>) + Send + 'static,
373    {
374        self.on(EventKind::FrameDecoded, move |e| {
375            if let Event::FrameDecoded { frame, .. } = e {
376                f(frame)
377            }
378        })
379    }
380
381    /// Zone preview point-cloud hook.
382    pub fn on_zone_preview<F>(self, mut f: F) -> Self
383    where
384        F: FnMut(ZoneRange, &Arc<PointCloud>) + Send + 'static,
385    {
386        self.on(EventKind::ZonePreview, move |e| {
387            if let Event::ZonePreview { range, cloud, .. } = e {
388                f(*range, cloud)
389            }
390        })
391    }
392
393    /// Zone refined point-cloud hook.
394    pub fn on_zone_refined<F>(self, mut f: F) -> Self
395    where
396        F: FnMut(ZoneRange, &Arc<PointCloud>) + Send + 'static,
397    {
398        self.on(EventKind::ZoneRefined, move |e| {
399            if let Event::ZoneRefined { range, cloud, .. } = e {
400                f(*range, cloud)
401            }
402        })
403    }
404
405    /// Snapshot hook (index, composite point cloud).
406    pub fn on_snapshot<F>(self, mut f: F) -> Self
407    where
408        F: FnMut(usize, &Arc<PointCloud>) + Send + 'static,
409    {
410        self.on(EventKind::Snapshot, move |e| {
411            if let Event::Snapshot { index, cloud, .. } = e {
412                f(*index, cloud)
413            }
414        })
415    }
416
417    /// Position-done hook (position, registered count, expected count).
418    pub fn on_position_done<F>(self, mut f: F) -> Self
419    where
420        F: FnMut(usize, usize, usize) + Send + 'static,
421    {
422        self.on(EventKind::PositionDone, move |e| {
423            if let Event::PositionDone { position, registered, expected, .. } = e {
424                f(*position, *registered, *expected)
425            }
426        })
427    }
428
429    /// Warning/error message hook (those emitted by the reducer; hook panic errors go only to the subscription channel).
430    pub fn on_message<F>(self, f: F) -> Self
431    where
432        F: FnMut(EventKind, &str) + Send + 'static,
433    {
434        let f = Arc::new(Mutex::new(f));
435        let g = Arc::clone(&f);
436        self.on(EventKind::Warning, move |e| {
437            if let Event::Warning { message, .. } = e {
438                (f.lock().unwrap_or_else(|p| p.into_inner()))(EventKind::Warning, message)
439            }
440        })
441        .on(EventKind::Error, move |e| {
442            if let Event::Error { message, .. } = e {
443                (g.lock().unwrap_or_else(|p| p.into_inner()))(EventKind::Error, message)
444            }
445        })
446    }
447
448    /// Registers a deriver: after each event is delivered, `f` may return further events (e.g. [`Event::DisplayDelta`]), which
449    /// are delivered right behind it, in order, to subscribers and hooks like reducer events (and counted in the summary).
450    /// It runs on the thread that pushes (not on a lane), so the derived event follows its cause deterministically; keep it
451    /// cheap or accept that it adds to the push time. A panic in `f` is caught and reported as `Event::Error` to subscribers.
452    /// Derivers also see derived events; ignore kinds you do not handle.
453    pub fn derive<F>(mut self, f: F) -> Self
454    where
455        F: FnMut(&Event) -> Vec<Event> + Send + 'static,
456    {
457        self.derivers.push(Box::new(f));
458        self
459    }
460
461    fn add_hook(&mut self, key: LaneKey, hook: Hook) {
462        let lane = self.lanes.map.entry(key).or_insert_with(Lane::new);
463        lane.hooks.lock().unwrap_or_else(|p| p.into_inner()).push(hook);
464    }
465
466    /// Channel that receives all events (including hook panic errors). It is released automatically if the receiver is dropped.
467    pub fn subscribe(&self) -> Receiver<Event> {
468        let (tx, rx) = channel();
469        self.shared.subscribers.lock().unwrap_or_else(|p| p.into_inner()).push(tx);
470        rx
471    }
472
473    /// Feed one input to the reducer and emit the resulting events. Returns the number of events emitted.
474    /// [`Reducer::prelude`] events enter the hook queue before processing, so asynchronous hooks run concurrently with the processing.
475    pub fn push(&mut self, input: R::Input) -> usize {
476        let pre = self.reducer.prelude(&input);
477        let n = self.dispatch(pre);
478        let evs = self.reducer.step(input);
479        n + self.dispatch(evs)
480    }
481
482    /// Collect finished background work and emit it (does not wait).
483    pub fn poll(&mut self) -> usize {
484        let evs = self.reducer.poll();
485        self.dispatch(evs)
486    }
487
488    /// Pass a command to the reducer and emit the resulting events.
489    pub fn command(&mut self, c: Command) -> usize {
490        let evs = self.reducer.command(c);
491        self.dispatch(evs)
492    }
493
494    /// Wait until all hook executions queued so far have finished.
495    pub fn flush(&mut self) {
496        for lane in self.lanes.map.values() {
497            lane.wait_idle();
498        }
499    }
500
501    /// Reducer reference.
502    pub fn reducer(&self) -> &R {
503        &self.reducer
504    }
505
506    /// Mutable reducer reference (events are not emitted).
507    pub fn reducer_mut(&mut self) -> &mut R {
508        &mut self.reducer
509    }
510
511    /// Collect and emit the remaining background work, drain the hook queues, close the workers and return the summary.
512    /// The subscription channels are closed when the pipeline ends.
513    pub fn finish(mut self) -> (R, Summary) {
514        let evs = self.reducer.finish();
515        self.dispatch(evs);
516        self.lanes.shutdown();
517        self.shared.subscribers.lock().unwrap_or_else(|p| p.into_inner()).clear();
518        let summary = Summary {
519            events: self.events,
520            by_kind: std::mem::take(&mut self.by_kind),
521            hook_calls: self.shared.hook_calls.load(Ordering::Relaxed),
522            hook_panics: self.shared.hook_panics.load(Ordering::Relaxed),
523            superseded: self.shared.superseded.load(Ordering::Relaxed),
524        };
525        let Pipeline { reducer, .. } = self;
526        (reducer, summary)
527    }
528
529    fn dispatch(&mut self, evs: Vec<Event>) -> usize {
530        let mut n = 0;
531        let mut queue: VecDeque<Event> = evs.into();
532        while let Some(ev) = queue.pop_front() {
533            n += 1;
534            if !self.derivers.is_empty() {
535                let mut derived = Vec::new();
536                for d in self.derivers.iter_mut() {
537                    match catch_unwind(AssertUnwindSafe(|| d(&ev))) {
538                        Ok(v) => derived.extend(v),
539                        Err(_) => {
540                            self.shared.hook_panics.fetch_add(1, Ordering::Relaxed);
541                            let meta = ev.meta().clone();
542                            self.shared.broadcast(&Event::Error { meta, message: "derive: panic".to_string() });
543                        }
544                    }
545                }
546                for d in derived.into_iter().rev() {
547                    queue.push_front(d);
548                }
549            }
550            self.events += 1;
551            *self.by_kind.entry(ev.kind()).or_insert(0) += 1;
552            self.shared.broadcast(&ev);
553            let kind = ev.kind();
554            if self.sync {
555                for key in [Some(kind), None] {
556                    if let Some(lane) = self.lanes.map.get(&key) {
557                        self.shared.run_hooks(&lane.hooks, &ev);
558                    }
559                }
560                continue;
561            }
562            let policy = self.policies.get(&kind).copied().unwrap_or_default();
563            let has_kind = self.lanes.map.contains_key(&Some(kind));
564            let has_any = self.lanes.map.contains_key(&None);
565            let mut pending = Some(ev);
566            for (key, present, last) in [(Some(kind), has_kind, !has_any), (None, has_any, true)] {
567                if !present {
568                    continue;
569                }
570                let ev = if last { pending.take() } else { pending.clone() };
571                let Some(ev) = ev else { break };
572                let name = match key {
573                    Some(k) => format!("hook-{k:?}"),
574                    None => "hook-any".to_string(),
575                };
576                let lane = self.lanes.map.get_mut(&key).expect("레인 존재 확인됨");
577                lane.ensure_worker(name, &self.shared);
578                let dropped = lane.enqueue(ev, policy);
579                self.shared.superseded.fetch_add(dropped, Ordering::Relaxed);
580            }
581        }
582        n
583    }
584}
585
586#[cfg(test)]
587mod tests {
588    use super::*;
589    use cumulus3d_core::Reconstruction;
590    use std::sync::mpsc::RecvTimeoutError;
591    use std::time::Duration;
592
593    fn meta(v: u64) -> Meta {
594        Meta { version: v, at: SystemTime::now() }
595    }
596    fn range(z: usize) -> ZoneRange {
597        ZoneRange { zone: z, lo: z * 10, hi: z * 10 + 10 }
598    }
599    fn cloud() -> Arc<PointCloud> {
600        Arc::new(PointCloud::default())
601    }
602    fn preview(v: u64, z: usize) -> Event {
603        Event::ZonePreview {
604            meta: meta(v),
605            range: range(z),
606            cloud: cloud(),
607            model: Arc::new(Reconstruction::default()),
608            dense: true,
609            frame: "gps".to_string(),
610        }
611    }
612    fn refined(v: u64, z: usize) -> Event {
613        Event::ZoneRefined { meta: meta(v), range: range(z), cloud: cloud(), model: Arc::new(Reconstruction::default()), dense: true }
614    }
615
616    /// For input n, emit PositionDone(n), and for even n also ZonePreview(zone n/2).
617    /// In finish, emit the remaining refined results (ZoneRefined).
618    #[derive(Default)]
619    struct Fake {
620        v: u64,
621        zones: Vec<usize>,
622        commands: Vec<String>,
623    }
624
625    impl Reducer for Fake {
626        type Input = usize;
627        fn step(&mut self, n: usize) -> Vec<Event> {
628            self.v += 1;
629            let mut out = vec![Event::PositionDone { meta: meta(self.v), position: n, registered: n, expected: n }];
630            if n.is_multiple_of(2) {
631                out.push(preview(self.v, n / 2));
632                self.zones.push(n / 2);
633            }
634            out
635        }
636        fn poll(&mut self) -> Vec<Event> {
637            Vec::new()
638        }
639        fn command(&mut self, c: Command) -> Vec<Event> {
640            self.commands.push(format!("{c:?}"));
641            match c {
642                Command::InvalidateZone(z) => vec![Event::Warning { meta: meta(self.v), message: format!("invalidate {z}") }],
643                Command::ResetFrom(p) => vec![Event::Warning { meta: meta(self.v), message: format!("reset {p}") }],
644            }
645        }
646        fn finish(&mut self) -> Vec<Event> {
647            self.zones.drain(..).map(|z| refined(self.v, z)).collect()
648        }
649    }
650
651    fn log<T>() -> Arc<Mutex<Vec<T>>> {
652        Arc::new(Mutex::new(Vec::new()))
653    }
654
655    #[test]
656    fn pipeline_sync_hook_order() {
657        let l = log();
658        let (a, b, c) = (Arc::clone(&l), Arc::clone(&l), Arc::clone(&l));
659        let mut p = Pipeline::new(Fake::default())
660            .sync(true)
661            .on_any(move |e| a.lock().unwrap().push(format!("any:{:?}", e.kind())))
662            .on_position_done(move |pos, _, _| b.lock().unwrap().push(format!("pos:{pos}")))
663            .on_zone_preview(move |r, _| c.lock().unwrap().push(format!("prev:{}", r.zone)));
664        p.push(1);
665        p.push(2);
666        let (_, s) = p.finish();
667        let got = l.lock().unwrap().clone();
668        assert_eq!(got, vec!["pos:1", "any:PositionDone", "pos:2", "any:PositionDone", "prev:1", "any:ZonePreview", "any:ZoneRefined"]);
669        assert_eq!(s.events, 4);
670        assert_eq!(s.by_kind[&EventKind::PositionDone], 2);
671    }
672
673    #[test]
674    fn pipeline_async_order_within_kind() {
675        let l = log::<String>();
676        let any = log::<String>();
677        let (a, b) = (Arc::clone(&l), Arc::clone(&any));
678        let mut p = Pipeline::new(Fake::default())
679            .on_position_done(move |pos, _, _| {
680                if pos.is_multiple_of(7) {
681                    std::thread::sleep(Duration::from_millis(1));
682                }
683                a.lock().unwrap().push(pos.to_string())
684            })
685            .on_any(move |e| b.lock().unwrap().push(format!("{:?}", e.kind())));
686        for i in 0..200 {
687            p.push(i);
688        }
689        let (_, s) = p.finish();
690        let got: Vec<usize> = l.lock().unwrap().iter().map(|s| s.parse().unwrap()).collect();
691        assert_eq!(got, (0..200).collect::<Vec<_>>());
692        assert_eq!(any.lock().unwrap().len(), s.events);
693        assert_eq!(s.superseded, 0);
694    }
695
696    #[test]
697    fn pipeline_latest_per_key_policy() {
698        // With the hooks blocked, putting in previews of the same zone several times leaves only the last one.
699        let gate = Arc::new((Mutex::new(false), Condvar::new()));
700        let seen = log();
701        let (g, s2) = (Arc::clone(&gate), Arc::clone(&seen));
702
703        struct Previews(u64);
704        impl Reducer for Previews {
705            type Input = (usize, u64);
706            fn step(&mut self, (z, v): (usize, u64)) -> Vec<Event> {
707                self.0 += 1;
708                vec![preview(v, z)]
709            }
710            fn poll(&mut self) -> Vec<Event> {
711                Vec::new()
712            }
713            fn command(&mut self, _: Command) -> Vec<Event> {
714                Vec::new()
715            }
716        }
717
718        let mut p =
719            Pipeline::new(Previews(0)).policy(EventKind::ZonePreview, QueuePolicy::LatestPerKey).on(EventKind::ZonePreview, move |e| {
720                let (m, cv) = &*g;
721                let mut open = m.lock().unwrap();
722                while !*open {
723                    open = cv.wait(open).unwrap();
724                }
725                if let Event::ZonePreview { meta, range, .. } = e {
726                    s2.lock().unwrap().push(format!("{}@{}", range.zone, meta.version));
727                }
728            });
729        // The worker may have picked up the first event and be blocked, so wait a moment to make sure it is picked up.
730        p.push((9, 0));
731        std::thread::sleep(Duration::from_millis(50));
732        for v in 1..=5 {
733            p.push((1, v));
734            p.push((2, v));
735        }
736        {
737            let (m, cv) = &*gate;
738            *m.lock().unwrap() = true;
739            cv.notify_all();
740        }
741        let (_, s) = p.finish();
742        assert_eq!(seen.lock().unwrap().clone(), vec!["9@0", "1@5", "2@5"]);
743        assert_eq!(s.superseded, 8);
744        assert_eq!(s.events, 11);
745    }
746
747    #[test]
748    fn pipeline_panic_isolated() {
749        let l = log();
750        let a = Arc::clone(&l);
751        let mut p = Pipeline::new(Fake::default())
752            .on_position_done(|pos, _, _| {
753                if pos == 1 {
754                    panic!("boom");
755                }
756            })
757            .on_position_done(move |pos, _, _| a.lock().unwrap().push(pos));
758        let rx = p.subscribe();
759        for i in 0..4 {
760            p.push(i);
761        }
762        let (_, s) = p.finish();
763        assert_eq!(l.lock().unwrap().clone(), vec![0, 1, 2, 3]);
764        assert_eq!(s.hook_panics, 1);
765        let errs: Vec<String> = rx
766            .iter()
767            .filter_map(|e| match e {
768                Event::Error { message, meta } => {
769                    assert_eq!(meta.version, 2);
770                    Some(message)
771                }
772                _ => None,
773            })
774            .collect();
775        assert_eq!(errs.len(), 1);
776        assert!(errs[0].contains("boom"), "{}", errs[0]);
777    }
778
779    #[test]
780    fn pipeline_channel_receives_all() {
781        let mut p = Pipeline::new(Fake::default());
782        let rx = p.subscribe();
783        p.push(1);
784        p.push(2);
785        let kinds: Vec<EventKind> = rx.try_iter().map(|e| e.kind()).collect();
786        assert_eq!(kinds, vec![EventKind::PositionDone, EventKind::PositionDone, EventKind::ZonePreview]);
787        let dropped = p.subscribe();
788        drop(dropped);
789        p.push(3);
790        let (_, s) = p.finish();
791        let rest: Vec<EventKind> = rx.iter().map(|e| e.kind()).collect();
792        assert_eq!(rest, vec![EventKind::PositionDone, EventKind::ZoneRefined]);
793        assert_eq!(s.events, 5);
794        assert!(matches!(rx.recv_timeout(Duration::from_millis(1)), Err(RecvTimeoutError::Disconnected)));
795    }
796
797    #[test]
798    fn pipeline_finish_drains() {
799        let n = Arc::new(AtomicUsize::new(0));
800        let refined = log();
801        let (a, b) = (Arc::clone(&n), Arc::clone(&refined));
802        let mut p = Pipeline::new(Fake::default())
803            .on(EventKind::PositionDone, move |_| {
804                std::thread::sleep(Duration::from_micros(200));
805                a.fetch_add(1, Ordering::SeqCst);
806            })
807            .on_zone_refined(move |r, _| b.lock().unwrap().push(r.zone));
808        for i in 0..50 {
809            p.push(i);
810        }
811        let (r, s) = p.finish();
812        assert_eq!(n.load(Ordering::SeqCst), 50);
813        assert_eq!(refined.lock().unwrap().len(), 25);
814        assert!(r.zones.is_empty());
815        assert_eq!(s.hook_calls, 75);
816    }
817
818    #[test]
819    fn pipeline_command_forwarded() {
820        let msgs = log();
821        let a = Arc::clone(&msgs);
822        let mut p = Pipeline::new(Fake::default()).sync(true).on_message(move |k, m| a.lock().unwrap().push(format!("{k:?}:{m}")));
823        assert_eq!(p.command(Command::InvalidateZone(3)), 1);
824        p.command(Command::ResetFrom(7));
825        assert_eq!(p.reducer().commands, vec!["InvalidateZone(3)", "ResetFrom(7)"]);
826        let (_, _) = p.finish();
827        assert_eq!(msgs.lock().unwrap().clone(), vec!["Warning:invalidate 3", "Warning:reset 7"]);
828    }
829}