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
196 if plan.live_runtime_enabled() {
197 let (mutations_tx, mutations_rx) = mpsc::channel::<MutationBatch>(1024);
198 mutations_tx_guard = Some(mutations_tx.clone());
199 let bus_manager = BusManager::new();
200 let entity_cache = EntityCache::new();
201
202 #[cfg(feature = "otel")]
203 let projector = Projector::new(
204 self.view_index.clone(),
205 bus_manager.clone(),
206 entity_cache.clone(),
207 mutations_rx,
208 self.metrics.clone(),
209 );
210 #[cfg(not(feature = "otel"))]
211 let projector = Projector::new(
212 self.view_index.clone(),
213 bus_manager.clone(),
214 entity_cache.clone(),
215 mutations_rx,
216 );
217
218 projector_handle = Some(tokio::spawn(
219 async move {
220 projector.run().await;
221 }
222 .instrument(info_span!("projector")),
223 ));
224
225 if plan.websocket {
226 if let Some(ws_config) = &self.config.websocket {
227 #[cfg(feature = "otel")]
228 let mut ws_server = WebSocketServer::new(
229 ws_config.bind_address,
230 bus_manager.clone(),
231 entity_cache.clone(),
232 self.view_index.clone(),
233 self.metrics.clone(),
234 );
235 #[cfg(not(feature = "otel"))]
236 let mut ws_server = WebSocketServer::new(
237 ws_config.bind_address,
238 bus_manager.clone(),
239 entity_cache.clone(),
240 self.view_index.clone(),
241 );
242
243 if let Some(max_clients) = self.websocket_max_clients {
244 ws_server = ws_server.with_max_clients(max_clients);
245 }
246 if let Some(plugin) = self.websocket_auth_plugin.clone() {
247 ws_server = ws_server.with_auth_plugin(plugin);
248 }
249 if let Some(emitter) = self.websocket_usage_emitter.clone() {
250 ws_server = ws_server.with_usage_emitter(emitter);
251 }
252 if let Some(rate_limit_config) = self.websocket_rate_limit_config {
253 ws_server = ws_server.with_rate_limit_config(rate_limit_config);
254 }
255
256 let bind_addr = ws_config.bind_address;
257 ws_handle = Some(tokio::spawn(
258 async move {
259 if let Err(e) = ws_server.start().await {
260 error!("WebSocket server error: {}", e);
261 }
262 }
263 .instrument(info_span!("ws.server", %bind_addr)),
264 ));
265 }
266 }
267
268 if let Some(spec) = self.spec.as_ref() {
269 if let Some(parser_setup) = spec.parser_setup.clone() {
270 let program_id = spec
271 .program_ids
272 .first()
273 .cloned()
274 .unwrap_or_else(|| "unknown".to_string());
275 info!("Starting parser runtime for program: {}", program_id);
276 let health = health_monitor.clone();
277 let reconnection_config = self.config.reconnection.clone().unwrap_or_default();
278 parser_handle = Some(tokio::spawn(
279 async move {
280 if let Err(e) =
281 parser_setup(mutations_tx, health, reconnection_config).await
282 {
283 error!("Vixen parser runtime error: {}", e);
284 }
285 }
286 .instrument(info_span!("vixen.parser", %program_id)),
287 ));
288 } else {
289 info!("Spec provided but no parser_setup configured - skipping parser runtime");
290 }
291 } else {
292 info!("No spec provided - running in websocket-only mode");
293 }
294
295 let cleanup_bus = bus_manager.clone();
296 bus_cleanup_handle = Some(tokio::spawn(
297 async move {
298 let mut interval = tokio::time::interval(Duration::from_secs(60));
299 loop {
300 interval.tick().await;
301 let state_cleaned = cleanup_bus.cleanup_stale_state_buses().await;
302 let list_cleaned = cleanup_bus.cleanup_stale_list_buses().await;
303 if state_cleaned > 0 || list_cleaned > 0 {
304 let (state_count, list_count) = cleanup_bus.bus_counts().await;
305 info!(
306 "Bus cleanup: removed {} state, {} list buses. Current: {} state, {} list",
307 state_cleaned, list_cleaned, state_count, list_count
308 );
309 }
310 }
311 }
312 .instrument(info_span!("bus.cleanup")),
313 ));
314
315 stats_handle = Some(tokio::spawn(
316 async move {
317 let mut interval = tokio::time::interval(Duration::from_secs(30));
318 loop {
319 interval.tick().await;
320 let (_state_buses, _list_buses) = bus_manager.bus_counts().await;
321 let _cache_stats = entity_cache.stats().await;
322 }
323 }
324 .instrument(info_span!("stats.reporter")),
325 ));
326 } else {
327 info!(
328 "Live runtime disabled; projection and Yellowstone resources were not initialized"
329 );
330 }
331
332 let _http_health_handle = if let Some(http_health_config) = &self.config.http_health {
335 let mut http_server = HttpServer::new(http_health_config.bind_address)
336 .with_runtime_plan(plan)
337 .with_program_runtime_catalog(program_runtime_catalog);
338 if let Some(target_id) = self.config.program_read_binding_target_id.clone() {
339 http_server = http_server.with_program_read_binding_target(target_id);
340 }
341 if let Some(target_id) = self.config.solana_gateway_target_id.clone() {
342 http_server = http_server.with_solana_gateway_target(target_id);
343 }
344 if let Some(monitor) = health_monitor.clone() {
345 http_server = http_server.with_health_monitor(monitor);
346 }
347 if let Some(plugin) = self
348 .http_auth_plugin
349 .clone()
350 .or_else(|| self.websocket_auth_plugin.clone())
351 {
352 http_server = http_server.with_auth_plugin(plugin);
353 }
354 if plan.transactions && transaction_config.enabled {
355 http_server = http_server.with_transaction_config(transaction_config.clone());
356 }
357 #[cfg(feature = "otel")]
358 {
359 http_server = http_server.with_metrics(self.metrics.clone());
360 }
361
362 let bind_addr = http_health_config.bind_address;
363 let join_handle = std::thread::Builder::new()
364 .name("health-server".into())
365 .spawn(move || {
366 let rt = tokio::runtime::Builder::new_current_thread()
367 .enable_all()
368 .build()
369 .expect("Failed to create health server runtime");
370 rt.block_on(async move {
371 let _span = info_span!("http.health", %bind_addr).entered();
372 if let Err(e) = http_server.start().await {
373 error!("HTTP health server error: {}", e);
374 }
375 });
376 })
377 .expect("Failed to spawn health server thread");
378 info!(
379 "HTTP health server running on dedicated thread at {}",
380 bind_addr
381 );
382 Some(join_handle)
383 } else {
384 None
385 };
386
387 info!("Arete runtime is running. Press Ctrl+C to stop.");
388
389 async fn wait_for_task(handle: Option<tokio::task::JoinHandle<()>>) {
390 if let Some(handle) = handle {
391 let _ = handle.await;
392 } else {
393 std::future::pending().await
394 }
395 }
396
397 tokio::select! {
398 _ = wait_for_task(ws_handle) => info!("WebSocket server task completed"),
399 _ = wait_for_task(projector_handle) => info!("Projector task completed"),
400 _ = wait_for_task(parser_handle) => info!("Parser runtime task completed"),
401 _ = wait_for_task(bus_cleanup_handle) => info!("Bus cleanup task completed"),
402 _ = wait_for_task(stats_handle) => info!("Stats reporter task completed"),
403 _ = shutdown_signal() => {}
404 }
405 drop(mutations_tx_guard);
406
407 info!("Shutting down Arete runtime");
408 Ok(())
409 }
410}