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}