1use std::net::SocketAddr;
21use std::sync::Arc;
22
23use crate::config::ConfigurationRegistrant;
24use crate::controller::RouteController;
25use crate::data::cache::RedisStorage;
26use crate::doc::DocumentationRegistrant;
27use crate::env::AppEnvironment;
28use crate::gateway::GatewayConnect;
29use crate::job::JobRegistry;
30
31#[derive(Debug, Clone)]
37pub struct StartupOptions {
38 pub env_file: Option<String>,
40 pub enable_http: bool,
42 pub enable_jobs: bool,
44 pub enable_consumers: bool,
46 pub service_protocol: String,
48 pub service_weight: i32,
50 pub auth_type: String,
52 pub worker_max_execute_time_minutes: u64,
54 pub event_loop_max_execute_time_minutes: u64,
56 pub blocked_thread_check_interval_millis: u64,
58 pub worker_pool_size: usize,
60 pub event_loop_pool_size: usize,
62}
63
64impl Default for StartupOptions {
65 fn default() -> Self {
66 Self::default_options()
67 }
68}
69
70impl StartupOptions {
71 pub fn default_options() -> Self {
73 Self {
74 env_file: None,
75 enable_http: true,
76 enable_jobs: true,
77 enable_consumers: false,
78 service_protocol: "http".to_string(),
79 service_weight: 1,
80 auth_type: "token".to_string(),
81 worker_max_execute_time_minutes: 2,
82 event_loop_max_execute_time_minutes: 1,
83 blocked_thread_check_interval_millis: 750,
84 worker_pool_size: 20,
87 event_loop_pool_size: 16,
88 }
89 }
90
91 pub fn with_env_file(mut self, path: impl Into<String>) -> Self {
93 self.env_file = Some(path.into());
94 self
95 }
96 pub fn with_http(mut self, enabled: bool) -> Self {
98 self.enable_http = enabled;
99 self
100 }
101 pub fn with_jobs(mut self, enabled: bool) -> Self {
103 self.enable_jobs = enabled;
104 self
105 }
106 pub fn with_consumers(mut self, enabled: bool) -> Self {
108 self.enable_consumers = enabled;
109 self
110 }
111}
112
113pub struct GenericStartup {
120 pub options: StartupOptions,
122 pub mount_paths: Vec<String>,
124 pub registrant: Option<Arc<ConfigurationRegistrant>>,
126 pub job_registry: JobRegistry,
128 pub gateway: Option<Arc<GatewayConnect>>,
130 pub redis: Option<RedisStorage>,
132 pub server_addr: Option<SocketAddr>,
134 pub socket_addr: Option<SocketAddr>,
136 pub consumer_names: Vec<String>,
138 docs_built: bool,
139}
140
141impl GenericStartup {
142 pub fn new(options: StartupOptions) -> Self {
144 Self {
145 options,
146 mount_paths: Vec::new(),
147 registrant: None,
148 job_registry: JobRegistry::new(),
149 gateway: None,
150 redis: None,
151 server_addr: None,
152 socket_addr: None,
153 consumer_names: Vec::new(),
154 docs_built: false,
155 }
156 }
157
158 pub fn with_mount_paths(mut self, paths: Vec<String>) -> Self {
160 self.mount_paths = paths;
161 self
162 }
163
164 pub async fn init(&mut self) -> anyhow::Result<()> {
172 AppEnvironment::with_env_file(self.options.env_file.as_deref())?;
173 Self::init_tracing();
174 Self::log_pool_options(&self.options);
175 let env = AppEnvironment::get();
176 let addr: SocketAddr = format!("0.0.0.0:{}", env.server_port).parse()?;
177 let socket_addr: SocketAddr = format!("0.0.0.0:{}", env.socket_port).parse()?;
178 self.registrant = Some(Arc::new(ConfigurationRegistrant::new(addr)));
179 self.server_addr = Some(addr);
180 self.socket_addr = Some(socket_addr);
181
182 if let Ok(mut reg) = DocumentationRegistrant::global().write() {
184 reg.bind_base_list(&self.mount_paths);
185 }
186
187 match RedisStorage::from_env() {
189 Ok(storage) => {
190 tracing::info!(
191 component = "cache",
192 backend = "redis",
193 "Redis storage initialized"
194 );
195 self.redis = Some(storage);
196 }
197 Err(e) => {
198 tracing::warn!(component = "cache", backend = "redis", error = %e, "Redis unavailable; continuing without cache");
199 }
200 }
201 Ok(())
202 }
203
204 fn init_tracing() {
205 use tracing_subscriber::EnvFilter;
206 use tracing_subscriber::layer::SubscriberExt;
207 use tracing_subscriber::util::SubscriberInitExt;
208 let (env_filter, filter_source) = match EnvFilter::try_from_default_env() {
210 Ok(filter) => (filter, "RUST_LOG"),
211 Err(_) => (EnvFilter::new("INFO"), "DEFAULT"),
212 };
213 let _ = tracing_subscriber::registry()
214 .with(env_filter)
215 .with(tracing_subscriber::fmt::layer().with_ansi(true))
216 .try_init();
217 tracing::info!(filter_source, "Tracing initialized");
218 }
219
220 fn log_pool_options(options: &StartupOptions) {
221 if options.worker_pool_size == 0 || options.event_loop_pool_size == 0 {
224 tracing::warn!(
225 worker_pool_size = options.worker_pool_size,
226 event_loop_pool_size = options.event_loop_pool_size,
227 "Invalid runtime pool-size hints; Tokio runtime sizing is owned by the host binary"
228 );
229 } else {
230 tracing::info!(
231 worker_max_execute_time_minutes = options.worker_max_execute_time_minutes,
232 event_loop_max_execute_time_minutes = options.event_loop_max_execute_time_minutes,
233 blocked_thread_check_interval_ms = options.blocked_thread_check_interval_millis,
234 worker_pool_size = options.worker_pool_size,
235 event_loop_pool_size = options.event_loop_pool_size,
236 "Configured runtime pool-size hints"
237 );
238 }
239 }
240
241 pub async fn bootstrap(
254 options: StartupOptions,
255 mount_paths: Vec<String>,
256 static_registrar: Option<Arc<dyn StaticRegistrar>>,
257 controller_registrar: Option<Arc<dyn ControllerRegistrar>>,
258 consumer_registrar: Option<Arc<dyn ConsumerRegistrar>>,
259 ) -> anyhow::Result<Self> {
260 let mut startup = Self::new(options);
261 startup.mount_paths = mount_paths.clone();
262 startup.init().await?;
264
265 let env = AppEnvironment::get();
266 let cpu_count = std::thread::available_parallelism()
267 .map(|n| n.get())
268 .unwrap_or(1);
269
270 if let Some(sr) = static_registrar.as_ref() {
272 if let Some(reg) = startup.registrant.as_ref() {
273 let handle = reg.router_handle();
274 let mut router = handle.write().await;
275 sr.register_static(&startup, &mut router);
276 }
277 }
278 if let Ok(mut reg) = DocumentationRegistrant::global().write() {
280 reg.bind_base_list(&mount_paths);
281 }
282
283 match GatewayConnect::set_up(
285 mount_paths.clone(),
286 startup.options.service_protocol.clone(),
287 startup.options.service_weight,
288 startup.options.auth_type.clone(),
289 )
290 .await
291 {
292 Ok(Some(gw)) => {
293 tracing::info!(
294 component = "gateway",
295 mount_path_count = mount_paths.len(),
296 "Registered service configuration"
297 );
298 startup.gateway = Some(gw);
299 }
300 Ok(None) => {
301 tracing::info!(
302 component = "gateway",
303 "No mount paths configured; skipped service registration"
304 );
305 }
306 Err(e) => {
307 tracing::error!(component = "gateway", error = %e, "Failed to register service configuration");
308 if !env.is_production() {
309 eprintln!("{:?}", e);
310 }
311 }
312 }
313
314 let deployable_count = env.server_count + env.socket_count + env.worker_count;
316 if deployable_count > cpu_count {
317 tracing::warn!(
318 deployable_count,
319 cpu_count,
320 "Configured deployables exceed available CPU cores"
321 );
322 }
323
324 if env.server_count > 0 && startup.options.enable_http {
326 if let Some(cr) = controller_registrar.as_ref() {
327 let controllers = cr.controllers(&startup);
328 for ctrl in controllers {
329 startup.mount_controller_boxed(ctrl).await?;
330 }
331 }
332 startup
334 .mount_controller_boxed(Box::new(crate::doc::controller::DocumentationController))
335 .await?;
336 let timer = std::time::Instant::now();
338 crate::doc::controller::DocumentationController::build_specs();
339 startup.docs_built = true;
340 tracing::info!(
341 component = "openapi",
342 duration_ms = timer.elapsed().as_millis() as u64,
343 "Built OpenAPI documentation"
344 );
345 }
346
347 if env.worker_count > 0 {
349 tracing::info!(
350 worker_count = env.worker_count,
351 worker_pool_size = startup.options.worker_pool_size,
352 "Jobs are available via run_jobs()"
353 );
354 }
355
356 if let Some(cons_reg) = consumer_registrar.as_ref() {
361 if startup.options.enable_consumers {
362 let consumers = cons_reg.consumers(&startup);
363 for c in &consumers {
364 tracing::info!(queue = %c.queue_name(), "Registered consumer");
365 startup.consumer_names.push(c.queue_name().to_string());
366 }
367 if consumers.is_empty() {
368 tracing::info!("Consumer registrar provided no consumers");
369 }
370 } else {
371 tracing::info!(enabled = false, "Consumer registration skipped");
372 }
373 }
374
375 if env.socket_count > 0 {
377 tracing::info!(
378 socket_count = env.socket_count,
379 socket_addr = ?startup.socket_addr,
380 "Socket server is available for binding"
381 );
382 }
383
384 Ok(startup)
385 }
386
387 pub async fn mount_controller<C: RouteController + 'static>(&self, c: C) -> anyhow::Result<()> {
389 if let Some(r) = &self.registrant {
390 r.mount_controller(c).await;
391 Ok(())
392 } else {
393 anyhow::bail!("not initialized — call init() or bootstrap() first")
394 }
395 }
396
397 pub async fn mount_controller_boxed(&self, c: Box<dyn RouteController>) -> anyhow::Result<()> {
399 if let Some(r) = &self.registrant {
400 let handle = r.router_handle();
401 let mut router = handle.write().await;
402 tracing::info!(
403 target: "routing",
404 handler = c.type_name(),
405 path = c.base_path(),
406 "Mounted controller '{}' at '{}'",
407 c.type_name(),
408 c.base_path()
409 );
410 c.register_routes(&mut router).await;
411 Ok(())
412 } else {
413 anyhow::bail!("not initialized")
414 }
415 }
416
417 pub async fn serve(&self) -> anyhow::Result<SocketAddr> {
419 if let Some(r) = self.registrant.clone() {
420 Ok(r.serve().await?)
421 } else {
422 anyhow::bail!("not initialized")
423 }
424 }
425
426 pub async fn shutdown(&mut self) -> anyhow::Result<()> {
428 if let Err(e) = self.job_registry.stop().await {
429 tracing::warn!(component = "jobs", error = %e, "Failed to stop jobs during shutdown");
430 }
431 if let Some(gw) = self.gateway.take() {
432 if let Err(e) = gw.deregister().await {
433 tracing::warn!(component = "gateway", error = %e, "Failed to deregister service during shutdown");
434 }
435 }
436 tracing::info!("Shutdown complete");
437 Ok(())
438 }
439
440 pub fn job_registry(&mut self) -> &mut JobRegistry {
442 &mut self.job_registry
443 }
444 pub fn add_job<J: crate::job::ServiceJob + 'static>(&mut self, job: J) {
446 self.job_registry.add_job(job);
447 }
448 pub async fn run_jobs(&mut self) -> anyhow::Result<()> {
450 if !self.options.enable_jobs {
451 tracing::info!(enabled = false, "Job startup skipped");
452 return Ok(());
453 }
454 if self.job_registry.job_count() == 0 {
455 tracing::info!("No jobs registered; nothing to start");
456 return Ok(());
457 }
458 tracing::info!(job_count = self.job_registry.job_count(), "Starting jobs");
459 self.job_registry.start().await?;
460 Ok(())
461 }
462 pub async fn start_jobs(&mut self) -> anyhow::Result<()> {
464 self.run_jobs().await
465 }
466 pub async fn stop_jobs(&mut self) -> anyhow::Result<()> {
468 self.job_registry.stop().await?;
469 Ok(())
470 }
471
472 pub fn registrant(&self) -> Option<Arc<ConfigurationRegistrant>> {
475 self.registrant.clone()
476 }
477 pub fn gateway_client(&self) -> Option<Arc<GatewayConnect>> {
479 self.gateway.clone()
480 }
481 pub fn redis_client(&self) -> Option<RedisStorage> {
483 self.redis.clone()
484 }
485 pub fn server_addr(&self) -> Option<SocketAddr> {
487 self.server_addr
488 }
489 pub fn socket_addr(&self) -> Option<SocketAddr> {
491 self.socket_addr
492 }
493 pub fn router_handle(&self) -> Option<Arc<tokio::sync::RwLock<crate::controller::Router>>> {
495 self.registrant.as_ref().map(|r| r.router_handle())
496 }
497 pub fn is_production(&self) -> bool {
499 AppEnvironment::try_get()
500 .map(|e| e.is_production())
501 .unwrap_or(false)
502 }
503 pub fn docs_built(&self) -> bool {
505 self.docs_built
506 }
507 pub fn server_urls(&self) -> Vec<String> {
509 let port = AppEnvironment::try_get()
510 .map(|e| e.server_port)
511 .unwrap_or(8080);
512 vec![
513 format!("http://localhost:{port}"),
514 format!("http://127.0.0.1:{port}"),
515 ]
516 }
517}
518
519pub trait ControllerRegistrar: Send + Sync {
524 fn controllers(&self, startup: &GenericStartup) -> Vec<Box<dyn RouteController>>;
526}
527
528pub trait ConsumerRegistrar: Send + Sync {
533 fn consumers(
535 &self,
536 startup: &GenericStartup,
537 ) -> Vec<Box<dyn queue_descriptor::QueueDescriptor>>;
538}
539
540pub trait StaticRegistrar: Send + Sync {
545 fn register_static(&self, startup: &GenericStartup, router: &mut crate::controller::Router);
547}
548
549pub mod queue_descriptor {
551 pub trait QueueDescriptor: Send + Sync {
553 fn queue_name(&self) -> &str;
555 }
556}