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