Skip to main content

arete_server/
runtime.rs

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
26/// Wait for shutdown signal (SIGINT on all platforms, SIGTERM on Unix)
27async 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    /// Configure rate limiting for WebSocket connections.
143    ///
144    /// This sets global rate limits such as maximum connections per IP,
145    /// timeouts, and rate windows. Per-subject limits are controlled
146    /// via AuthContext.Limits from the authentication token.
147    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    /// Return the immutable capability plan selected by the builder.
153    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        // Run the HTTP server on a dedicated OS thread with its own single-threaded
333        // tokio runtime so liveness remains responsive under projection load.
334        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}