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