1use super::reload_generation::{HttpRoutingGenerationsSnapshot, RoutingGeneration, RoutingTable};
14use super::RuntimeHttpHost;
15use axum::body::Body;
16use axum::extract::{Request, State};
17use axum::http::{Response, StatusCode};
18use axum::routing::any;
19use axum::Router;
20use std::fmt;
21use std::io;
22use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
23use std::sync::Arc;
24use std::time::{Duration, Instant};
25use tower::ServiceExt;
26
27const ROUTING_RETRY_DELAY: Duration = Duration::from_millis(1);
28const MAX_RELOAD_TIMEOUT: Duration = Duration::from_secs(60);
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum HttpReloadPhase {
33 Prepare,
35 Switch,
37 Drain,
39 Rollback,
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub struct HttpReloadPolicy {
46 health_timeout: Duration,
47 drain_timeout: Duration,
48}
49
50impl HttpReloadPolicy {
51 pub fn new(
53 health_timeout: Duration,
54 drain_timeout: Duration,
55 ) -> Result<Self, RuntimeHttpReloadError> {
56 if health_timeout.is_zero()
57 || drain_timeout.is_zero()
58 || health_timeout > MAX_RELOAD_TIMEOUT
59 || drain_timeout > MAX_RELOAD_TIMEOUT
60 {
61 return Err(RuntimeHttpReloadError::InvalidPolicy);
62 }
63 Ok(Self {
64 health_timeout,
65 drain_timeout,
66 })
67 }
68
69 pub fn health_timeout(self) -> Duration {
71 self.health_timeout
72 }
73
74 pub fn drain_timeout(self) -> Duration {
76 self.drain_timeout
77 }
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub struct HttpReloadSnapshot {
83 pub active_generation: u64,
85 pub active_inflight: usize,
87 pub reload_in_progress: bool,
89 pub successful_reloads: u64,
91 pub failed_reloads: u64,
93 pub rollbacks: u64,
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub enum RuntimeHttpReloadError {
100 ListenerDisabled,
102 ListenerAddressChanged,
104 AuthenticationPolicyChanged,
106 StaleGeneration,
108 ReloadInProgress,
110 RetiringGenerationBusy,
112 InvalidPolicy,
114 HealthGateFailed(HttpReloadPhase),
116 DrainTimedOut,
118 RollbackDrainTimedOut,
120}
121
122impl fmt::Display for RuntimeHttpReloadError {
123 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
124 match self {
125 Self::ListenerDisabled => formatter.write_str("HTTP reload listener is disabled"),
126 Self::ListenerAddressChanged => {
127 formatter.write_str("HTTP reload requires a prepared listener generation")
128 }
129 Self::AuthenticationPolicyChanged => {
130 formatter.write_str("HTTP reload cannot change the authentication boundary")
131 }
132 Self::StaleGeneration => formatter.write_str("HTTP routing generation must increase"),
133 Self::ReloadInProgress => formatter.write_str("HTTP reload is already in progress"),
134 Self::RetiringGenerationBusy => {
135 formatter.write_str("HTTP routing generation is still draining")
136 }
137 Self::InvalidPolicy => formatter.write_str("HTTP reload policy is invalid"),
138 Self::HealthGateFailed(phase) => {
139 write!(formatter, "HTTP reload health gate failed during {phase:?}")
140 }
141 Self::DrainTimedOut => formatter.write_str("HTTP routing generation drain timed out"),
142 Self::RollbackDrainTimedOut => {
143 formatter.write_str("HTTP rollback generation drain timed out")
144 }
145 }
146 }
147}
148
149impl std::error::Error for RuntimeHttpReloadError {}
150
151pub struct PreparedRuntimeHttpGeneration {
153 generation: Arc<RoutingGeneration>,
154}
155
156impl fmt::Debug for PreparedRuntimeHttpGeneration {
157 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
158 formatter
159 .debug_struct("PreparedRuntimeHttpGeneration")
160 .field("generation", &self.generation.id())
161 .finish_non_exhaustive()
162 }
163}
164
165pub struct ReloadableRuntimeHttpHost {
167 config: super::HttpApiConfig,
168 authentication_required: bool,
169 routing: Arc<RoutingTable>,
170 reload_in_progress: AtomicBool,
171 successful_reloads: AtomicU64,
172 failed_reloads: AtomicU64,
173 rollbacks: AtomicU64,
174}
175
176impl ReloadableRuntimeHttpHost {
177 pub fn new(
179 initial_generation: u64,
180 host: RuntimeHttpHost,
181 ) -> Result<Self, RuntimeHttpReloadError> {
182 if !host.config().enabled {
183 return Err(RuntimeHttpReloadError::ListenerDisabled);
184 }
185 if initial_generation == 0 {
186 return Err(RuntimeHttpReloadError::StaleGeneration);
187 }
188 let config = host.config().clone();
189 let authentication_required = host.authentication_required();
190 Ok(Self {
191 config,
192 authentication_required,
193 routing: Arc::new(RoutingTable::new(initial_generation, host.router())),
194 reload_in_progress: AtomicBool::new(false),
195 successful_reloads: AtomicU64::new(0),
196 failed_reloads: AtomicU64::new(0),
197 rollbacks: AtomicU64::new(0),
198 })
199 }
200
201 pub fn prepare(
203 &self,
204 generation: u64,
205 host: RuntimeHttpHost,
206 ) -> Result<PreparedRuntimeHttpGeneration, RuntimeHttpReloadError> {
207 let active = self.routing.active();
208 if generation <= active.id() {
209 return Err(RuntimeHttpReloadError::StaleGeneration);
210 }
211 if !host.config().enabled {
212 return Err(RuntimeHttpReloadError::ListenerDisabled);
213 }
214 if host.config().host != self.config.host || host.config().port != self.config.port {
215 return Err(RuntimeHttpReloadError::ListenerAddressChanged);
216 }
217 if host.authentication_required() != self.authentication_required {
218 return Err(RuntimeHttpReloadError::AuthenticationPolicyChanged);
219 }
220 Ok(PreparedRuntimeHttpGeneration {
221 generation: self.routing.generation(generation, host.router()),
222 })
223 }
224
225 #[cfg(test)]
226 pub(super) fn new_for_test(
227 initial_generation: u64,
228 config: super::HttpApiConfig,
229 router: Router,
230 ) -> Self {
231 Self {
232 config,
233 authentication_required: true,
234 routing: Arc::new(RoutingTable::new(initial_generation, router)),
235 reload_in_progress: AtomicBool::new(false),
236 successful_reloads: AtomicU64::new(0),
237 failed_reloads: AtomicU64::new(0),
238 rollbacks: AtomicU64::new(0),
239 }
240 }
241
242 #[cfg(test)]
243 pub(super) fn prepare_router_for_test(
244 &self,
245 generation: u64,
246 router: Router,
247 ) -> PreparedRuntimeHttpGeneration {
248 PreparedRuntimeHttpGeneration {
249 generation: self.routing.generation(generation, router),
250 }
251 }
252
253 pub fn router(&self) -> Router {
255 dynamic_router(Arc::clone(&self.routing))
256 }
257
258 pub fn run_until_shutdown(&self, shutdown: Arc<AtomicBool>) -> io::Result<()> {
260 let address = format!("{}:{}", self.config.host, self.config.port);
261 let runtime = super::build_http_runtime()?;
262 runtime.block_on(async move {
263 let listener = tokio::net::TcpListener::bind(address).await?;
264 super::validate_listener_auth_boundary(&listener, self.authentication_required)?;
265 serve_listener(listener, self.router(), shutdown).await
266 })
267 }
268
269 pub fn run_on_listener_until_shutdown(
271 &self,
272 listener: std::net::TcpListener,
273 shutdown: Arc<AtomicBool>,
274 ) -> io::Result<()> {
275 listener.set_nonblocking(true)?;
276 let router = self.router();
277 let runtime = super::build_http_runtime()?;
278 runtime.block_on(async move {
279 let listener = tokio::net::TcpListener::from_std(listener)?;
280 super::validate_listener_auth_boundary(&listener, self.authentication_required)?;
281 serve_listener(listener, router, shutdown).await
282 })
283 }
284
285 pub async fn reload(
287 &self,
288 prepared: PreparedRuntimeHttpGeneration,
289 policy: HttpReloadPolicy,
290 ) -> Result<(), RuntimeHttpReloadError> {
291 let mut guard = match ReloadGuard::acquire(
292 &self.reload_in_progress,
293 &self.routing,
294 &self.failed_reloads,
295 &self.rollbacks,
296 ) {
297 Ok(guard) => guard,
298 Err(error) => return self.fail(error),
299 };
300 self.routing.release_drained_retiring();
301 if self.routing.generations_snapshot().retiring.is_some() {
302 return self.fail(RuntimeHttpReloadError::RetiringGenerationBusy);
303 }
304 let previous = self.routing.active();
305 if prepared.generation.id() <= previous.id() {
306 return self.fail(RuntimeHttpReloadError::StaleGeneration);
307 }
308 if !probe_health(prepared.generation.router(), policy.health_timeout).await {
309 return self.fail(RuntimeHttpReloadError::HealthGateFailed(
310 HttpReloadPhase::Prepare,
311 ));
312 }
313
314 previous.stop_accepting();
315 if !self.routing.retire(Arc::clone(&previous)) {
316 previous.start_accepting();
317 return self.fail(RuntimeHttpReloadError::RetiringGenerationBusy);
318 }
319 guard.arm(Arc::clone(&previous), Arc::clone(&prepared.generation));
320 self.routing.activate(Arc::clone(&prepared.generation));
321 if !probe_health(prepared.generation.router(), policy.health_timeout).await {
322 guard.disarm();
323 return self
324 .rollback(previous, prepared.generation, policy)
325 .await
326 .and(Err(RuntimeHttpReloadError::HealthGateFailed(
327 HttpReloadPhase::Switch,
328 )));
329 }
330 if !wait_for_drain(&previous, policy.drain_timeout).await {
331 guard.disarm();
332 return self
333 .rollback(previous, prepared.generation, policy)
334 .await
335 .and(Err(RuntimeHttpReloadError::DrainTimedOut));
336 }
337 self.routing.release_retiring(previous.id());
338 guard.disarm();
339 increment(&self.successful_reloads);
340 Ok(())
341 }
342
343 pub fn snapshot(&self) -> HttpReloadSnapshot {
345 let active = self.routing.active();
346 HttpReloadSnapshot {
347 active_generation: active.id(),
348 active_inflight: active.inflight(),
349 reload_in_progress: self.reload_in_progress.load(Ordering::Acquire),
350 successful_reloads: self.successful_reloads.load(Ordering::Relaxed),
351 failed_reloads: self.failed_reloads.load(Ordering::Relaxed),
352 rollbacks: self.rollbacks.load(Ordering::Relaxed),
353 }
354 }
355
356 pub fn generation_snapshot(&self) -> HttpRoutingGenerationsSnapshot {
358 self.routing.generations_snapshot()
359 }
360
361 async fn rollback(
362 &self,
363 previous: Arc<RoutingGeneration>,
364 failed: Arc<RoutingGeneration>,
365 policy: HttpReloadPolicy,
366 ) -> Result<(), RuntimeHttpReloadError> {
367 failed.stop_accepting();
368 previous.start_accepting();
369 self.routing.activate(Arc::clone(&previous));
370 self.routing.release_retiring(previous.id());
371 if !self.routing.retire(Arc::clone(&failed)) {
372 return Err(RuntimeHttpReloadError::RetiringGenerationBusy);
373 }
374 increment(&self.rollbacks);
375 increment(&self.failed_reloads);
376 if wait_for_drain(&failed, policy.drain_timeout).await {
377 self.routing.release_retiring(failed.id());
378 Ok(())
379 } else {
380 Err(RuntimeHttpReloadError::RollbackDrainTimedOut)
381 }
382 }
383
384 fn fail<T>(&self, error: RuntimeHttpReloadError) -> Result<T, RuntimeHttpReloadError> {
385 increment(&self.failed_reloads);
386 Err(error)
387 }
388}
389
390struct ReloadGuard<'a> {
391 flag: &'a AtomicBool,
392 routing: &'a RoutingTable,
393 failed_reloads: &'a AtomicU64,
394 rollbacks: &'a AtomicU64,
395 rollback: Option<(Arc<RoutingGeneration>, Arc<RoutingGeneration>)>,
396}
397
398impl<'a> ReloadGuard<'a> {
399 fn acquire(
400 flag: &'a AtomicBool,
401 routing: &'a RoutingTable,
402 failed_reloads: &'a AtomicU64,
403 rollbacks: &'a AtomicU64,
404 ) -> Result<Self, RuntimeHttpReloadError> {
405 flag.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
406 .map_err(|_| RuntimeHttpReloadError::ReloadInProgress)?;
407 Ok(Self {
408 flag,
409 routing,
410 failed_reloads,
411 rollbacks,
412 rollback: None,
413 })
414 }
415
416 fn arm(&mut self, previous: Arc<RoutingGeneration>, candidate: Arc<RoutingGeneration>) {
417 self.rollback = Some((previous, candidate));
418 }
419
420 fn disarm(&mut self) {
421 self.rollback = None;
422 }
423}
424
425impl Drop for ReloadGuard<'_> {
426 fn drop(&mut self) {
427 if let Some((previous, failed)) = self.rollback.take() {
428 failed.stop_accepting();
429 previous.start_accepting();
430 self.routing.activate(Arc::clone(&previous));
431 self.routing.release_retiring(previous.id());
432 if self.routing.retire(failed) {
433 increment(self.rollbacks);
434 increment(self.failed_reloads);
435 }
436 }
437 self.routing.release_drained_retiring();
438 self.flag.store(false, Ordering::Release);
439 }
440}
441
442fn dynamic_router(routing: Arc<RoutingTable>) -> Router {
443 Router::new()
444 .fallback(any(dispatch_active_generation))
445 .with_state(routing)
446}
447
448async fn dispatch_active_generation(
449 State(routing): State<Arc<RoutingTable>>,
450 request: Request,
451) -> Response<Body> {
452 loop {
453 let generation = routing.active();
454 if let Some(_permit) = generation.try_admit() {
455 return generation
456 .router()
457 .clone()
458 .oneshot(request)
459 .await
460 .unwrap_or_else(|never| match never {});
461 }
462 tokio::time::sleep(ROUTING_RETRY_DELAY).await;
463 }
464}
465
466async fn probe_health(router: &Router, timeout: Duration) -> bool {
467 let request = Request::get("/v1/health").body(Body::empty());
468 let Ok(request) = request else {
469 return false;
470 };
471 tokio::time::timeout(timeout, router.clone().oneshot(request))
472 .await
473 .ok()
474 .and_then(Result::ok)
475 .is_some_and(|response| response.status() == StatusCode::OK)
476}
477
478async fn wait_for_drain(generation: &RoutingGeneration, timeout: Duration) -> bool {
479 let deadline = Instant::now() + timeout;
480 loop {
481 if generation.inflight() == 0 {
482 return true;
483 }
484 if Instant::now() >= deadline {
485 return false;
486 }
487 tokio::time::sleep(ROUTING_RETRY_DELAY).await;
488 }
489}
490
491async fn serve_listener(
492 listener: tokio::net::TcpListener,
493 router: Router,
494 shutdown: Arc<AtomicBool>,
495) -> io::Result<()> {
496 axum::serve(super::connection::with_read_timeout(listener), router)
497 .with_graceful_shutdown(super::wait_for_shutdown(shutdown))
498 .await
499}
500
501fn increment(counter: &AtomicU64) {
502 let _ = counter.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
503 Some(current.saturating_add(1))
504 });
505}