Skip to main content

proxy_watch/
bridge.rs

1//! Tokio bridge: single-consumer [`ProxyWatcher`] → multi-consumer [`tokio::sync::watch`].
2//! Behind the off-by-default `tokio` feature.
3
4use std::future::{Future, poll_fn};
5use std::pin::{Pin, pin};
6use std::task::Poll;
7
8use futures_core::Stream;
9use tokio::sync::watch;
10
11use crate::config::ProxyConfig;
12use crate::watch::{ProxyWatcher, WatchEvent};
13
14/// Drive `watcher` into a [`tokio::sync::watch`] channel (clone [`watch::Receiver`] for fan-out).
15///
16/// Starts at [`ProxyWatcher::current`]. Errors dropped; identical snapshots not republished;
17/// last receiver drop ends the task. Panics outside a tokio runtime.
18///
19/// **Configuration only.** [`WatchEvent`] also carries [`WatchHealth`](crate::WatchHealth),
20/// and this channel
21/// drops it: a route that dies without changing the configuration publishes nothing here.
22/// Read liveness from [`ProxyWatcher::state`], or poll the [`Stream`] directly, and use
23/// this for fanning the configuration out to many tasks.
24///
25/// ```no_run
26/// # async fn example() -> Result<(), proxy_watch::Error> {
27/// let mut rx = proxy_watch::watch_channel(proxy_watch::ProxyWatcher::new()?);
28/// let mirror = rx.clone();
29/// tokio::spawn(async move {
30///     println!("another task sees {:?}", mirror.borrow().effective);
31/// });
32///
33/// while rx.changed().await.is_ok() {
34///     println!("changed to {:?}", rx.borrow_and_update().effective);
35/// }
36/// # Ok(())
37/// # }
38/// ```
39#[must_use]
40pub fn watch_channel(watcher: ProxyWatcher) -> watch::Receiver<ProxyConfig> {
41    bridge(watcher.current(), watcher)
42}
43
44// Generic over the stream so tests need no platform backend.
45fn bridge<S>(initial: ProxyConfig, stream: S) -> watch::Receiver<ProxyConfig>
46where
47    S: Stream<Item = WatchEvent> + Unpin + Send + 'static,
48{
49    let (sender, receiver) = watch::channel(initial);
50    let mut stream = stream;
51    tokio::spawn(async move {
52        // End as soon as the last receiver drops (do not wait for a change that may never come).
53        let mut closed = pin!(sender.closed());
54        loop {
55            let next = poll_fn(|cx| {
56                if closed.as_mut().poll(cx).is_ready() {
57                    return Poll::Ready(None);
58                }
59                Pin::new(&mut stream).poll_next(cx)
60            })
61            .await;
62            let Some(item) = next else {
63                break;
64            };
65            // A health-only snapshot repeats the configuration it already published, so
66            // the equality filter below drops it without a special case.
67            if let WatchEvent::Snapshot { state } = item {
68                let config = state.config;
69                sender.send_if_modified(|current| {
70                    if *current == config {
71                        return false;
72                    }
73                    *current = config;
74                    true
75                });
76            }
77        }
78    });
79    receiver
80}
81
82#[cfg(test)]
83mod tests {
84    use std::sync::Arc;
85    use std::sync::atomic::{AtomicBool, Ordering};
86    use std::task::Context;
87
88    use super::*;
89
90    use crate::config::ProxyConfigSource;
91    use crate::error::Error;
92    use crate::mode::ProxyMode;
93    use crate::watch::{WatchHealth, WatchState};
94
95    // The health half the bridge is contracted to ignore, as a baseline the events below
96    // share — except the last, which departs from it deliberately. That one is the event
97    // that shows anything reaching a receiver got there through the config: it moves the
98    // health and nothing else, and no receiver wakes.
99    fn live() -> WatchHealth {
100        WatchHealth {
101            degraded: Vec::new(),
102            has_live_notifications: true,
103            poll_interval: None,
104            stopped: false,
105        }
106    }
107
108    fn snapshot(config: ProxyConfig) -> WatchEvent {
109        WatchEvent::Snapshot {
110            state: WatchState {
111                config,
112                health: live(),
113            },
114        }
115    }
116
117    // The configuration an error carries is a real snapshot, and one the bridge is
118    // contracted to drop along with the error. It differs from every other configuration
119    // here so that dropping it is something a receiver can tell from not dropping it.
120    fn failure(error: Error) -> WatchEvent {
121        WatchEvent::Error {
122            error,
123            state: WatchState {
124                config: ProxyConfig::from_source(ProxyConfigSource::Env, ProxyMode::Direct),
125                health: live(),
126            },
127        }
128    }
129
130    // A stream the test feeds one event at a time. The bridge consumes everything already
131    // waiting in a single poll, so a stream handing out a canned list cannot show what any
132    // one event did — the receiver first looks after the last of them has been folded in.
133    struct Fed(tokio::sync::mpsc::UnboundedReceiver<WatchEvent>);
134
135    impl Stream for Fed {
136        type Item = WatchEvent;
137
138        fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
139            self.0.poll_recv(cx)
140        }
141    }
142
143    fn runtime() -> tokio::runtime::Runtime {
144        tokio::runtime::Builder::new_current_thread()
145            .build()
146            .expect("current-thread runtime")
147    }
148
149    #[test]
150    fn changes_reach_every_receiver_and_noise_is_filtered() {
151        let initial = ProxyConfig::direct();
152        let changed =
153            ProxyConfig::from_source(ProxyConfigSource::Registry, ProxyMode::WpadAutoDetect);
154
155        runtime().block_on(async {
156            let (events, stream) = tokio::sync::mpsc::unbounded_channel();
157            let mut receiver = bridge(initial.clone(), Fed(stream));
158            let clone = receiver.clone();
159
160            // The initial value is available before the task has even run.
161            assert_eq!(*receiver.borrow(), initial);
162
163            // The watcher re-emits the current value at subscription time.
164            events.send(snapshot(initial.clone())).unwrap();
165            events.send(snapshot(changed.clone())).unwrap();
166
167            receiver.changed().await.expect("a change is published");
168            assert_eq!(*receiver.borrow_and_update(), changed);
169            // Every clone sees it too.
170            assert_eq!(*clone.borrow(), changed);
171
172            // What the bridge must publish nothing for, sent once the receiver has caught
173            // up with everything before it — anything sent earlier is folded into the
174            // change above by the channel and shows nothing.
175            //
176            // A transient failure must not disturb the channel, and a route that died
177            // without the configuration moving must not either: this bridge carries the
178            // configuration alone.
179            events.send(failure(Error::Unsupported)).unwrap();
180            events
181                .send(WatchEvent::Snapshot {
182                    state: WatchState {
183                        config: changed.clone(),
184                        health: WatchHealth {
185                            degraded: vec![ProxyConfigSource::GroupPolicy],
186                            ..live()
187                        },
188                    },
189                })
190                .unwrap();
191            tokio::task::yield_now().await;
192            assert!(
193                !receiver
194                    .has_changed()
195                    .expect("the stream is still open, so the channel is too"),
196                "neither an error nor a health-only snapshot may publish a configuration"
197            );
198
199            // Nothing left to read from ends the task, and with it the sender.
200            drop(events);
201            assert!(receiver.changed().await.is_err());
202        });
203    }
204
205    // A watcher that never reports anything, and says when it is dropped.
206    struct Idle(Arc<AtomicBool>);
207
208    impl Stream for Idle {
209        type Item = WatchEvent;
210
211        fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
212            Poll::Pending
213        }
214    }
215
216    impl Drop for Idle {
217        fn drop(&mut self) {
218            self.0.store(true, Ordering::SeqCst);
219        }
220    }
221
222    // The watcher owns an OS thread and a change notification, so the bridge must let
223    // go of it as soon as nobody is listening — without waiting for a configuration
224    // change that may never come.
225    #[test]
226    fn dropping_the_last_receiver_releases_the_watcher() {
227        let dropped = Arc::new(AtomicBool::new(false));
228
229        runtime().block_on(async {
230            let receiver = bridge(ProxyConfig::direct(), Idle(Arc::clone(&dropped)));
231            drop(receiver);
232            // Let the task run; it has no snapshot to wait for.
233            tokio::task::yield_now().await;
234            tokio::task::yield_now().await;
235            assert!(
236                dropped.load(Ordering::SeqCst),
237                "the bridge task must end once the last receiver is gone"
238            );
239        });
240    }
241}