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 std::future::Future;
7use std::marker::PhantomData;
8use std::pin::Pin;
9use std::sync::{Arc, Mutex};
10use std::task::{Context, Poll, Waker};
11use std::time::Instant;
12use tower::Service;
13use tower::discover::Change;
14use tower::load::Load;
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    pub fn as_secs(self) -> f64 {
25        self.0
26    }
27}
28
29#[derive(Debug, Default)]
30struct ReadyWaker {
31    wakers: Mutex<Vec<Waker>>,
32}
33
34impl ReadyWaker {
35    fn register(&self, waker: &Waker) {
36        let mut wakers = self.wakers.lock().expect("ready waker mutex poisoned");
37        if !wakers.iter().any(|existing| existing.will_wake(waker)) {
38            wakers.push(waker.clone());
39        }
40    }
41
42    fn wake(&self) {
43        let wakers = self
44            .wakers
45            .lock()
46            .expect("ready waker mutex poisoned")
47            .drain(..)
48            .collect::<Vec<_>>();
49        for waker in wakers {
50            waker.wake();
51        }
52    }
53}
54
55struct Shared<S> {
56    inner: tokio::sync::Mutex<S>,
57    controller: Mutex<EndpointController>,
58    readiness_reservations: Mutex<usize>,
59    ready: ReadyWaker,
60    dispatch: tokio::sync::Notify,
61}
62
63/// A Tower service with per-endpoint adaptive pacing and bounded admission.
64///
65/// `poll_ready` reports whether another request can enter the endpoint's
66/// bounded scheduling horizon. `call` reserves a virtual GCRA slot; the
67/// returned future waits until that slot is due, waits for the inner service to
68/// be ready, and only then records the actual dispatch. Queue delay therefore
69/// never contaminates the RTT sample.
70pub struct AdaptiveEndpoint<S> {
71    shared: Arc<Shared<S>>,
72    readiness_reserved: bool,
73}
74
75impl<S> AdaptiveEndpoint<S> {
76    pub fn new(inner: S, config: EndpointConfig) -> Self {
77        Self::new_at(inner, config, Instant::now())
78    }
79
80    pub fn new_at(inner: S, config: EndpointConfig, now: Instant) -> Self {
81        Self {
82            shared: Arc::new(Shared {
83                inner: tokio::sync::Mutex::new(inner),
84                controller: Mutex::new(EndpointController::new(config, now)),
85                readiness_reservations: Mutex::new(0),
86                ready: ReadyWaker::default(),
87                dispatch: tokio::sync::Notify::new(),
88            }),
89            readiness_reserved: false,
90        }
91    }
92
93    pub fn controller(&self) -> &Mutex<EndpointController> {
94        &self.shared.controller
95    }
96
97    pub fn snapshot(&self) -> loadpace::ControllerSnapshot {
98        self.shared
99            .controller
100            .lock()
101            .expect("controller mutex poisoned")
102            .snapshot(Instant::now())
103    }
104
105    pub fn load_metric(&self) -> LoadMetric {
106        let mut controller = self
107            .shared
108            .controller
109            .lock()
110            .expect("controller mutex poisoned");
111        let now = Instant::now();
112        controller.refresh(now);
113        LoadMetric(controller.load(now))
114    }
115}
116
117/// Maps a Tower discovery stream into freshly initialized adaptive endpoints.
118///
119/// The wrapper intentionally creates new controller state for every insert.
120/// This is the safe behavior when discovery removes and later reuses an
121/// endpoint key; state retention can be added without changing the discovery
122/// contract once churn behavior is better understood.
123pub struct AdaptiveDiscovery<D, Request> {
124    inner: D,
125    config: EndpointConfig,
126    _request: PhantomData<fn() -> Request>,
127}
128
129impl<D, Request> AdaptiveDiscovery<D, Request> {
130    pub fn new(inner: D, config: EndpointConfig) -> Self {
131        Self {
132            inner,
133            config,
134            _request: PhantomData,
135        }
136    }
137
138    pub fn into_inner(self) -> D {
139        self.inner
140    }
141}
142
143impl<D, Request, K, S> Stream for AdaptiveDiscovery<D, Request>
144where
145    D: TryStream<Ok = Change<K, S>> + Unpin,
146    K: Eq,
147    S: Service<Request> + Send + 'static,
148    S::Future: Send + 'static,
149    S::Response: Send + 'static,
150    S::Error: Send + 'static,
151    Request: 'static,
152{
153    type Item = Result<Change<K, AdaptiveEndpoint<S>>, D::Error>;
154
155    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
156        let this = self.get_mut();
157        Pin::new(&mut this.inner).try_poll_next(cx).map(|change| {
158            change.map(|result| {
159                result.map(|change| match change {
160                    Change::Insert(key, service) => {
161                        Change::Insert(key, AdaptiveEndpoint::new(service, this.config.clone()))
162                    }
163                    Change::Remove(key) => Change::Remove(key),
164                })
165            })
166        })
167    }
168}
169
170impl<S> Clone for AdaptiveEndpoint<S> {
171    fn clone(&self) -> Self {
172        Self {
173            shared: Arc::clone(&self.shared),
174            readiness_reserved: false,
175        }
176    }
177}
178
179impl<S> Drop for AdaptiveEndpoint<S> {
180    fn drop(&mut self) {
181        if self.readiness_reserved {
182            let mut reservations = self
183                .shared
184                .readiness_reservations
185                .lock()
186                .expect("readiness mutex poisoned");
187            *reservations -= 1;
188            self.shared.ready.wake();
189        }
190    }
191}
192
193impl<S> Load for AdaptiveEndpoint<S> {
194    type Metric = LoadMetric;
195
196    fn load(&self) -> Self::Metric {
197        self.load_metric()
198    }
199}
200
201impl<S, Request> Service<Request> for AdaptiveEndpoint<S>
202where
203    S: Service<Request> + Send + 'static,
204    S::Future: Send + 'static,
205    S::Response: Send + 'static,
206    S::Error: Send + 'static,
207    Request: Send + 'static,
208{
209    type Response = S::Response;
210    type Error = S::Error;
211    type Future = ResponseFuture<S::Response, S::Error>;
212
213    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
214        if self.readiness_reserved {
215            return Poll::Ready(Ok(()));
216        }
217
218        self.shared.ready.register(cx.waker());
219
220        // Recheck after registering to avoid missing a completion/cancellation
221        // that raced with the first check.
222        let available = {
223            let mut reservations = self
224                .shared
225                .readiness_reservations
226                .lock()
227                .expect("readiness mutex poisoned");
228            let mut controller = self
229                .shared
230                .controller
231                .lock()
232                .expect("controller mutex poisoned");
233            controller.refresh(Instant::now());
234            let available =
235                *reservations + controller.queued() < controller.config().queue_capacity;
236            if available {
237                *reservations += 1;
238            }
239            available
240        };
241        if available {
242            self.readiness_reserved = true;
243            Poll::Ready(Ok(()))
244        } else {
245            Poll::Pending
246        }
247    }
248
249    fn call(&mut self, request: Request) -> Self::Future {
250        assert!(
251            self.readiness_reserved,
252            "AdaptiveEndpoint::call invoked without available readiness"
253        );
254        self.readiness_reserved = false;
255        *self
256            .shared
257            .readiness_reservations
258            .lock()
259            .expect("readiness mutex poisoned") -= 1;
260        let reservation = self
261            .shared
262            .controller
263            .lock()
264            .expect("controller mutex poisoned")
265            .reserve(Instant::now())
266            .expect("readiness reservation was not reflected in controller capacity");
267
268        let guard = RequestGuard::new(Arc::clone(&self.shared), reservation);
269        let future = dispatch_request(Arc::clone(&self.shared), request, guard);
270        ResponseFuture {
271            inner: Box::pin(future),
272        }
273    }
274}
275
276/// The future returned by [`AdaptiveEndpoint::call`].
277pub struct ResponseFuture<T, E> {
278    inner: Pin<Box<dyn Future<Output = Result<T, E>> + Send + 'static>>,
279}
280
281impl<T, E> Future for ResponseFuture<T, E> {
282    type Output = Result<T, E>;
283
284    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
285        // `ResponseFuture` does not move after being pinned, and the boxed
286        // future is itself pinned.
287        unsafe { self.get_unchecked_mut() }.inner.as_mut().poll(cx)
288    }
289}
290
291struct RequestGuard<S> {
292    shared: Arc<Shared<S>>,
293    reservation: Option<DispatchReservation>,
294    active: Option<InFlightRequest>,
295    finished: bool,
296}
297
298impl<S> RequestGuard<S> {
299    fn new(shared: Arc<Shared<S>>, reservation: DispatchReservation) -> Self {
300        Self {
301            shared,
302            reservation: Some(reservation),
303            active: None,
304            finished: false,
305        }
306    }
307
308    fn mark_dispatched(&mut self, active: InFlightRequest) {
309        self.reservation = None;
310        self.active = Some(active);
311    }
312
313    fn finish(&mut self, outcome: Outcome, now: Instant) {
314        if self.finished {
315            return;
316        }
317        self.finished = true;
318        if let Some(active) = self.active.take() {
319            let latency = now.saturating_duration_since(active.dispatched_at());
320            self.shared
321                .controller
322                .lock()
323                .expect("controller mutex poisoned")
324                .on_complete(active, outcome, latency, now);
325        } else if let Some(reservation) = self.reservation.take() {
326            self.shared
327                .controller
328                .lock()
329                .expect("controller mutex poisoned")
330                .cancel(reservation, now);
331        }
332        self.shared.dispatch.notify_waiters();
333        self.shared.ready.wake();
334    }
335}
336
337impl<S> Drop for RequestGuard<S> {
338    fn drop(&mut self) {
339        if !self.finished {
340            self.finish(Outcome::Failure, Instant::now());
341        }
342    }
343}
344
345async fn dispatch_request<S, Request>(
346    shared: Arc<Shared<S>>,
347    request: Request,
348    mut guard: RequestGuard<S>,
349) -> Result<S::Response, S::Error>
350where
351    S: Service<Request> + Send + 'static,
352    S::Future: Send + 'static,
353    S::Response: Send + 'static,
354    S::Error: Send + 'static,
355    Request: Send + 'static,
356{
357    let reservation = guard
358        .reservation
359        .expect("request guard must begin with a reservation");
360
361    loop {
362        let notified = shared.dispatch.notified();
363        let decision = {
364            let mut controller = shared.controller.lock().expect("controller mutex poisoned");
365            let now = Instant::now();
366            controller.refresh(now);
367            controller.dispatch_state(reservation, now)
368        };
369
370        match decision {
371            DispatchState::Ready => {
372                break;
373            }
374            DispatchState::WaitUntil(deadline) => {
375                let delay = deadline.saturating_duration_since(Instant::now());
376                tokio::select! {
377                    _ = tokio::time::sleep(delay) => {},
378                    _ = notified => {},
379                }
380            }
381            DispatchState::WaitForPrevious | DispatchState::InflightLimit => {
382                notified.await;
383            }
384            DispatchState::Cancelled => {
385                panic!("an AdaptiveEndpoint request was cancelled while being polled");
386            }
387        }
388    }
389
390    let result = {
391        let mut inner = shared.inner.lock().await;
392        match std::future::poll_fn(|cx| inner.poll_ready(cx)).await {
393            Ok(()) => {
394                let now = Instant::now();
395                let active = shared
396                    .controller
397                    .lock()
398                    .expect("controller mutex poisoned")
399                    .on_dispatched(reservation, now)
400                    .expect("dispatch state changed unexpectedly");
401                guard.mark_dispatched(active);
402                let future = inner.call(request);
403                drop(inner);
404                future.await
405            }
406            Err(error) => {
407                shared
408                    .controller
409                    .lock()
410                    .expect("controller mutex poisoned")
411                    .on_admission_failure(Instant::now());
412                Err(error)
413            }
414        }
415    };
416
417    let outcome = if result.is_ok() {
418        Outcome::Success
419    } else {
420        Outcome::Failure
421    };
422    let now = Instant::now();
423    guard.finish(outcome, now);
424    result
425}