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
24async 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 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 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 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}