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