1use crate::bus::BusManager;
2use crate::cache::EntityCache;
3use crate::config::ServerConfig;
4use crate::config::TransactionConfig;
5use crate::health::HealthMonitor;
6use crate::http_server::HttpServer;
7use crate::materialized_view::MaterializedViewRegistry;
8use crate::mutation_batch::MutationBatch;
9use crate::program_runtime::ProgramRuntimeCatalog;
10use crate::projector::Projector;
11use crate::view::ViewIndex;
12use crate::websocket::client_manager::RateLimitConfig;
13use crate::websocket::WebSocketServer;
14use crate::Spec;
15use crate::WebSocketAuthPlugin;
16use crate::WebSocketUsageEmitter;
17use anyhow::Result;
18use std::sync::Arc;
19use std::time::Duration;
20use tokio::sync::mpsc;
21use tracing::{error, info, info_span, Instrument};
22
23#[cfg(feature = "otel")]
24use crate::metrics::Metrics;
25
26async fn shutdown_signal() {
28 let ctrl_c = async {
29 tokio::signal::ctrl_c()
30 .await
31 .expect("Failed to install Ctrl+C handler");
32 };
33
34 #[cfg(unix)]
35 let terminate = async {
36 tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
37 .expect("Failed to install SIGTERM handler")
38 .recv()
39 .await;
40 };
41
42 #[cfg(not(unix))]
43 let terminate = std::future::pending::<()>();
44
45 tokio::select! {
46 _ = ctrl_c => {
47 info!("Received SIGINT (Ctrl+C), initiating shutdown");
48 }
49 _ = terminate => {
50 info!("Received SIGTERM, initiating graceful shutdown");
51 }
52 }
53}
54
55pub struct Runtime {
56 config: ServerConfig,
57 view_index: Arc<ViewIndex>,
58 spec: Option<Spec>,
59 program_runtime_catalog: ProgramRuntimeCatalog,
60 materialized_views: Option<MaterializedViewRegistry>,
61 websocket_auth_plugin: Option<Arc<dyn WebSocketAuthPlugin>>,
62 http_auth_plugin: Option<Arc<dyn WebSocketAuthPlugin>>,
63 websocket_usage_emitter: Option<Arc<dyn WebSocketUsageEmitter>>,
64 websocket_max_clients: Option<usize>,
65 websocket_rate_limit_config: Option<RateLimitConfig>,
66 #[cfg(feature = "otel")]
67 metrics: Option<Arc<Metrics>>,
68}
69
70impl Runtime {
71 #[cfg(feature = "otel")]
72 pub fn new(config: ServerConfig, view_index: ViewIndex, metrics: Option<Arc<Metrics>>) -> Self {
73 Self {
74 config,
75 view_index: Arc::new(view_index),
76 spec: None,
77 program_runtime_catalog: ProgramRuntimeCatalog::default(),
78 materialized_views: None,
79 websocket_auth_plugin: None,
80 http_auth_plugin: None,
81 websocket_usage_emitter: None,
82 websocket_max_clients: None,
83 websocket_rate_limit_config: None,
84 metrics,
85 }
86 }
87
88 #[cfg(not(feature = "otel"))]
89 pub fn new(config: ServerConfig, view_index: ViewIndex) -> Self {
90 Self {
91 config,
92 view_index: Arc::new(view_index),
93 spec: None,
94 program_runtime_catalog: ProgramRuntimeCatalog::default(),
95 materialized_views: None,
96 websocket_auth_plugin: None,
97 http_auth_plugin: None,
98 websocket_usage_emitter: None,
99 websocket_max_clients: None,
100 websocket_rate_limit_config: None,
101 }
102 }
103
104 pub fn with_spec(mut self, spec: Spec) -> Result<Self> {
105 self.program_runtime_catalog =
106 ProgramRuntimeCatalog::try_new(spec.program_runtime_definitions.clone())?;
107 self.spec = Some(spec);
108 Ok(self)
109 }
110
111 pub fn with_materialized_views(mut self, registry: MaterializedViewRegistry) -> Self {
112 self.materialized_views = Some(registry);
113 self
114 }
115
116 pub fn with_websocket_auth_plugin(
117 mut self,
118 websocket_auth_plugin: Arc<dyn WebSocketAuthPlugin>,
119 ) -> Self {
120 self.websocket_auth_plugin = Some(websocket_auth_plugin);
121 self
122 }
123
124 pub fn with_http_auth_plugin(mut self, http_auth_plugin: Arc<dyn WebSocketAuthPlugin>) -> Self {
125 self.http_auth_plugin = Some(http_auth_plugin);
126 self
127 }
128
129 pub fn with_websocket_usage_emitter(
130 mut self,
131 websocket_usage_emitter: Arc<dyn WebSocketUsageEmitter>,
132 ) -> Self {
133 self.websocket_usage_emitter = Some(websocket_usage_emitter);
134 self
135 }
136
137 pub fn with_websocket_max_clients(mut self, websocket_max_clients: usize) -> Self {
138 self.websocket_max_clients = Some(websocket_max_clients);
139 self
140 }
141
142 pub fn with_websocket_rate_limit_config(mut self, config: RateLimitConfig) -> Self {
148 self.websocket_rate_limit_config = Some(config);
149 self
150 }
151
152 pub fn plan(&self) -> crate::RuntimePlan {
154 self.config.runtime_plan
155 }
156
157 pub async fn run(self) -> Result<()> {
158 info!("Starting Arete runtime");
159
160 let plan = self.config.runtime_plan;
161 let transaction_config = if plan.transactions {
162 match self.config.transactions.clone() {
163 Some(config) => config,
164 None => TransactionConfig::from_env()?,
165 }
166 } else {
167 TransactionConfig::default()
168 };
169 if plan.transactions && !transaction_config.enabled {
170 anyhow::bail!(
171 "the runtime plan enables transactions but transaction configuration is disabled"
172 );
173 }
174 let program_runtime_catalog = self.program_runtime_catalog.clone();
175
176 let health_monitor = if plan.health {
177 self.config
178 .health
179 .as_ref()
180 .map(|health_config| HealthMonitor::new(health_config.clone()))
181 } else {
182 None
183 };
184 if let Some(monitor) = &health_monitor {
185 let _health_task = monitor.start().await;
186 info!("Health monitoring enabled");
187 }
188
189 let mut projector_handle = None;
190 let mut ws_handle = None;
191 let mut parser_handle = None;
192 let mut bus_cleanup_handle = None;
193 let mut stats_handle = None;
194 let mut mutations_tx_guard = None;
195 let mut snapshot_service: Option<Arc<crate::snapshot::SnapshotService>> = None;
196 let mut snapshot_manager_handle = None;
197 let mut snapshot_runtime = None;
198
199 if plan.live_runtime_enabled() {
200 let (mutations_tx, mutations_rx) = mpsc::channel::<MutationBatch>(1024);
201 mutations_tx_guard = Some(mutations_tx.clone());
202 let bus_manager = BusManager::new();
203 let entity_cache = EntityCache::new();
204
205 if let Some(spec) = self.spec.as_ref() {
210 let snapshot_config = match self.config.snapshots.clone() {
211 Some(config) => Some(config),
212 None => match crate::snapshot::SnapshotConfig::from_env() {
213 Ok(config) => Some(config),
214 Err(e) => {
215 error!("Invalid snapshot configuration; snapshots disabled: {e:#}");
216 None
217 }
218 },
219 };
220 if let Some(snapshot_config) = snapshot_config.filter(|c| c.enabled) {
221 match crate::snapshot::SnapshotService::initialize(
222 snapshot_config,
223 spec,
224 entity_cache.clone(),
225 &self.view_index,
226 mutations_tx.clone(),
227 )
228 .await
229 {
230 Ok(service) => {
231 snapshot_runtime = Some(service.runtime());
232 snapshot_manager_handle = Some(service.spawn());
233 snapshot_service = Some(service);
234 }
235 Err(e) => {
236 error!("Failed to initialize snapshots; continuing without: {e:#}")
237 }
238 }
239 }
240 }
241
242 #[cfg(feature = "otel")]
243 let projector = Projector::new(
244 self.view_index.clone(),
245 bus_manager.clone(),
246 entity_cache.clone(),
247 mutations_rx,
248 self.metrics.clone(),
249 );
250 #[cfg(not(feature = "otel"))]
251 let projector = Projector::new(
252 self.view_index.clone(),
253 bus_manager.clone(),
254 entity_cache.clone(),
255 mutations_rx,
256 );
257 let projector = match snapshot_runtime.clone() {
258 Some(runtime) => projector.with_snapshot_runtime(runtime),
259 None => projector,
260 };
261
262 projector_handle = Some(tokio::spawn(
263 async move {
264 projector.run().await;
265 }
266 .instrument(info_span!("projector")),
267 ));
268
269 if plan.websocket {
270 if let Some(ws_config) = &self.config.websocket {
271 #[cfg(feature = "otel")]
272 let mut ws_server = WebSocketServer::new(
273 ws_config.bind_address,
274 bus_manager.clone(),
275 entity_cache.clone(),
276 self.view_index.clone(),
277 self.metrics.clone(),
278 );
279 #[cfg(not(feature = "otel"))]
280 let mut ws_server = WebSocketServer::new(
281 ws_config.bind_address,
282 bus_manager.clone(),
283 entity_cache.clone(),
284 self.view_index.clone(),
285 );
286
287 if let Some(max_clients) = self.websocket_max_clients {
288 ws_server = ws_server.with_max_clients(max_clients);
289 }
290 if let Some(plugin) = self.websocket_auth_plugin.clone() {
291 ws_server = ws_server.with_auth_plugin(plugin);
292 }
293 if let Some(emitter) = self.websocket_usage_emitter.clone() {
294 ws_server = ws_server.with_usage_emitter(emitter);
295 }
296 if let Some(rate_limit_config) = self.websocket_rate_limit_config {
297 ws_server = ws_server.with_rate_limit_config(rate_limit_config);
298 }
299
300 let bind_addr = ws_config.bind_address;
301 ws_handle = Some(tokio::spawn(
302 async move {
303 if let Err(e) = ws_server.start().await {
304 error!("WebSocket server error: {}", e);
305 }
306 }
307 .instrument(info_span!("ws.server", %bind_addr)),
308 ));
309 }
310 }
311
312 if let Some(spec) = self.spec.as_ref() {
313 if let Some(parser_setup) = spec.parser_setup.clone() {
314 let program_id = spec
315 .program_ids
316 .first()
317 .cloned()
318 .unwrap_or_else(|| "unknown".to_string());
319 info!("Starting parser runtime for program: {}", program_id);
320 let health = health_monitor.clone();
321 let reconnection_config = self.config.reconnection.clone().unwrap_or_default();
322 let parser_snapshot_runtime = snapshot_runtime.clone();
323 parser_handle = Some(tokio::spawn(
324 async move {
325 let parser = async move {
326 parser_setup(mutations_tx, health, reconnection_config).await
327 };
328 let result = match parser_snapshot_runtime {
329 Some(runtime) => runtime.scope(parser).await,
330 None => parser.await,
331 };
332 if let Err(e) = result {
333 error!("Vixen parser runtime error: {}", e);
334 }
335 }
336 .instrument(info_span!("vixen.parser", %program_id)),
337 ));
338 } else {
339 info!("Spec provided but no parser_setup configured - skipping parser runtime");
340 }
341 } else {
342 info!("No spec provided - running in websocket-only mode");
343 }
344
345 let cleanup_bus = bus_manager.clone();
346 bus_cleanup_handle = Some(tokio::spawn(
347 async move {
348 let mut interval = tokio::time::interval(Duration::from_secs(60));
349 loop {
350 interval.tick().await;
351 let state_cleaned = cleanup_bus.cleanup_stale_state_buses().await;
352 let list_cleaned = cleanup_bus.cleanup_stale_list_buses().await;
353 if state_cleaned > 0 || list_cleaned > 0 {
354 let (state_count, list_count) = cleanup_bus.bus_counts().await;
355 info!(
356 "Bus cleanup: removed {} state, {} list buses. Current: {} state, {} list",
357 state_cleaned, list_cleaned, state_count, list_count
358 );
359 }
360 }
361 }
362 .instrument(info_span!("bus.cleanup")),
363 ));
364
365 stats_handle = Some(tokio::spawn(
366 async move {
367 let mut interval = tokio::time::interval(Duration::from_secs(30));
368 loop {
369 interval.tick().await;
370 let (_state_buses, _list_buses) = bus_manager.bus_counts().await;
371 let _cache_stats = entity_cache.stats().await;
372 }
373 }
374 .instrument(info_span!("stats.reporter")),
375 ));
376 } else {
377 info!(
378 "Live runtime disabled; projection and Yellowstone resources were not initialized"
379 );
380 }
381
382 let _http_health_handle = if let Some(http_health_config) = &self.config.http_health {
385 let mut http_server = HttpServer::new(http_health_config.bind_address)
386 .with_runtime_plan(plan)
387 .with_program_runtime_catalog(program_runtime_catalog);
388 if let Some(target_id) = self.config.program_read_binding_target_id.clone() {
389 http_server = http_server.with_program_read_binding_target(target_id);
390 }
391 if let Some(target_id) = self.config.solana_gateway_target_id.clone() {
392 http_server = http_server.with_solana_gateway_target(target_id);
393 }
394 if let Some(monitor) = health_monitor.clone() {
395 http_server = http_server.with_health_monitor(monitor);
396 }
397 if let Some(runtime) = snapshot_runtime.clone() {
398 http_server = http_server.with_snapshot_runtime(runtime);
399 }
400 if let Some(plugin) = self
401 .http_auth_plugin
402 .clone()
403 .or_else(|| self.websocket_auth_plugin.clone())
404 {
405 http_server = http_server.with_auth_plugin(plugin);
406 }
407 if plan.transactions && transaction_config.enabled {
408 http_server = http_server.with_transaction_config(transaction_config.clone());
409 }
410 #[cfg(feature = "otel")]
411 {
412 http_server = http_server.with_metrics(self.metrics.clone());
413 }
414
415 let bind_addr = http_health_config.bind_address;
416 let join_handle = std::thread::Builder::new()
417 .name("health-server".into())
418 .spawn(move || {
419 let rt = tokio::runtime::Builder::new_current_thread()
420 .enable_all()
421 .build()
422 .expect("Failed to create health server runtime");
423 rt.block_on(async move {
424 let _span = info_span!("http.health", %bind_addr).entered();
425 if let Err(e) = http_server.start().await {
426 error!("HTTP health server error: {}", e);
427 }
428 });
429 })
430 .expect("Failed to spawn health server thread");
431 info!(
432 "HTTP health server running on dedicated thread at {}",
433 bind_addr
434 );
435 Some(join_handle)
436 } else {
437 None
438 };
439
440 info!("Arete runtime is running. Press Ctrl+C to stop.");
441
442 async fn wait_for_task(handle: Option<tokio::task::JoinHandle<()>>) {
443 if let Some(handle) = handle {
444 let _ = handle.await;
445 } else {
446 std::future::pending().await
447 }
448 }
449
450 tokio::select! {
451 _ = wait_for_task(ws_handle) => info!("WebSocket server task completed"),
452 _ = wait_for_task(projector_handle) => info!("Projector task completed"),
453 _ = wait_for_task(parser_handle) => info!("Parser runtime task completed"),
454 _ = wait_for_task(bus_cleanup_handle) => info!("Bus cleanup task completed"),
455 _ = wait_for_task(stats_handle) => info!("Stats reporter task completed"),
456 _ = shutdown_signal() => {}
457 }
458
459 if let Some(service) = snapshot_service.take() {
463 if let Some(handle) = snapshot_manager_handle.take() {
464 handle.abort();
465 }
466 if service.config().snapshot_on_shutdown {
467 info!("Taking final snapshot before shutdown");
468 match tokio::time::timeout(
469 Duration::from_secs(20),
470 service.snapshot_now(crate::snapshot::SnapshotTrigger::Shutdown),
471 )
472 .await
473 {
474 Ok(Ok(_)) => {}
475 Ok(Err(e)) => error!("Shutdown snapshot failed: {e:#}"),
476 Err(_) => error!("Shutdown snapshot timed out"),
477 }
478 }
479 }
480 drop(mutations_tx_guard);
481
482 info!("Shutting down Arete runtime");
483 Ok(())
484 }
485}