vector/sinks/util/service/
health.rs1use std::{
2 future::Future,
3 pin::Pin,
4 sync::{
5 Arc,
6 atomic::{AtomicUsize, Ordering},
7 },
8 task::{Context, Poll, ready},
9};
10
11use futures::FutureExt;
12use futures_util::{TryFuture, future::BoxFuture};
13use pin_project::pin_project;
14use serde_with::serde_as;
15use stream_cancel::{Trigger, Tripwire};
16use tokio::time::{Duration, sleep};
17use tower::Service;
18use vector_lib::{configurable::configurable_component, emit};
19
20use crate::{
21 common::backoff::ExponentialBackoff,
22 internal_events::{EndpointsActive, OpenGauge},
23};
24
25const RETRY_MAX_DURATION_SECONDS_DEFAULT: u64 = 3_600;
26const RETRY_INITIAL_BACKOFF_SECONDS_DEFAULT: u64 = 1;
27const UNHEALTHY_AMOUNT_OF_ERRORS: usize = 5;
28
29#[serde_as]
31#[configurable_component]
32#[derive(Clone, Debug)]
33#[serde(rename_all = "snake_case")]
34pub struct HealthConfig {
35 #[serde(default = "default_retry_initial_backoff_secs")]
37 #[configurable(metadata(docs::type_unit = "seconds"))]
38 #[configurable(metadata(docs::human_name = "Retry Initial Backoff"))]
40 pub retry_initial_backoff_secs: u64,
41
42 #[serde_as(as = "serde_with::DurationSeconds<u64>")]
44 #[serde(default = "default_retry_max_duration_secs")]
45 #[configurable(metadata(docs::human_name = "Max Retry Duration"))]
46 pub retry_max_duration_secs: Duration,
47}
48
49const fn default_retry_initial_backoff_secs() -> u64 {
50 RETRY_INITIAL_BACKOFF_SECONDS_DEFAULT
51}
52
53const fn default_retry_max_duration_secs() -> std::time::Duration {
54 Duration::from_secs(RETRY_MAX_DURATION_SECONDS_DEFAULT)
55}
56
57impl Default for HealthConfig {
58 fn default() -> Self {
59 Self {
60 retry_initial_backoff_secs: default_retry_initial_backoff_secs(),
61 retry_max_duration_secs: default_retry_max_duration_secs(),
62 }
63 }
64}
65
66impl HealthConfig {
67 pub fn build<S, L>(
68 &self,
69 logic: L,
70 inner: S,
71 open: OpenGauge,
72 endpoint: String,
73 ) -> HealthService<S, L> {
74 let counters = Arc::new(HealthCounters::new());
75 let snapshot = counters.snapshot();
76
77 open.clone().open(emit_active_endpoints);
78 HealthService {
79 inner,
80 logic,
81 counters,
82 snapshot,
83 endpoint,
84 state: CircuitState::Closed,
85 open,
86 backoff: ExponentialBackoff::default()
89 .factor((self.retry_initial_backoff_secs.saturating_mul(1000) / 2).max(1))
90 .max_delay(self.retry_max_duration_secs),
91 }
92 }
93}
94
95pub trait HealthLogic: Clone + Send + Sync + 'static {
96 type Error: Send + Sync + 'static;
97 type Response;
98
99 fn is_healthy(&self, response: &Result<Self::Response, Self::Error>) -> Option<bool>;
102}
103
104enum CircuitState {
105 Open(BoxFuture<'static, ()>),
108
109 HalfOpen {
111 permit: Option<Trigger>,
112 done: Tripwire,
113 },
114
115 Closed,
117}
118
119pub struct HealthService<S, L> {
122 inner: S,
123 logic: L,
124 counters: Arc<HealthCounters>,
125 snapshot: HealthSnapshot,
126 backoff: ExponentialBackoff,
127 state: CircuitState,
128 open: OpenGauge,
129 endpoint: String,
130}
131
132impl<S, L, Req> Service<Req> for HealthService<S, L>
133where
134 L: HealthLogic<Response = S::Response, Error = S::Error>,
135 S: Service<Req>,
136{
137 type Response = S::Response;
138 type Error = S::Error;
139 type Future = HealthFuture<S::Future, L>;
140
141 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
142 loop {
143 self.state = match self.state {
144 CircuitState::Open(ref mut timer) => {
145 ready!(timer.as_mut().poll(cx));
146
147 debug!(message = "Endpoint is on probation.", endpoint = %&self.endpoint);
148
149 let (permit, done) = Tripwire::new();
152
153 CircuitState::HalfOpen {
154 permit: Some(permit),
155 done,
156 }
157 }
158 CircuitState::HalfOpen {
159 permit: Some(_), ..
160 } => {
161 return self.inner.poll_ready(cx).map_err(Into::into);
163 }
164 CircuitState::HalfOpen {
165 permit: None,
166 ref mut done,
167 } => {
168 let done = Pin::new(done);
169 ready!(done.poll(cx));
170
171 if self.counters.healthy(self.snapshot).is_ok() {
172 info!(message = "Endpoint is healthy.", endpoint = %&self.endpoint);
174
175 self.backoff.reset();
176 self.open.clone().open(emit_active_endpoints);
177 CircuitState::Closed
178 } else {
179 debug!(message = "Endpoint failed probation.", endpoint = %&self.endpoint);
180
181 CircuitState::Open(
182 sleep(self.backoff.next().expect("Should never end")).boxed(),
183 )
184 }
185 }
186 CircuitState::Closed => {
187 match self.counters.healthy(self.snapshot) {
189 Ok(snapshot) => {
190 self.snapshot = snapshot;
192 return self.inner.poll_ready(cx).map_err(Into::into);
193 }
194 Err(errors) if errors >= UNHEALTHY_AMOUNT_OF_ERRORS => {
195 warn!(message = "Endpoint is unhealthy.", endpoint = %&self.endpoint);
197 CircuitState::Open(
198 sleep(self.backoff.next().expect("Should never end")).boxed(),
199 )
200 }
201 Err(_) => {
202 return self.inner.poll_ready(cx).map_err(Into::into);
204 }
205 }
206 }
207 }
208 }
209 }
210
211 fn call(&mut self, req: Req) -> Self::Future {
212 let permit = if let CircuitState::HalfOpen { permit, .. } = &mut self.state {
213 permit.take()
214 } else {
215 None
216 };
217
218 HealthFuture {
219 inner: self.inner.call(req),
220 logic: self.logic.clone(),
221 counters: Arc::clone(&self.counters),
222 permit,
223 }
224 }
225}
226
227#[pin_project]
229pub struct HealthFuture<F, L> {
230 #[pin]
231 inner: F,
232 logic: L,
233 counters: Arc<HealthCounters>,
234 permit: Option<Trigger>,
235}
236
237impl<F: TryFuture, L> Future for HealthFuture<F, L>
238where
239 F: Future<Output = Result<F::Ok, F::Error>>,
240 L: HealthLogic<Response = F::Ok, Error = F::Error>,
241{
242 type Output = Result<F::Ok, F::Error>;
243
244 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
245 let this = self.project();
247 let output = ready!(this.inner.poll(cx));
248
249 match this.logic.is_healthy(&output) {
250 None => (),
251 Some(true) => this.counters.inc_healthy(),
252 Some(false) => this.counters.inc_unhealthy(),
253 }
254
255 this.permit.take();
257
258 Poll::Ready(output)
259 }
260}
261
262struct HealthCounters {
264 healthy: AtomicUsize,
265 unhealthy: AtomicUsize,
266}
267
268impl HealthCounters {
269 const fn new() -> Self {
270 HealthCounters {
271 healthy: AtomicUsize::new(0),
272 unhealthy: AtomicUsize::new(0),
273 }
274 }
275
276 fn inc_healthy(&self) {
277 self.healthy.fetch_add(1, Ordering::Release);
278 }
279
280 fn inc_unhealthy(&self) {
281 self.unhealthy.fetch_add(1, Ordering::Release);
282 }
283
284 fn healthy(&self, snapshot: HealthSnapshot) -> Result<HealthSnapshot, usize> {
289 let now = self.snapshot();
290
291 if now.healthy > snapshot.healthy {
293 Ok(now)
295 } else if now.unhealthy > snapshot.unhealthy {
296 Err(now.unhealthy - snapshot.unhealthy)
298 } else {
299 Ok(now)
301 }
302 }
303
304 fn snapshot(&self) -> HealthSnapshot {
305 HealthSnapshot {
306 healthy: self.healthy.load(Ordering::Acquire),
307 unhealthy: self.unhealthy.load(Ordering::Acquire),
308 }
309 }
310}
311
312#[derive(Clone, Copy, Eq, PartialEq, Debug)]
313struct HealthSnapshot {
314 healthy: usize,
315 unhealthy: usize,
316}
317
318fn emit_active_endpoints(count: usize) {
319 emit!(EndpointsActive { count });
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325
326 #[test]
327 fn test_health_counters() {
328 let counters = HealthCounters::new();
329 let mut snapshot = counters.snapshot();
330
331 counters.inc_healthy();
332 snapshot = counters.healthy(snapshot).unwrap();
333
334 counters.inc_unhealthy();
335 counters.inc_unhealthy();
336 assert_eq!(counters.healthy(snapshot), Err(2));
337
338 counters.inc_healthy();
339 assert!(counters.healthy(snapshot).is_ok());
340 }
341
342 #[test]
343 fn health_config_default_matches_deserialize_defaults() {
344 let config = HealthConfig::default();
345
346 assert_eq!(
347 config.retry_initial_backoff_secs,
348 RETRY_INITIAL_BACKOFF_SECONDS_DEFAULT
349 );
350 assert_eq!(
351 config.retry_max_duration_secs,
352 Duration::from_secs(RETRY_MAX_DURATION_SECONDS_DEFAULT)
353 );
354 }
355}