Skip to main content

loadpace_tower/
service.rs

1use futures_core::stream::{Stream, TryStream};
2use loadpace::{
3    DispatchReservation, DispatchState, EndpointConfig, EndpointController, InFlightRequest,
4    Outcome,
5};
6use rand::Rng;
7use std::future::Future;
8use std::marker::PhantomData;
9use std::pin::Pin;
10use std::sync::{Arc, Mutex};
11use std::task::{Context, Poll};
12use std::time::Instant;
13use tower::Service;
14use tower::discover::Change;
15use tower::load::Load;
16
17/// A comparable predicted completion cost for P2C selection.
18///
19/// Lower values are better. The value is measured in seconds from the moment
20/// the metric was read and includes the endpoint's expected RTT.
21#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)]
22pub struct LoadMetric(pub f64);
23
24impl LoadMetric {
25    pub fn as_secs(self) -> f64 {
26        self.0
27    }
28}
29
30struct Shared<S> {
31    inner: tokio::sync::Mutex<S>,
32    controller: Mutex<EndpointController>,
33    ready: Arc<tokio::sync::Semaphore>,
34    dispatch: tokio::sync::Notify,
35}
36
37type ReadinessFuture = Pin<
38    Box<
39        dyn Future<Output = Result<tokio::sync::OwnedSemaphorePermit, tokio::sync::AcquireError>>
40            + Send
41            + 'static,
42    >,
43>;
44
45// Core deliberately uses `std::time::Instant`; Tower waits use Tokio's
46// runtime clock. Converting here keeps controller deadlines and Tokio timers
47// in the same clock domain, including when Tokio time is paused in tests.
48fn now() -> Instant {
49    tokio::time::Instant::now().into_std()
50}
51
52/// A Tower service with per-endpoint adaptive pacing and bounded admission.
53///
54/// `poll_ready` reports whether another request can enter the endpoint's
55/// bounded scheduling horizon. `call` reserves a virtual GCRA slot; the
56/// returned future waits until that slot is due, waits for the inner service to
57/// be ready, and only then records the actual dispatch. Queue delay therefore
58/// never contaminates the RTT sample.
59pub struct AdaptiveEndpoint<S> {
60    shared: Arc<Shared<S>>,
61    readiness_permit: Option<tokio::sync::OwnedSemaphorePermit>,
62    readiness: Mutex<Option<ReadinessFuture>>,
63}
64
65impl<S> AdaptiveEndpoint<S> {
66    pub fn new(inner: S, config: EndpointConfig) -> Self {
67        Self::new_at(inner, config, now())
68    }
69
70    pub fn new_at(inner: S, config: EndpointConfig, now: Instant) -> Self {
71        let queue_capacity = config.queue_capacity;
72        Self {
73            shared: Arc::new(Shared {
74                inner: tokio::sync::Mutex::new(inner),
75                controller: Mutex::new(EndpointController::new(config, now)),
76                ready: Arc::new(tokio::sync::Semaphore::new(queue_capacity)),
77                dispatch: tokio::sync::Notify::new(),
78            }),
79            readiness_permit: None,
80            readiness: Mutex::new(None),
81        }
82    }
83
84    fn with_controller<T>(&self, operation: impl FnOnce(&mut EndpointController) -> T) -> T {
85        let (result, changed) = {
86            let mut controller = self
87                .shared
88                .controller
89                .lock()
90                .expect("controller mutex poisoned");
91            let before = controller.probe().current();
92            let result = operation(&mut controller);
93            (result, before != controller.probe().current())
94        };
95        if changed {
96            self.shared.dispatch.notify_waiters();
97        }
98        result
99    }
100
101    pub fn snapshot(&self) -> loadpace::ControllerSnapshot {
102        self.with_controller(|controller| controller.snapshot(now()))
103    }
104
105    pub fn start_positive_probe(&self, delta: f64, until: Instant) {
106        self.with_controller(|controller| {
107            controller.start_positive_probe(delta, until, now());
108        });
109    }
110
111    pub fn start_negative_probe(&self, factor: f64, until: Instant) {
112        self.with_controller(|controller| {
113            controller.start_negative_probe(factor, until, now());
114        });
115    }
116
117    /// Gives a caller-provided RNG a time-gated chance to start a probe.
118    ///
119    /// The method is intentionally caller-driven: applications can choose
120    /// where to run the check and simulations can provide deterministic RNGs.
121    pub fn maybe_start_probe<R: Rng + ?Sized>(
122        &self,
123        schedule: &loadpace::ProbeSchedule,
124        rng: &mut R,
125    ) -> Option<loadpace::Probe> {
126        self.with_controller(|controller| controller.maybe_start_probe(schedule, rng, now()))
127    }
128
129    pub fn load_metric(&self) -> LoadMetric {
130        self.with_controller(|controller| {
131            let current = now();
132            controller.refresh(current);
133            LoadMetric(controller.load(current))
134        })
135    }
136}
137
138/// Maps a Tower discovery stream into freshly initialized adaptive endpoints.
139///
140/// The wrapper intentionally creates new controller state for every insert.
141/// This is the safe behavior when discovery removes and later reuses an
142/// endpoint key; state retention can be added without changing the discovery
143/// contract once churn behavior is better understood.
144pub struct AdaptiveDiscovery<D, Request> {
145    inner: D,
146    config: EndpointConfig,
147    _request: PhantomData<fn() -> Request>,
148}
149
150impl<D, Request> AdaptiveDiscovery<D, Request> {
151    pub fn new(inner: D, config: EndpointConfig) -> Self {
152        Self {
153            inner,
154            config,
155            _request: PhantomData,
156        }
157    }
158
159    pub fn into_inner(self) -> D {
160        self.inner
161    }
162}
163
164impl<D, Request, K, S> Stream for AdaptiveDiscovery<D, Request>
165where
166    D: TryStream<Ok = Change<K, S>> + Unpin,
167    K: Eq,
168    S: Service<Request> + Send + 'static,
169    S::Future: Send + 'static,
170    S::Response: Send + 'static,
171    S::Error: Send + 'static,
172    Request: 'static,
173{
174    type Item = Result<Change<K, AdaptiveEndpoint<S>>, D::Error>;
175
176    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
177        let this = self.get_mut();
178        Pin::new(&mut this.inner).try_poll_next(cx).map(|change| {
179            change.map(|result| {
180                result.map(|change| match change {
181                    Change::Insert(key, service) => {
182                        Change::Insert(key, AdaptiveEndpoint::new(service, this.config.clone()))
183                    }
184                    Change::Remove(key) => Change::Remove(key),
185                })
186            })
187        })
188    }
189}
190
191impl<S> Clone for AdaptiveEndpoint<S> {
192    fn clone(&self) -> Self {
193        Self {
194            shared: Arc::clone(&self.shared),
195            readiness_permit: None,
196            readiness: Mutex::new(None),
197        }
198    }
199}
200
201impl<S> Load for AdaptiveEndpoint<S> {
202    type Metric = LoadMetric;
203
204    fn load(&self) -> Self::Metric {
205        self.load_metric()
206    }
207}
208
209impl<S, Request> Service<Request> for AdaptiveEndpoint<S>
210where
211    S: Service<Request> + Send + 'static,
212    S::Future: Send + 'static,
213    S::Response: Send + 'static,
214    S::Error: Send + 'static,
215    Request: Send + 'static,
216{
217    type Response = S::Response;
218    type Error = S::Error;
219    type Future = ResponseFuture<S::Response, S::Error>;
220
221    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
222        if self.readiness_permit.is_some() {
223            return Poll::Ready(Ok(()));
224        }
225
226        let mut readiness = self
227            .readiness
228            .lock()
229            .expect("readiness future mutex poisoned");
230        if readiness.is_none() {
231            match Arc::clone(&self.shared.ready).try_acquire_owned() {
232                Ok(permit) => {
233                    drop(readiness);
234                    self.readiness_permit = Some(permit);
235                    return Poll::Ready(Ok(()));
236                }
237                Err(tokio::sync::TryAcquireError::NoPermits) => {
238                    *readiness = Some(Box::pin(Arc::clone(&self.shared.ready).acquire_owned()));
239                }
240                Err(tokio::sync::TryAcquireError::Closed) => {
241                    panic!("AdaptiveEndpoint readiness semaphore was closed")
242                }
243            }
244        }
245
246        match readiness
247            .as_mut()
248            .expect("readiness future must exist")
249            .as_mut()
250            .poll(cx)
251        {
252            Poll::Ready(Ok(permit)) => {
253                *readiness = None;
254                self.readiness_permit = Some(permit);
255                Poll::Ready(Ok(()))
256            }
257            Poll::Ready(Err(_)) => {
258                panic!("AdaptiveEndpoint readiness semaphore was closed")
259            }
260            Poll::Pending => Poll::Pending,
261        }
262    }
263
264    fn call(&mut self, request: Request) -> Self::Future {
265        assert!(
266            self.readiness_permit.is_some(),
267            "AdaptiveEndpoint::call invoked without available readiness"
268        );
269        let readiness_permit = self
270            .readiness_permit
271            .take()
272            .expect("readiness permit must exist after poll_ready");
273        let reservation = self
274            .shared
275            .controller
276            .lock()
277            .expect("controller mutex poisoned")
278            .reserve(now())
279            .expect("readiness reservation was not reflected in controller capacity");
280
281        let guard = RequestGuard::new(Arc::clone(&self.shared), reservation, readiness_permit);
282        let future = dispatch_request(Arc::clone(&self.shared), request, guard);
283        ResponseFuture {
284            inner: Box::pin(future),
285        }
286    }
287}
288
289/// The future returned by [`AdaptiveEndpoint::call`].
290pub struct ResponseFuture<T, E> {
291    inner: Pin<Box<dyn Future<Output = Result<T, E>> + Send + 'static>>,
292}
293
294impl<T, E> Future for ResponseFuture<T, E> {
295    type Output = Result<T, E>;
296
297    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
298        self.get_mut().inner.as_mut().poll(cx)
299    }
300}
301
302struct RequestGuard<S> {
303    shared: Arc<Shared<S>>,
304    reservation: Option<DispatchReservation>,
305    readiness_permit: Option<tokio::sync::OwnedSemaphorePermit>,
306    active: Option<InFlightRequest>,
307    finished: bool,
308}
309
310impl<S> RequestGuard<S> {
311    fn new(
312        shared: Arc<Shared<S>>,
313        reservation: DispatchReservation,
314        readiness_permit: tokio::sync::OwnedSemaphorePermit,
315    ) -> Self {
316        Self {
317            shared,
318            reservation: Some(reservation),
319            readiness_permit: Some(readiness_permit),
320            active: None,
321            finished: false,
322        }
323    }
324
325    fn mark_dispatched(&mut self, active: InFlightRequest) {
326        self.reservation = None;
327        self.active = Some(active);
328        // The controller has removed the request from its virtual queue, so
329        // releasing this permit cannot expose more work than the queue allows.
330        self.readiness_permit = None;
331    }
332
333    fn finish(&mut self, outcome: Outcome, now: Instant) {
334        if self.finished {
335            return;
336        }
337        self.finished = true;
338        if let Some(active) = self.active.take() {
339            let latency = now.saturating_duration_since(active.dispatched_at());
340            self.shared
341                .controller
342                .lock()
343                .expect("controller mutex poisoned")
344                .on_complete(active, outcome, latency, now);
345        } else if let Some(reservation) = self.reservation.take() {
346            self.shared
347                .controller
348                .lock()
349                .expect("controller mutex poisoned")
350                .cancel(reservation, now);
351        }
352        // Release capacity only after the controller has been updated. A
353        // newly woken caller must observe the cancellation/completion first.
354        self.readiness_permit = None;
355        self.shared.dispatch.notify_waiters();
356    }
357}
358
359impl<S> Drop for RequestGuard<S> {
360    fn drop(&mut self) {
361        if !self.finished {
362            self.finish(Outcome::Failure, now());
363        }
364    }
365}
366
367async fn dispatch_request<S, Request>(
368    shared: Arc<Shared<S>>,
369    request: Request,
370    mut guard: RequestGuard<S>,
371) -> Result<S::Response, S::Error>
372where
373    S: Service<Request> + Send + 'static,
374    S::Future: Send + 'static,
375    S::Response: Send + 'static,
376    S::Error: Send + 'static,
377    Request: Send + 'static,
378{
379    let reservation = guard
380        .reservation
381        .expect("request guard must begin with a reservation");
382
383    loop {
384        let notified = shared.dispatch.notified();
385        let (decision, probe_until) = {
386            let mut controller = shared.controller.lock().expect("controller mutex poisoned");
387            let current = now();
388            controller.refresh(current);
389            (
390                controller.dispatch_state(reservation, current),
391                controller.probe().current().map(|probe| probe.until),
392            )
393        };
394
395        match decision {
396            DispatchState::Ready => {
397                break;
398            }
399            DispatchState::WaitUntil(deadline) => {
400                let wake_at = probe_until.map_or(deadline, |until| deadline.min(until));
401                let delay = wake_at.saturating_duration_since(now());
402                let _ = tokio::time::timeout(delay, notified).await;
403            }
404            DispatchState::WaitForPrevious | DispatchState::InflightLimit => {
405                notified.await;
406            }
407            DispatchState::Cancelled => {
408                panic!("an AdaptiveEndpoint request was cancelled while being polled");
409            }
410        }
411    }
412
413    let result = {
414        let mut inner = shared.inner.lock().await;
415        match std::future::poll_fn(|cx| inner.poll_ready(cx)).await {
416            Ok(()) => {
417                let now = now();
418                let active = shared
419                    .controller
420                    .lock()
421                    .expect("controller mutex poisoned")
422                    .on_dispatched(reservation, now)
423                    .expect("dispatch state changed unexpectedly");
424                guard.mark_dispatched(active);
425                let future = inner.call(request);
426                drop(inner);
427                future.await
428            }
429            Err(error) => {
430                shared
431                    .controller
432                    .lock()
433                    .expect("controller mutex poisoned")
434                    .on_admission_failure(now());
435                Err(error)
436            }
437        }
438    };
439
440    let outcome = if result.is_ok() {
441        Outcome::Success
442    } else {
443        Outcome::Failure
444    };
445    let now = now();
446    guard.finish(outcome, now);
447    result
448}