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