1use crate::bus::BusManager;
2use crate::cache::EntityCache;
3use crate::config::ServerConfig;
4use crate::config::TransactionConfig;
5use crate::config::WebSocketDeliveryConfig;
6use crate::health::HealthMonitor;
7use crate::http_server::HttpServer;
8use crate::materialized_view::MaterializedViewRegistry;
9use crate::mutation_batch::MutationBatch;
10use crate::program_runtime::ProgramRuntimeCatalog;
11use crate::projector::Projector;
12use crate::view::ViewIndex;
13use crate::websocket::client_manager::RateLimitConfig;
14use crate::websocket::server::ConnectionAcceptor;
15use crate::websocket::WebSocketServer;
16use crate::SolanaGatewayUsageObserver;
17use crate::Spec;
18use crate::WebSocketAuthPlugin;
19use crate::WebSocketUsageEmitter;
20use anyhow::Result;
21use std::net::SocketAddr;
22use std::sync::Arc;
23use std::time::Duration;
24use tokio::net::{TcpListener, TcpStream};
25use tokio::sync::mpsc;
26use tokio::task::JoinHandle;
27use tokio_util::sync::CancellationToken;
28use tracing::{error, info, info_span, warn, Instrument};
29
30#[cfg(feature = "otel")]
31use crate::metrics::Metrics;
32
33async fn shutdown_signal() {
35 let ctrl_c = async {
36 tokio::signal::ctrl_c()
37 .await
38 .expect("Failed to install Ctrl+C handler");
39 };
40
41 #[cfg(unix)]
42 let terminate = async {
43 tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
44 .expect("Failed to install SIGTERM handler")
45 .recv()
46 .await;
47 };
48
49 #[cfg(not(unix))]
50 let terminate = std::future::pending::<()>();
51
52 tokio::select! {
53 _ = ctrl_c => {
54 info!("Received SIGINT (Ctrl+C), initiating shutdown");
55 }
56 _ = terminate => {
57 info!("Received SIGTERM, initiating graceful shutdown");
58 }
59 }
60}
61
62pub struct Runtime {
63 config: ServerConfig,
64 view_index: Arc<ViewIndex>,
65 spec: Option<Spec>,
66 program_runtime_catalog: ProgramRuntimeCatalog,
67 materialized_views: Option<MaterializedViewRegistry>,
68 websocket_auth_plugin: Option<Arc<dyn WebSocketAuthPlugin>>,
69 http_auth_plugin: Option<Arc<dyn WebSocketAuthPlugin>>,
70 websocket_usage_emitter: Option<Arc<dyn WebSocketUsageEmitter>>,
71 token_accounts_provider: Option<Arc<dyn crate::token_discovery::OwnerTokenAccountsProvider>>,
72 solana_gateway_usage_observer: Option<Arc<dyn SolanaGatewayUsageObserver>>,
73 websocket_max_clients: Option<usize>,
74 websocket_rate_limit_config: Option<RateLimitConfig>,
75 websocket_admission_provider: Option<Arc<dyn crate::WebSocketAdmissionProvider>>,
76 #[cfg(feature = "otel")]
77 metrics: Option<Arc<Metrics>>,
78}
79
80impl Runtime {
81 #[cfg(feature = "otel")]
82 pub fn new(config: ServerConfig, view_index: ViewIndex, metrics: Option<Arc<Metrics>>) -> Self {
83 Self {
84 config,
85 view_index: Arc::new(view_index),
86 spec: None,
87 program_runtime_catalog: ProgramRuntimeCatalog::default(),
88 materialized_views: None,
89 websocket_auth_plugin: None,
90 http_auth_plugin: None,
91 websocket_usage_emitter: None,
92 token_accounts_provider: None,
93 solana_gateway_usage_observer: None,
94 websocket_max_clients: None,
95 websocket_rate_limit_config: None,
96 websocket_admission_provider: None,
97 metrics,
98 }
99 }
100
101 #[cfg(not(feature = "otel"))]
102 pub fn new(config: ServerConfig, view_index: ViewIndex) -> Self {
103 Self {
104 config,
105 view_index: Arc::new(view_index),
106 spec: None,
107 program_runtime_catalog: ProgramRuntimeCatalog::default(),
108 materialized_views: None,
109 websocket_auth_plugin: None,
110 http_auth_plugin: None,
111 websocket_usage_emitter: None,
112 token_accounts_provider: None,
113 solana_gateway_usage_observer: None,
114 websocket_max_clients: None,
115 websocket_rate_limit_config: None,
116 websocket_admission_provider: None,
117 }
118 }
119
120 pub fn with_spec(mut self, spec: Spec) -> Result<Self> {
121 self.program_runtime_catalog =
122 ProgramRuntimeCatalog::try_new(spec.program_runtime_definitions.clone())?;
123 self.spec = Some(spec);
124 Ok(self)
125 }
126
127 pub fn with_materialized_views(mut self, registry: MaterializedViewRegistry) -> Self {
128 self.materialized_views = Some(registry);
129 self
130 }
131
132 pub fn with_websocket_auth_plugin(
133 mut self,
134 websocket_auth_plugin: Arc<dyn WebSocketAuthPlugin>,
135 ) -> Self {
136 self.websocket_auth_plugin = Some(websocket_auth_plugin);
137 self
138 }
139
140 pub fn with_http_auth_plugin(mut self, http_auth_plugin: Arc<dyn WebSocketAuthPlugin>) -> Self {
141 self.http_auth_plugin = Some(http_auth_plugin);
142 self
143 }
144
145 pub fn with_websocket_usage_emitter(
146 mut self,
147 websocket_usage_emitter: Arc<dyn WebSocketUsageEmitter>,
148 ) -> Self {
149 self.websocket_usage_emitter = Some(websocket_usage_emitter);
150 self
151 }
152
153 pub fn with_owner_token_accounts_provider(
154 mut self,
155 provider: Arc<dyn crate::token_discovery::OwnerTokenAccountsProvider>,
156 ) -> Self {
157 self.token_accounts_provider = Some(provider);
158 self
159 }
160
161 pub fn with_solana_gateway_usage_observer(
162 mut self,
163 observer: Arc<dyn SolanaGatewayUsageObserver>,
164 ) -> Self {
165 self.solana_gateway_usage_observer = Some(observer);
166 self
167 }
168
169 pub fn with_websocket_max_clients(mut self, websocket_max_clients: usize) -> Self {
170 self.websocket_max_clients = Some(websocket_max_clients);
171 self
172 }
173
174 pub fn with_websocket_rate_limit_config(mut self, config: RateLimitConfig) -> Self {
180 self.websocket_rate_limit_config = Some(config);
181 self
182 }
183
184 pub fn with_websocket_admission_provider(
185 mut self,
186 provider: Arc<dyn crate::WebSocketAdmissionProvider>,
187 ) -> Self {
188 self.websocket_admission_provider = Some(provider);
189 self
190 }
191
192 pub fn plan(&self) -> crate::RuntimePlan {
194 self.config.runtime_plan
195 }
196
197 pub async fn run(self) -> Result<()> {
204 let mut handle = self.spawn().await?;
205 info!("Arete runtime is running. Press Ctrl+C to stop.");
206
207 tokio::select! {
208 _ = handle.exited() => {}
209 _ = shutdown_signal() => {}
210 }
211
212 handle.shutdown().await
213 }
214
215 pub async fn spawn(self) -> Result<RuntimeHandle> {
227 info!("Starting Arete runtime");
228
229 let plan = self.config.runtime_plan;
230 let transaction_config = if plan.transactions {
231 match self.config.transactions.clone() {
232 Some(config) => config,
233 None => TransactionConfig::from_env()?,
234 }
235 } else {
236 TransactionConfig::default()
237 };
238 if plan.transactions && !transaction_config.enabled {
239 anyhow::bail!(
240 "the runtime plan enables transactions but transaction configuration is disabled"
241 );
242 }
243 let websocket_limits = match self.config.websocket_limits.clone() {
246 Some(limits) => {
247 limits.validate()?;
248 limits
249 }
250 None => crate::config::WebSocketLimitsConfig::from_env()?,
251 };
252 let http_limits = match self.config.http_limits.clone() {
253 Some(limits) => {
254 limits.validate()?;
255 limits
256 }
257 None => crate::config::HttpLimitsConfig::from_env()?,
258 };
259 let program_runtime_catalog = self.program_runtime_catalog.clone();
260
261 let health_monitor = if plan.health {
262 self.config
263 .health
264 .as_ref()
265 .map(|health_config| HealthMonitor::new(health_config.clone()))
266 } else {
267 None
268 };
269 let mut background = Vec::new();
270 if let Some(monitor) = &health_monitor {
271 background.push(monitor.start().await);
272 info!("Health monitoring enabled");
273 }
274
275 let mut projector_handle = None;
276 let mut ws_handle = None;
277 let mut parser_handle = None;
278 let mut mutations_tx_guard = None;
279 let mut snapshot_service: Option<Arc<crate::snapshot::SnapshotService>> = None;
280 let mut snapshot_manager_handle = None;
281 let mut snapshot_runtime = None;
282 let mut acceptor = None;
283 let mut entity_cache_handle = None;
284
285 if plan.live_runtime_enabled() {
286 let (mutations_tx, mutations_rx) = mpsc::channel::<MutationBatch>(1024);
287 mutations_tx_guard = Some(mutations_tx.clone());
288 let websocket_delivery = match self.config.websocket_delivery.clone() {
289 Some(config) => {
290 config.validate()?;
291 config
292 }
293 None => WebSocketDeliveryConfig::from_env()?,
294 };
295 info!(
296 list_bus_capacity = websocket_delivery.list_bus_capacity,
297 collection_coalesce_ms = ?websocket_delivery.collection_coalesce_ms,
298 "WebSocket delivery configured"
299 );
300 let bus_manager = BusManager::with_capacity(websocket_delivery.list_bus_capacity);
301 let entity_cache = EntityCache::new();
302 entity_cache_handle = Some(entity_cache.clone());
303
304 let journal_config = match self.config.journal.clone() {
310 Some(config) => config,
311 None => match crate::journal::JournalConfig::from_env() {
312 Ok(config) => config,
313 Err(e) => {
314 error!("Invalid journal configuration; event replay disabled: {e:#}");
315 crate::journal::JournalConfig::default()
316 }
317 },
318 };
319 let journal = Arc::new(crate::journal::EventJournal::new(journal_config));
320 if journal.is_enabled() {
321 info!(
322 max_bytes_per_view = journal.config().max_bytes_per_view,
323 max_records_per_view = journal.config().max_records_per_view,
324 max_age_secs = journal.config().max_age.as_secs(),
325 "Event replay enabled for append views"
326 );
327 }
328
329 if let Some(spec) = self.spec.as_ref() {
334 let snapshot_config = match self.config.snapshots.clone() {
335 Some(config) => Some(config),
336 None => match crate::snapshot::SnapshotConfig::from_env() {
337 Ok(config) => Some(config),
338 Err(e) => {
339 error!("Invalid snapshot configuration; snapshots disabled: {e:#}");
340 None
341 }
342 },
343 };
344 if let Some(snapshot_config) = snapshot_config.filter(|c| c.enabled) {
345 match crate::snapshot::SnapshotService::initialize(
346 snapshot_config,
347 spec,
348 entity_cache.clone(),
349 &self.view_index,
350 journal.clone(),
351 mutations_tx.clone(),
352 )
353 .await
354 {
355 Ok(service) => {
356 snapshot_runtime = Some(service.runtime());
357 snapshot_manager_handle = Some(service.spawn());
358 snapshot_service = Some(service);
359 }
360 Err(e) => {
361 error!("Failed to initialize snapshots; continuing without: {e:#}")
362 }
363 }
364 }
365 }
366
367 #[cfg(feature = "otel")]
368 let projector = Projector::new(
369 self.view_index.clone(),
370 bus_manager.clone(),
371 entity_cache.clone(),
372 mutations_rx,
373 self.metrics.clone(),
374 );
375 #[cfg(not(feature = "otel"))]
376 let projector = Projector::new(
377 self.view_index.clone(),
378 bus_manager.clone(),
379 entity_cache.clone(),
380 mutations_rx,
381 );
382 let projector = match snapshot_runtime.clone() {
383 Some(runtime) => projector.with_snapshot_runtime(runtime),
384 None => projector,
385 };
386 let entity_resync = crate::projector::EntityResync::new();
389 let projector = projector
390 .with_journal(journal.clone())
391 .with_entity_resync(entity_resync.clone());
392
393 projector_handle = Some(tokio::spawn(async move {
399 projector.run().await;
400 }));
401
402 let bind_address = self
407 .config
408 .websocket
409 .as_ref()
410 .map(|ws_config| ws_config.bind_address)
411 .unwrap_or_else(|| SocketAddr::from(([0, 0, 0, 0], 0)));
412 #[cfg(feature = "otel")]
413 let mut ws_server = WebSocketServer::new(
414 bind_address,
415 bus_manager.clone(),
416 entity_cache.clone(),
417 self.view_index.clone(),
418 self.metrics.clone(),
419 );
420 #[cfg(not(feature = "otel"))]
421 let mut ws_server = WebSocketServer::new(
422 bind_address,
423 bus_manager.clone(),
424 entity_cache.clone(),
425 self.view_index.clone(),
426 );
427
428 ws_server = ws_server.with_journal(journal.clone());
429 ws_server = ws_server.with_delivery_config(websocket_delivery);
430 ws_server = ws_server.with_limits(websocket_limits.clone());
431 if let Some(max_clients) = self.websocket_max_clients {
432 ws_server = ws_server.with_max_clients(max_clients);
433 }
434 if let Some(plugin) = self.websocket_auth_plugin.clone() {
435 ws_server = ws_server.with_auth_plugin(plugin);
436 }
437 if let Some(emitter) = self.websocket_usage_emitter.clone() {
438 ws_server = ws_server.with_usage_emitter(emitter);
439 }
440 if let Some(rate_limit_config) = self.websocket_rate_limit_config {
441 ws_server = ws_server.with_rate_limit_config(rate_limit_config);
442 }
443 if let Some(provider) = self.websocket_admission_provider.clone() {
444 ws_server = ws_server.with_admission_provider(provider);
445 }
446 let (connection_acceptor, cleanup_handle) = ws_server.into_acceptor();
447 background.push(cleanup_handle);
448
449 if plan.websocket && self.config.websocket.is_some() {
450 let listener_acceptor = connection_acceptor.clone();
451 ws_handle = Some(tokio::spawn(
452 async move {
453 info!("Starting WebSocket server on {}", bind_address);
454 let listener = match TcpListener::bind(&bind_address).await {
455 Ok(listener) => listener,
456 Err(e) => {
457 error!("WebSocket server error: {}", e);
458 return;
459 }
460 };
461 if let Err(e) = listener_acceptor.serve_listener(listener).await {
462 error!("WebSocket server error: {}", e);
463 }
464 }
465 .instrument(info_span!("ws.server", %bind_address)),
466 ));
467 }
468 acceptor = Some(connection_acceptor);
469
470 if let Some(spec) = self.spec.as_ref() {
471 if let Some(parser_setup) = spec.parser_setup.clone() {
472 let program_id = spec
473 .program_ids
474 .first()
475 .cloned()
476 .unwrap_or_else(|| "unknown".to_string());
477 info!("Starting parser runtime for program: {}", program_id);
478 let health = health_monitor.clone();
479 let reconnection_config = self.config.reconnection.clone().unwrap_or_default();
480 let parser_snapshot_runtime = snapshot_runtime.clone();
481 let parser_journal = journal.clone();
482 let parser_entity_resync = entity_resync.clone();
483 parser_handle = Some(tokio::spawn(async move {
489 let parser = async move {
490 parser_setup(mutations_tx, health, reconnection_config).await
491 };
492 let scoped = async move {
493 match parser_snapshot_runtime {
494 Some(runtime) => runtime.scope(parser).await,
495 None => parser.await,
496 }
497 };
498 let result = parser_entity_resync
502 .scope(parser_journal.scope(scoped))
503 .await;
504 if let Err(e) = result {
505 error!(%program_id, "Vixen parser runtime error: {}", e);
506 }
507 }));
508 } else {
509 info!("Spec provided but no parser_setup configured - skipping parser runtime");
510 }
511 } else {
512 info!("No spec provided - running in websocket-only mode");
513 }
514
515 let cleanup_bus = bus_manager.clone();
516 background.push(tokio::spawn(
517 async move {
518 let mut interval = tokio::time::interval(Duration::from_secs(60));
519 loop {
520 interval.tick().await;
521 let state_cleaned = cleanup_bus.cleanup_stale_state_buses().await;
522 let list_cleaned = cleanup_bus.cleanup_stale_list_buses().await;
523 if state_cleaned > 0 || list_cleaned > 0 {
524 let (state_count, list_count) = cleanup_bus.bus_counts().await;
525 info!(
526 "Bus cleanup: removed {} state, {} list buses. Current: {} state, {} list",
527 state_cleaned, list_cleaned, state_count, list_count
528 );
529 }
530 }
531 }
532 .instrument(info_span!("bus.cleanup")),
533 ));
534
535 background.push(tokio::spawn(
536 async move {
537 let mut interval = tokio::time::interval(Duration::from_secs(30));
538 loop {
539 interval.tick().await;
540 let (_state_buses, _list_buses) = bus_manager.bus_counts().await;
541 let _cache_stats = entity_cache.stats().await;
542 }
543 }
544 .instrument(info_span!("stats.reporter")),
545 ));
546 } else {
547 info!(
548 "Live runtime disabled; projection and Yellowstone resources were not initialized"
549 );
550 }
551
552 let http_shutdown = CancellationToken::new();
555 let http_health_thread = if let Some(http_health_config) = &self.config.http_health {
556 let mut http_server = HttpServer::new(http_health_config.bind_address)
557 .with_limits(http_limits.clone())
558 .with_runtime_plan(plan)
559 .with_program_runtime_catalog(program_runtime_catalog)
560 .with_shutdown(http_shutdown.clone());
561 if let Some(target_id) = self.config.program_read_binding_target_id.clone() {
562 http_server = http_server.with_program_read_binding_target(target_id);
563 }
564 if let Some(target_id) = self.config.solana_gateway_target_id.clone() {
565 http_server = http_server.with_solana_gateway_target(target_id);
566 }
567 if let Some(monitor) = health_monitor.clone() {
568 http_server = http_server.with_health_monitor(monitor);
569 }
570 if let Some(runtime) = snapshot_runtime.clone() {
571 http_server = http_server.with_snapshot_runtime(runtime);
572 }
573 if let Some(plugin) = self
574 .http_auth_plugin
575 .clone()
576 .or_else(|| self.websocket_auth_plugin.clone())
577 {
578 http_server = http_server.with_auth_plugin(plugin);
579 }
580 if plan.transactions && transaction_config.enabled {
581 http_server = http_server.with_transaction_config(transaction_config.clone());
582 }
583 if let Some(provider) = self.token_accounts_provider.clone() {
584 http_server = http_server.with_owner_token_accounts_provider(provider);
585 }
586 if let Some(observer) = self.solana_gateway_usage_observer.clone() {
587 http_server = http_server.with_solana_gateway_usage_observer(observer);
588 }
589 #[cfg(feature = "otel")]
590 {
591 http_server = http_server.with_metrics(self.metrics.clone());
592 }
593
594 let bind_addr = http_health_config.bind_address;
595 let join_handle = std::thread::Builder::new()
596 .name("health-server".into())
597 .spawn(move || {
598 let rt = tokio::runtime::Builder::new_current_thread()
599 .enable_all()
600 .build()
601 .expect("Failed to create health server runtime");
602 rt.block_on(async move {
603 let _span = info_span!("http.health", %bind_addr).entered();
604 if let Err(e) = http_server.start().await {
605 error!("HTTP health server error: {}", e);
606 }
607 });
608 })
609 .expect("Failed to spawn health server thread");
610 info!(
611 "HTTP health server running on dedicated thread at {}",
612 bind_addr
613 );
614 Some(join_handle)
615 } else {
616 None
617 };
618
619 Ok(RuntimeHandle {
620 plan,
621 health_monitor,
622 snapshot_runtime,
623 snapshot_service,
624 snapshot_manager_handle,
625 mutations_tx: mutations_tx_guard,
626 projector_handle,
627 parser_handle,
628 ws_handle,
629 background,
630 acceptor,
631 entity_cache: entity_cache_handle,
632 websocket_usage_emitter: self.websocket_usage_emitter,
633 http_shutdown,
634 http_health_thread,
635 })
636 }
637}
638
639pub struct RuntimeHandle {
647 plan: crate::RuntimePlan,
648 health_monitor: Option<HealthMonitor>,
649 snapshot_runtime: Option<crate::snapshot::SnapshotRuntime>,
650 snapshot_service: Option<Arc<crate::snapshot::SnapshotService>>,
651 snapshot_manager_handle: Option<JoinHandle<()>>,
652 mutations_tx: Option<mpsc::Sender<MutationBatch>>,
653 projector_handle: Option<JoinHandle<()>>,
654 parser_handle: Option<JoinHandle<()>>,
655 ws_handle: Option<JoinHandle<()>>,
656 background: Vec<JoinHandle<()>>,
657 acceptor: Option<ConnectionAcceptor>,
658 entity_cache: Option<EntityCache>,
659 websocket_usage_emitter: Option<Arc<dyn WebSocketUsageEmitter>>,
660 http_shutdown: CancellationToken,
661 http_health_thread: Option<std::thread::JoinHandle<()>>,
662}
663
664#[derive(Clone)]
668pub struct ConnectionServer(ConnectionAcceptor);
669
670impl ConnectionServer {
671 pub async fn serve(&self, stream: TcpStream, remote_addr: SocketAddr) -> Result<()> {
674 self.0.serve(stream, remote_addr).await
675 }
676
677 pub fn client_count(&self) -> usize {
679 self.0.client_count()
680 }
681}
682
683const SESSION_DRAIN_TIMEOUT: Duration = Duration::from_secs(5);
686
687const PROJECTOR_DRAIN_TIMEOUT: Duration = Duration::from_secs(10);
690
691const USAGE_EMITTER_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(10);
694
695const SHUTDOWN_SNAPSHOT_TIMEOUT: Duration = Duration::from_secs(20);
698
699impl RuntimeHandle {
700 pub fn plan(&self) -> crate::RuntimePlan {
702 self.plan
703 }
704
705 pub async fn is_ready(&self) -> bool {
709 let stream_ready = match self.health_monitor.as_ref() {
710 Some(monitor) => monitor.is_healthy().await,
711 None => true,
712 };
713 let snapshot_ready = self
714 .snapshot_runtime
715 .as_ref()
716 .is_none_or(crate::snapshot::SnapshotRuntime::resume_gate_ready);
717 stream_ready && snapshot_ready
718 }
719
720 pub fn client_count(&self) -> usize {
722 self.acceptor
723 .as_ref()
724 .map(ConnectionAcceptor::client_count)
725 .unwrap_or(0)
726 }
727
728 pub async fn entity_cache_stats(&self) -> Option<crate::cache::CacheStats> {
732 match &self.entity_cache {
733 Some(cache) => Some(cache.stats().await),
734 None => None,
735 }
736 }
737
738 pub async fn serve_connection(&self, stream: TcpStream, remote_addr: SocketAddr) -> Result<()> {
748 match self.connection_server() {
749 Some(server) => server.serve(stream, remote_addr).await,
750 None => anyhow::bail!("this runtime has no live runtime to serve connections from"),
751 }
752 }
753
754 pub fn connection_server(&self) -> Option<ConnectionServer> {
759 self.acceptor.clone().map(ConnectionServer)
760 }
761
762 pub async fn exited(&mut self) {
765 async fn wait(handle: Option<&mut JoinHandle<()>>) {
766 match handle {
767 Some(handle) => {
768 let _ = handle.await;
769 }
770 None => std::future::pending().await,
771 }
772 }
773
774 tokio::select! {
775 _ = wait(self.ws_handle.as_mut()) => info!("WebSocket server task completed"),
776 _ = wait(self.projector_handle.as_mut()) => info!("Projector task completed"),
777 _ = wait(self.parser_handle.as_mut()) => info!("Parser runtime task completed"),
778 }
779 }
780
781 pub async fn shutdown(mut self) -> Result<()> {
797 if let Some(service) = self.snapshot_service.take() {
800 if let Some(handle) = self.snapshot_manager_handle.take() {
801 handle.abort();
802 }
803 if service.config().snapshot_on_shutdown {
804 info!("Taking final snapshot before shutdown");
805 match tokio::time::timeout(
806 SHUTDOWN_SNAPSHOT_TIMEOUT,
807 service.snapshot_now(crate::snapshot::SnapshotTrigger::Shutdown),
808 )
809 .await
810 {
811 Ok(Ok(_)) => {}
812 Ok(Err(e)) => error!("Shutdown snapshot failed: {e:#}"),
813 Err(_) => error!("Shutdown snapshot timed out"),
814 }
815 }
816 }
817 if let Some(handle) = self.snapshot_manager_handle.take() {
818 handle.abort();
819 }
820
821 if let Some(parser) = self.parser_handle.take() {
822 parser.abort();
823 let _ = parser.await;
824 }
825 if let Some(acceptor) = &self.acceptor {
826 acceptor.shutdown();
827 }
828 if let Some(ws) = self.ws_handle.take() {
829 let _ = ws.await;
830 }
831 let sessions_drained = if let Some(acceptor) = &self.acceptor {
832 if tokio::time::timeout(SESSION_DRAIN_TIMEOUT, acceptor.wait_for_sessions())
833 .await
834 .is_err()
835 {
836 warn!(
837 "Sessions did not finish within {:?} of shutdown",
838 SESSION_DRAIN_TIMEOUT
839 );
840 false
841 } else {
842 true
843 }
844 } else {
845 true
846 };
847
848 let usage_events_drained = if !sessions_drained {
849 false
850 } else if let Some(acceptor) = &self.acceptor {
851 if tokio::time::timeout(
852 USAGE_EMITTER_SHUTDOWN_TIMEOUT,
853 acceptor.wait_for_usage_events(),
854 )
855 .await
856 .is_err()
857 {
858 warn!(
859 "WebSocket usage event tasks did not finish within {:?}",
860 USAGE_EMITTER_SHUTDOWN_TIMEOUT
861 );
862 false
863 } else {
864 true
865 }
866 } else {
867 true
868 };
869
870 if let Some(emitter) = self.websocket_usage_emitter.take() {
871 let operation = if usage_events_drained {
872 emitter.shutdown()
873 } else {
874 emitter.flush()
879 };
880 if tokio::time::timeout(USAGE_EMITTER_SHUTDOWN_TIMEOUT, operation)
881 .await
882 .is_err()
883 {
884 warn!(
885 operation = if usage_events_drained {
886 "shutdown"
887 } else {
888 "flush"
889 },
890 "WebSocket usage emitter operation did not finish within {:?}",
891 USAGE_EMITTER_SHUTDOWN_TIMEOUT
892 );
893 }
894 }
895
896 drop(self.mutations_tx.take());
897 if let Some(mut projector) = self.projector_handle.take() {
898 if tokio::time::timeout(PROJECTOR_DRAIN_TIMEOUT, &mut projector)
899 .await
900 .is_err()
901 {
902 warn!(
903 "Projector did not drain within {:?}; aborting it",
904 PROJECTOR_DRAIN_TIMEOUT
905 );
906 projector.abort();
907 let _ = projector.await;
908 }
909 }
910
911 for handle in self.background.drain(..) {
912 handle.abort();
913 }
914
915 self.http_shutdown.cancel();
916 if let Some(thread) = self.http_health_thread.take() {
917 if let Err(e) = tokio::task::spawn_blocking(move || thread.join()).await {
918 error!("Health server thread join failed: {e}");
919 }
920 }
921
922 info!("Shutting down Arete runtime");
923 Ok(())
924 }
925}