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 pin_project_lite::pin_project;
7use std::future::Future;
8use std::pin::Pin;
9use std::sync::{Arc, Mutex, MutexGuard};
10use std::task::{Context, Poll, Waker};
11use std::time::Instant;
12use tower::discover::Change;
13use tower::load::Load;
14use tower::{Layer, Service};
15
16/// A comparable predicted completion cost for P2C selection.
17///
18/// Lower values are better. The value is measured in seconds from the moment
19/// the metric was read and includes the endpoint's expected RTT.
20#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)]
21pub struct LoadMetric(pub f64);
22
23impl LoadMetric {
24    /// Returns the predicted completion cost in seconds.
25    pub fn as_secs(self) -> f64 {
26        self.0
27    }
28}
29
30struct Shared<S> {
31    service: tokio::sync::Mutex<S>,
32    controller: Mutex<ControllerState>,
33    dispatch: tokio::sync::Notify,
34}
35
36struct ControllerState {
37    controller: EndpointController,
38    admission_waker: Option<Waker>,
39}
40
41impl ControllerState {
42    fn poll_admission(&mut self, cx: &mut Context<'_>) -> Poll<()> {
43        if self.controller.may_schedule() {
44            self.admission_waker = None;
45            return Poll::Ready(());
46        }
47
48        if self
49            .admission_waker
50            .as_ref()
51            .is_none_or(|waker| !waker.will_wake(cx.waker()))
52        {
53            self.admission_waker = Some(cx.waker().clone());
54        }
55        Poll::Pending
56    }
57
58    fn take_admission_waker(&mut self) -> Option<Waker> {
59        self.admission_waker.take()
60    }
61}
62
63// Core deliberately uses `std::time::Instant`; Tower waits use Tokio's
64// runtime clock. Converting here keeps controller deadlines and Tokio timers
65// in the same clock domain, including when Tokio time is paused in tests.
66fn now() -> Instant {
67    tokio::time::Instant::now().into_std()
68}
69
70fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
71    mutex
72        .lock()
73        .expect("loadpace-tower internal mutex was poisoned")
74}
75
76/// A Tower service with per-endpoint adaptive pacing and bounded admission.
77///
78/// `poll_ready` reports whether another request can enter the endpoint's
79/// bounded scheduling horizon. `call` reserves a virtual GCRA slot; the
80/// returned future waits until that slot is due, waits for the inner service to
81/// be ready, and only then records the actual dispatch. Queue delay therefore
82/// never contaminates the RTT sample.
83///
84/// The endpoint is intentionally single-owner: Tower's P2C balancer owns one
85/// service per discovered backend and does not require endpoint services to be
86/// cloneable. This lets readiness use the controller's queue state directly,
87/// without a second shared allocation for coordinating cloned handles.
88pub struct AdaptiveEndpoint<S> {
89    shared: Arc<Shared<S>>,
90}
91
92impl<S> AdaptiveEndpoint<S> {
93    /// Wraps a Tower endpoint using the current Tokio runtime time.
94    ///
95    /// The returned service starts with an immediately available pacing slot.
96    ///
97    /// # Panics
98    ///
99    /// Panics when `config` contains invalid controller settings.
100    pub fn new(inner: S, config: EndpointConfig) -> Self {
101        Self::new_at(inner, config, now())
102    }
103
104    /// Wraps a Tower endpoint using an explicit controller start time.
105    ///
106    /// This constructor is useful in deterministic tests. Production code
107    /// should normally use [`Self::new`] so Tokio and controller deadlines
108    /// share the runtime's clock domain.
109    ///
110    /// # Panics
111    ///
112    /// Panics when `config` contains invalid controller settings.
113    pub fn new_at(inner: S, config: EndpointConfig, now: Instant) -> Self {
114        Self {
115            shared: Arc::new(Shared {
116                service: tokio::sync::Mutex::new(inner),
117                controller: Mutex::new(ControllerState {
118                    controller: EndpointController::new(config, now),
119                    admission_waker: None,
120                }),
121                dispatch: tokio::sync::Notify::new(),
122            }),
123        }
124    }
125
126    fn with_controller<T>(&self, operation: impl FnOnce(&mut EndpointController) -> T) -> T {
127        let (result, changed) = {
128            let mut state = lock(&self.shared.controller);
129            let before = state.controller.active_probe();
130            let result = operation(&mut state.controller);
131            (result, before != state.controller.active_probe())
132        };
133        if changed {
134            self.shared.dispatch.notify_waiters();
135        }
136        result
137    }
138
139    /// Returns a current snapshot of this endpoint's controller.
140    ///
141    /// Reading a snapshot refreshes time-driven probe state and may update the
142    /// effective pacing rate.
143    pub fn snapshot(&self) -> loadpace::ControllerSnapshot {
144        self.with_controller(|controller| controller.snapshot(now()))
145    }
146
147    /// Returns the endpoint's predicted completion cost for load balancing.
148    ///
149    /// Lower values are preferred. Reading the metric refreshes time-driven
150    /// controller state, including probes.
151    pub fn load_metric(&self) -> LoadMetric {
152        self.with_controller(|controller| {
153            let current = now();
154            LoadMetric(controller.load(current))
155        })
156    }
157}
158
159/// A Tower layer that wraps a service in an [`AdaptiveEndpoint`].
160#[derive(Clone, Debug)]
161pub struct AdaptiveLayer {
162    config: EndpointConfig,
163}
164
165impl AdaptiveLayer {
166    /// Creates a layer using the supplied endpoint configuration.
167    pub fn new(config: EndpointConfig) -> Self {
168        Self { config }
169    }
170
171    /// Returns the configuration cloned into wrapped services.
172    pub fn config(&self) -> &EndpointConfig {
173        &self.config
174    }
175}
176
177impl Default for AdaptiveLayer {
178    fn default() -> Self {
179        Self::new(EndpointConfig::default())
180    }
181}
182
183impl<S> Layer<S> for AdaptiveLayer {
184    type Service = AdaptiveEndpoint<S>;
185
186    fn layer(&self, inner: S) -> Self::Service {
187        AdaptiveEndpoint::new(inner, self.config.clone())
188    }
189}
190
191pin_project! {
192    #[doc = "Maps a Tower discovery stream into freshly initialized adaptive endpoints."]
193    #[doc = ""]
194    #[doc = "The wrapper intentionally creates new controller state for every insert."]
195    #[doc = "This is the safe behavior when discovery removes and later reuses an"]
196    #[doc = "endpoint key; state retention can be added without changing the discovery"]
197    #[doc = "contract once churn behavior is better understood."]
198    pub struct AdaptiveDiscovery<D> {
199        #[pin]
200        inner: D,
201        config: EndpointConfig,
202    }
203}
204
205impl<D> AdaptiveDiscovery<D> {
206    /// Wraps a discovery stream and clones `config` into every inserted service.
207    pub fn new(inner: D, config: EndpointConfig) -> Self {
208        Self { inner, config }
209    }
210
211    /// Consumes the wrapper and returns the original discovery stream.
212    pub fn into_inner(self) -> D {
213        self.inner
214    }
215}
216
217impl<D, K, S> Stream for AdaptiveDiscovery<D>
218where
219    D: TryStream<Ok = Change<K, S>>,
220{
221    type Item = Result<Change<K, AdaptiveEndpoint<S>>, D::Error>;
222
223    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
224        let this = self.project();
225        this.inner.try_poll_next(cx).map(|change| {
226            change.map(|result| {
227                result.map(|change| match change {
228                    Change::Insert(key, service) => {
229                        Change::Insert(key, AdaptiveEndpoint::new(service, this.config.clone()))
230                    }
231                    Change::Remove(key) => Change::Remove(key),
232                })
233            })
234        })
235    }
236}
237
238impl<S> Load for AdaptiveEndpoint<S> {
239    type Metric = LoadMetric;
240
241    fn load(&self) -> Self::Metric {
242        self.load_metric()
243    }
244}
245
246impl<S, Request> Service<Request> for AdaptiveEndpoint<S>
247where
248    S: Service<Request> + Send + 'static,
249    S::Future: Send + 'static,
250    Request: Send + 'static,
251{
252    type Response = S::Response;
253    type Error = S::Error;
254    type Future = ResponseFuture<S::Response, S::Error>;
255
256    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
257        let mut state = lock(&self.shared.controller);
258        match state.poll_admission(cx) {
259            Poll::Ready(()) => Poll::Ready(Ok(())),
260            Poll::Pending => Poll::Pending,
261        }
262    }
263
264    fn call(&mut self, request: Request) -> Self::Future {
265        // With no endpoint clones, nothing can add another reservation between
266        // this service reporting readiness and receiving the corresponding
267        // call. Tower permits a panic if callers skip `poll_ready`.
268        let reservation = lock(&self.shared.controller).controller.reserve(now());
269        let reservation =
270            reservation.expect("AdaptiveEndpoint::call invoked without available readiness");
271
272        let pending = PendingRequest::new(Arc::clone(&self.shared), reservation);
273        ResponseFuture {
274            inner: Box::pin(pending.execute(request)),
275        }
276    }
277}
278
279/// The future returned by [`AdaptiveEndpoint::call`].
280pub struct ResponseFuture<T, E> {
281    inner: Pin<Box<dyn Future<Output = Result<T, E>> + Send + 'static>>,
282}
283
284impl<T, E> Future for ResponseFuture<T, E> {
285    type Output = Result<T, E>;
286
287    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
288        self.get_mut().inner.as_mut().poll(cx)
289    }
290}
291
292enum RequestState {
293    Queued(DispatchReservation),
294    InFlight(InFlightRequest),
295    Finished,
296}
297
298struct PendingRequest<S> {
299    shared: Arc<Shared<S>>,
300    state: RequestState,
301}
302
303impl<S> PendingRequest<S> {
304    fn new(shared: Arc<Shared<S>>, reservation: DispatchReservation) -> Self {
305        Self {
306            shared,
307            state: RequestState::Queued(reservation),
308        }
309    }
310
311    fn reservation(&self) -> DispatchReservation {
312        match &self.state {
313            RequestState::Queued(reservation) => *reservation,
314            RequestState::InFlight(_) | RequestState::Finished => {
315                panic!("only a queued request has a dispatch reservation")
316            }
317        }
318    }
319
320    fn mark_dispatched(&mut self, active: InFlightRequest) {
321        // The controller has already removed the request from its virtual
322        // queue, so the next admitted caller observes the updated capacity.
323        self.state = RequestState::InFlight(active);
324    }
325
326    fn finish(&mut self, outcome: Outcome, now: Instant) {
327        match std::mem::replace(&mut self.state, RequestState::Finished) {
328            RequestState::InFlight(active) => {
329                let latency = now.saturating_duration_since(active.dispatched_at());
330                lock(&self.shared.controller)
331                    .controller
332                    .on_complete(active, outcome, latency, now);
333            }
334            RequestState::Queued(reservation) => {
335                let admission_waker = {
336                    let mut state = lock(&self.shared.controller);
337                    state.controller.cancel(reservation, now);
338                    state.take_admission_waker()
339                };
340                if let Some(waker) = admission_waker {
341                    waker.wake();
342                }
343            }
344            RequestState::Finished => return,
345        }
346        self.shared.dispatch.notify_waiters();
347    }
348
349    async fn wait_until_dispatchable(&self) {
350        let reservation = self.reservation();
351        loop {
352            let notified = self.shared.dispatch.notified();
353            let mut notified = std::pin::pin!(notified);
354            // Register before inspecting controller state so a transition
355            // between the check and the await cannot be lost.
356            notified.as_mut().enable();
357
358            let (state, probe_changed) = {
359                let mut shared_state = lock(&self.shared.controller);
360                let previous_probe = shared_state.controller.active_probe();
361                let state = shared_state.controller.dispatch_state(reservation, now());
362                (
363                    state,
364                    previous_probe != shared_state.controller.active_probe(),
365                )
366            };
367            if probe_changed {
368                self.shared.dispatch.notify_waiters();
369            }
370
371            match state {
372                DispatchState::Ready => return,
373                DispatchState::WaitUntil(deadline) => {
374                    let delay = deadline.saturating_duration_since(now());
375                    let _ = tokio::time::timeout(delay, notified.as_mut()).await;
376                }
377                DispatchState::WaitForPrevious | DispatchState::InflightLimit => {
378                    notified.as_mut().await;
379                }
380                DispatchState::Cancelled => {
381                    panic!("a live request reservation was cancelled externally")
382                }
383            }
384        }
385    }
386
387    async fn start<Request>(&mut self, request: Request) -> Result<S::Future, S::Error>
388    where
389        S: Service<Request>,
390    {
391        let shared = Arc::clone(&self.shared);
392        let mut service = shared.service.lock().await;
393        std::future::poll_fn(|cx| service.poll_ready(cx)).await?;
394
395        // Controller state may have changed while the inner service was
396        // becoming ready. Keep its readiness claim and wait for the revised
397        // pacing deadline instead of assuming the earlier decision is stable.
398        loop {
399            let (dispatch, admission_waker) = {
400                let mut state = lock(&self.shared.controller);
401                let dispatch = state.controller.on_dispatched(self.reservation(), now());
402                let admission_waker = if dispatch.is_ok() {
403                    state.take_admission_waker()
404                } else {
405                    None
406                };
407                (dispatch, admission_waker)
408            };
409            if let Some(waker) = admission_waker {
410                waker.wake();
411            }
412            match dispatch {
413                Ok(active) => {
414                    self.mark_dispatched(active);
415                    break;
416                }
417                Err(_) => self.wait_until_dispatchable().await,
418            }
419        }
420        // Committing the FIFO head changes the next reservation's deadline.
421        self.shared.dispatch.notify_waiters();
422
423        Ok(service.call(request))
424    }
425
426    async fn execute<Request>(mut self, request: Request) -> Result<S::Response, S::Error>
427    where
428        S: Service<Request>,
429    {
430        self.wait_until_dispatchable().await;
431
432        let response = match self.start(request).await {
433            Ok(response) => response,
434            Err(error) => {
435                lock(&self.shared.controller)
436                    .controller
437                    .on_admission_failure(now());
438                self.finish(Outcome::Failure, now());
439                return Err(error);
440            }
441        };
442        let response = response.await;
443        let outcome = if response.is_ok() {
444            Outcome::Success
445        } else {
446            Outcome::Failure
447        };
448        self.finish(outcome, now());
449        response
450    }
451}
452
453impl<S> Drop for PendingRequest<S> {
454    fn drop(&mut self) {
455        self.finish(Outcome::Failure, now());
456    }
457}