Skip to main content

arete_server/
runtime.rs

1use crate::bus::BusManager;
2use crate::cache::EntityCache;
3use crate::config::ServerConfig;
4use crate::health::HealthMonitor;
5use crate::http_server::HttpServer;
6use crate::materialized_view::MaterializedViewRegistry;
7use crate::mutation_batch::MutationBatch;
8use crate::projector::Projector;
9use crate::view::ViewIndex;
10use crate::websocket::client_manager::RateLimitConfig;
11use crate::websocket::WebSocketServer;
12use crate::Spec;
13use crate::WebSocketAuthPlugin;
14use crate::WebSocketUsageEmitter;
15use anyhow::Result;
16use std::sync::Arc;
17use std::time::Duration;
18use tokio::sync::mpsc;
19use tracing::{error, info, info_span, Instrument};
20
21#[cfg(feature = "otel")]
22use crate::metrics::Metrics;
23
24/// Wait for shutdown signal (SIGINT on all platforms, SIGTERM on Unix)
25async fn shutdown_signal() {
26    let ctrl_c = async {
27        tokio::signal::ctrl_c()
28            .await
29            .expect("Failed to install Ctrl+C handler");
30    };
31
32    #[cfg(unix)]
33    let terminate = async {
34        tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
35            .expect("Failed to install SIGTERM handler")
36            .recv()
37            .await;
38    };
39
40    #[cfg(not(unix))]
41    let terminate = std::future::pending::<()>();
42
43    tokio::select! {
44        _ = ctrl_c => {
45            info!("Received SIGINT (Ctrl+C), initiating shutdown");
46        }
47        _ = terminate => {
48            info!("Received SIGTERM, initiating graceful shutdown");
49        }
50    }
51}
52
53pub struct Runtime {
54    config: ServerConfig,
55    view_index: Arc<ViewIndex>,
56    spec: Option<Spec>,
57    materialized_views: Option<MaterializedViewRegistry>,
58    websocket_auth_plugin: Option<Arc<dyn WebSocketAuthPlugin>>,
59    http_auth_plugin: Option<Arc<dyn WebSocketAuthPlugin>>,
60    websocket_usage_emitter: Option<Arc<dyn WebSocketUsageEmitter>>,
61    websocket_max_clients: Option<usize>,
62    websocket_rate_limit_config: Option<RateLimitConfig>,
63    #[cfg(feature = "otel")]
64    metrics: Option<Arc<Metrics>>,
65}
66
67impl Runtime {
68    #[cfg(feature = "otel")]
69    pub fn new(config: ServerConfig, view_index: ViewIndex, metrics: Option<Arc<Metrics>>) -> Self {
70        Self {
71            config,
72            view_index: Arc::new(view_index),
73            spec: None,
74            materialized_views: None,
75            websocket_auth_plugin: None,
76            http_auth_plugin: None,
77            websocket_usage_emitter: None,
78            websocket_max_clients: None,
79            websocket_rate_limit_config: None,
80            metrics,
81        }
82    }
83
84    #[cfg(not(feature = "otel"))]
85    pub fn new(config: ServerConfig, view_index: ViewIndex) -> Self {
86        Self {
87            config,
88            view_index: Arc::new(view_index),
89            spec: None,
90            materialized_views: None,
91            websocket_auth_plugin: None,
92            http_auth_plugin: None,
93            websocket_usage_emitter: None,
94            websocket_max_clients: None,
95            websocket_rate_limit_config: None,
96        }
97    }
98
99    pub fn with_spec(mut self, spec: Spec) -> Self {
100        self.spec = Some(spec);
101        self
102    }
103
104    pub fn with_materialized_views(mut self, registry: MaterializedViewRegistry) -> Self {
105        self.materialized_views = Some(registry);
106        self
107    }
108
109    pub fn with_websocket_auth_plugin(
110        mut self,
111        websocket_auth_plugin: Arc<dyn WebSocketAuthPlugin>,
112    ) -> Self {
113        self.websocket_auth_plugin = Some(websocket_auth_plugin);
114        self
115    }
116
117    pub fn with_http_auth_plugin(mut self, http_auth_plugin: Arc<dyn WebSocketAuthPlugin>) -> Self {
118        self.http_auth_plugin = Some(http_auth_plugin);
119        self
120    }
121
122    pub fn with_websocket_usage_emitter(
123        mut self,
124        websocket_usage_emitter: Arc<dyn WebSocketUsageEmitter>,
125    ) -> Self {
126        self.websocket_usage_emitter = Some(websocket_usage_emitter);
127        self
128    }
129
130    pub fn with_websocket_max_clients(mut self, websocket_max_clients: usize) -> Self {
131        self.websocket_max_clients = Some(websocket_max_clients);
132        self
133    }
134
135    /// Configure rate limiting for WebSocket connections.
136    ///
137    /// This sets global rate limits such as maximum connections per IP,
138    /// timeouts, and rate windows. Per-subject limits are controlled
139    /// via AuthContext.Limits from the authentication token.
140    pub fn with_websocket_rate_limit_config(mut self, config: RateLimitConfig) -> Self {
141        self.websocket_rate_limit_config = Some(config);
142        self
143    }
144
145    pub async fn run(self) -> Result<()> {
146        info!("Starting Arete runtime");
147
148        let (mutations_tx, mutations_rx) = mpsc::channel::<MutationBatch>(1024);
149
150        let bus_manager = BusManager::new();
151        let entity_cache = EntityCache::new();
152
153        let health_monitor = if let Some(health_config) = &self.config.health {
154            let monitor = HealthMonitor::new(health_config.clone());
155            let _health_task = monitor.start().await;
156            info!("Health monitoring enabled");
157            Some(monitor)
158        } else {
159            None
160        };
161
162        #[cfg(feature = "otel")]
163        let projector = Projector::new(
164            self.view_index.clone(),
165            bus_manager.clone(),
166            entity_cache.clone(),
167            mutations_rx,
168            self.metrics.clone(),
169        );
170        #[cfg(not(feature = "otel"))]
171        let projector = Projector::new(
172            self.view_index.clone(),
173            bus_manager.clone(),
174            entity_cache.clone(),
175            mutations_rx,
176        );
177
178        let projector_handle = tokio::spawn(
179            async move {
180                projector.run().await;
181            }
182            .instrument(info_span!("projector")),
183        );
184
185        let ws_handle = if let Some(ws_config) = &self.config.websocket {
186            #[cfg(feature = "otel")]
187            let mut ws_server = WebSocketServer::new(
188                ws_config.bind_address,
189                bus_manager.clone(),
190                entity_cache.clone(),
191                self.view_index.clone(),
192                self.metrics.clone(),
193            );
194            #[cfg(not(feature = "otel"))]
195            let mut ws_server = WebSocketServer::new(
196                ws_config.bind_address,
197                bus_manager.clone(),
198                entity_cache.clone(),
199                self.view_index.clone(),
200            );
201
202            if let Some(max_clients) = self.websocket_max_clients {
203                ws_server = ws_server.with_max_clients(max_clients);
204            }
205
206            if let Some(plugin) = self.websocket_auth_plugin.clone() {
207                ws_server = ws_server.with_auth_plugin(plugin);
208            }
209
210            if let Some(emitter) = self.websocket_usage_emitter.clone() {
211                ws_server = ws_server.with_usage_emitter(emitter);
212            }
213
214            if let Some(rate_limit_config) = self.websocket_rate_limit_config {
215                ws_server = ws_server.with_rate_limit_config(rate_limit_config);
216            }
217
218            let bind_addr = ws_config.bind_address;
219            Some(tokio::spawn(
220                async move {
221                    if let Err(e) = ws_server.start().await {
222                        error!("WebSocket server error: {}", e);
223                    }
224                }
225                .instrument(info_span!("ws.server", %bind_addr)),
226            ))
227        } else {
228            None
229        };
230
231        let parser_handle = if let Some(spec) = self.spec.as_ref() {
232            if let Some(parser_setup) = spec.parser_setup.clone() {
233                let program_id = spec
234                    .program_ids
235                    .first()
236                    .cloned()
237                    .unwrap_or_else(|| "unknown".to_string());
238                info!("Starting parser runtime for program: {}", program_id);
239                let tx = mutations_tx.clone();
240                let health = health_monitor.clone();
241                let reconnection_config = self.config.reconnection.clone().unwrap_or_default();
242                Some(tokio::spawn(
243                    async move {
244                        if let Err(e) = parser_setup(tx, health, reconnection_config).await {
245                            error!("Vixen parser runtime error: {}", e);
246                        }
247                    }
248                    .instrument(info_span!("vixen.parser", %program_id)),
249                ))
250            } else {
251                info!("Spec provided but no parser_setup configured - skipping parser runtime");
252                None
253            }
254        } else {
255            info!("No spec provided - running in websocket-only mode");
256            None
257        };
258
259        // Run the HTTP health server on a dedicated OS thread with its own single-threaded
260        // tokio runtime. This isolates it from the main runtime so that liveness probes
261        // always respond even when the event processing pipeline saturates worker threads
262        // (e.g. due to std::sync::Mutex contention on VmContext under high throughput).
263        let _http_health_handle = if let Some(http_health_config) = &self.config.http_health {
264            let mut http_server = HttpServer::new(http_health_config.bind_address);
265            if let Some(monitor) = health_monitor.clone() {
266                http_server = http_server.with_health_monitor(monitor);
267            }
268            if let Some(spec) = &self.spec {
269                if let Some(reader) = spec.program_account_reader.clone() {
270                    http_server = http_server.with_program_account_reader(reader);
271                }
272            }
273            if let Some(plugin) = self
274                .http_auth_plugin
275                .clone()
276                .or_else(|| self.websocket_auth_plugin.clone())
277            {
278                http_server = http_server.with_auth_plugin(plugin);
279            }
280
281            let bind_addr = http_health_config.bind_address;
282            let join_handle = std::thread::Builder::new()
283                .name("health-server".into())
284                .spawn(move || {
285                    let rt = tokio::runtime::Builder::new_current_thread()
286                        .enable_all()
287                        .build()
288                        .expect("Failed to create health server runtime");
289                    rt.block_on(async move {
290                        let _span = info_span!("http.health", %bind_addr).entered();
291                        if let Err(e) = http_server.start().await {
292                            error!("HTTP health server error: {}", e);
293                        }
294                    });
295                })
296                .expect("Failed to spawn health server thread");
297            info!(
298                "HTTP health server running on dedicated thread at {}",
299                bind_addr
300            );
301            Some(join_handle)
302        } else {
303            None
304        };
305
306        let bus_cleanup_handle = {
307            let bus = bus_manager.clone();
308            tokio::spawn(
309                async move {
310                    let mut interval = tokio::time::interval(Duration::from_secs(60));
311                    loop {
312                        interval.tick().await;
313                        let state_cleaned = bus.cleanup_stale_state_buses().await;
314                        let list_cleaned = bus.cleanup_stale_list_buses().await;
315                        if state_cleaned > 0 || list_cleaned > 0 {
316                            let (state_count, list_count) = bus.bus_counts().await;
317                            info!(
318                                "Bus cleanup: removed {} state, {} list buses. Current: {} state, {} list",
319                                state_cleaned, list_cleaned, state_count, list_count
320                            );
321                        }
322                    }
323                }
324                .instrument(info_span!("bus.cleanup")),
325            )
326        };
327
328        let stats_handle = {
329            let bus = bus_manager.clone();
330            let cache = entity_cache.clone();
331            tokio::spawn(
332                async move {
333                    let mut interval = tokio::time::interval(Duration::from_secs(30));
334                    loop {
335                        interval.tick().await;
336                        let (_state_buses, _list_buses) = bus.bus_counts().await;
337                        let _cache_stats = cache.stats().await;
338                    }
339                }
340                .instrument(info_span!("stats.reporter")),
341            )
342        };
343
344        info!("Arete runtime is running. Press Ctrl+C to stop.");
345
346        // Wait for any task to complete (or handle shutdown signals)
347        tokio::select! {
348            _ = async {
349                if let Some(handle) = ws_handle {
350                    handle.await
351                } else {
352                    std::future::pending().await
353                }
354            } => {
355                info!("WebSocket server task completed");
356            }
357            _ = projector_handle => {
358                info!("Projector task completed");
359            }
360            _ = async {
361                if let Some(handle) = parser_handle {
362                    handle.await
363                } else {
364                    std::future::pending().await
365                }
366            } => {
367                info!("Parser runtime task completed");
368            }
369            _ = bus_cleanup_handle => {
370                info!("Bus cleanup task completed");
371            }
372            _ = stats_handle => {
373                info!("Stats reporter task completed");
374            }
375            _ = shutdown_signal() => {}
376        }
377
378        info!("Shutting down Arete runtime");
379        Ok(())
380    }
381}