Skip to main content

zebrad/components/sync/
hedge.rs

1//! Preserve a live hedge arm when its sibling exhausts its retries.
2//!
3//! Tower still owns latency sampling, warmup, delay, and histogram rotation. Only its arm
4//! responses are coordinated: an error cannot win while another arm can still succeed.
5//! If every arm fails, preserve an original `NotFound` error in preference to other failures,
6//! regardless of completion order, so sync can re-request the missing block without restarting.
7//!
8//! Readiness errors join this same race without dispatching to the failed service. Downloads
9//! therefore reports them as `DownloadFailed`, rather than `NetworkServiceError`.
10
11use std::{
12    future::Future,
13    pin::Pin,
14    sync::{
15        atomic::{AtomicU8, Ordering},
16        Arc, Mutex,
17    },
18    task::{Context, Poll},
19};
20
21use futures::{ready, task::AtomicWaker};
22use pin_project::pin_project;
23use tower::{hedge, BoxError, Service};
24use zebra_network as zn;
25
26use super::{is_not_found, SYNC_RESTART_DELAY};
27
28#[cfg(test)]
29mod tests;
30
31/// Tower's usual hedge, preferring success over an already-completed sibling error.
32#[derive(Debug)]
33pub(super) struct SuccessHedge<S>(hedge::Hedge<ArmService<S>, HedgePolicy>);
34
35impl<S> SuccessHedge<S>
36where
37    S: Service<zn::Request> + Clone + Send + 'static,
38    S::Future: Send + 'static,
39    S::Error: Into<BoxError>,
40{
41    /// Wrap a retrying service without changing its retry policy or Tower's hedge timing.
42    pub(super) fn new(service: S) -> Self {
43        Self(hedge::Hedge::new(
44            ArmService {
45                inner: service,
46                ready_error: None,
47            },
48            HedgePolicy,
49            20,
50            0.95,
51            2 * SYNC_RESTART_DELAY,
52        ))
53    }
54}
55
56impl<S> Service<zn::Request> for SuccessHedge<S>
57where
58    S: Service<zn::Request> + Clone + Send + 'static,
59    S::Future: Send + 'static,
60    S::Error: Into<BoxError>,
61{
62    type Response = S::Response;
63    type Error = BoxError;
64    type Future = <hedge::Hedge<ArmService<S>, HedgePolicy> as Service<HedgeRequest>>::Future;
65
66    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
67        self.0.poll_ready(cx)
68    }
69
70    fn call(&mut self, request: zn::Request) -> Self::Future {
71        self.0.call(HedgeRequest {
72            request,
73            arm: Arm {
74                state: Arc::new(ArmState {
75                    flags: AtomicU8::new(1),
76                    wakers: [AtomicWaker::new(), AtomicWaker::new()],
77                    error: Mutex::new(None),
78                }),
79                index: 0,
80            },
81        })
82    }
83}
84
85#[derive(Debug)]
86struct ArmState {
87    /// Bits 0 and 1 mark live arms.
88    flags: AtomicU8,
89    wakers: [AtomicWaker; 2],
90    /// The preferred terminal error, consumed once by the outer hedge.
91    error: Mutex<Option<BoxError>>,
92}
93
94/// One non-cloneable owner, first in the request and then in its response future.
95#[derive(Debug)]
96struct Arm {
97    state: Arc<ArmState>,
98    index: usize,
99}
100
101impl Arm {
102    fn finish(&self) {
103        let bit = 1 << self.index;
104        if self.state.flags.fetch_and(!bit, Ordering::AcqRel) & bit != 0 {
105            self.state.wakers[1 - self.index].wake();
106        }
107    }
108}
109
110impl Drop for Arm {
111    fn drop(&mut self) {
112        // Also covers a cold histogram rejecting the clone, an unissued delayed clone, and
113        // cancellation. No Retry clone owns an arm: Retry only receives the raw request below.
114        self.finish();
115    }
116}
117
118/// A request owns its arm even before Tower's delay and readiness checks have completed.
119#[derive(Debug)]
120pub(super) struct HedgeRequest {
121    request: zn::Request,
122    arm: Arm,
123}
124
125/// Clone exactly one additional arm; unlike retry payloads, arm ownership is not cloneable.
126#[derive(Clone, Copy, Debug)]
127pub(super) struct HedgePolicy;
128
129impl hedge::Policy<HedgeRequest> for HedgePolicy {
130    fn clone_request(&self, request: &HedgeRequest) -> Option<HedgeRequest> {
131        let payload = request.request.clone();
132        request.arm.state.flags.fetch_or(2, Ordering::AcqRel);
133        Some(HedgeRequest {
134            request: payload,
135            arm: Arm {
136                state: Arc::clone(&request.arm.state),
137                index: 1,
138            },
139        })
140    }
141
142    fn can_retry(&self, _request: &HedgeRequest) -> bool {
143        true
144    }
145}
146
147/// Keep each entire retry sequence inside one hedge arm.
148#[derive(Debug)]
149pub(super) struct ArmService<S> {
150    inner: S,
151    ready_error: Option<BoxError>,
152}
153
154impl<S: Clone> Clone for ArmService<S> {
155    fn clone(&self) -> Self {
156        // Readiness, including a saved error, belongs to the source instance. Its clone must
157        // poll its own inner readiness; Tower moves the ready source into the delayed future.
158        Self {
159            inner: self.inner.clone(),
160            ready_error: None,
161        }
162    }
163}
164
165impl<S> Service<HedgeRequest> for ArmService<S>
166where
167    S: Service<zn::Request> + Clone + Send + 'static,
168    S::Future: Send + 'static,
169    S::Error: Into<BoxError>,
170{
171    type Response = S::Response;
172    type Error = BoxError;
173    type Future = ArmFuture<S::Future>;
174
175    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
176        if self.ready_error.is_none() {
177            if let Err(error) = ready!(self.inner.poll_ready(cx)) {
178                // Tower's delayed Oneshot must deliver readiness errors through the arm too,
179                // rather than returning early and canceling a still-useful sibling.
180                self.ready_error = Some(error.into());
181                // A failed readiness check can still hold a ConcurrencyLimit reservation.
182                // Drop that service instance without dispatching or discarding its error.
183                self.inner = self.inner.clone();
184            }
185        }
186        Poll::Ready(Ok(()))
187    }
188
189    fn call(&mut self, request: HedgeRequest) -> Self::Future {
190        let error = self.ready_error.take();
191        ArmFuture {
192            inner: error.is_none().then(|| self.inner.call(request.request)),
193            arm: request.arm,
194            error,
195        }
196    }
197}
198
199/// An exhausted arm waits until its sibling finishes or drops, without holding inner capacity.
200#[pin_project]
201#[derive(Debug)]
202pub(super) struct ArmFuture<F> {
203    #[pin]
204    inner: Option<F>,
205    arm: Arm,
206    error: Option<BoxError>,
207}
208
209impl<F, T, E> Future for ArmFuture<F>
210where
211    F: Future<Output = Result<T, E>>,
212    E: Into<BoxError>,
213{
214    type Output = Result<T, BoxError>;
215
216    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
217        let mut this = self.project();
218        if let Some(inner) = this.inner.as_mut().as_pin_mut() {
219            let result = ready!(inner.poll(cx));
220            this.inner.set(None);
221            match result {
222                Ok(response) => {
223                    this.arm.finish();
224                    return Poll::Ready(Ok(response));
225                }
226                Err(error) => *this.error = Some(error.into()),
227            }
228        }
229        if let Some(error) = this.error.take() {
230            let mut preferred = this
231                .arm
232                .state
233                .error
234                .lock()
235                .expect("thread panicked while holding the hedge error mutex");
236            if preferred.is_none() || is_not_found(error.as_ref()) {
237                *preferred = Some(error);
238            }
239        }
240        this.arm.finish();
241        this.arm.state.wakers[this.arm.index].register(cx.waker());
242        if this.arm.state.flags.load(Ordering::Acquire) == 0 {
243            Poll::Ready(Err(this
244                .arm
245                .state
246                .error
247                .lock()
248                .expect("thread panicked while holding the hedge error mutex")
249                .take()
250                .expect("all arms failed before the hedge returns an error")))
251        } else {
252            Poll::Pending
253        }
254    }
255}