use std::sync::Arc; use std::time::Duration; use futures::StreamExt; use tracing::{error, info, warn}; use application::processing::ProcessNextJobCommand; use domain::events::DomainEvent; use domain::ports::EventConsumer; mod config; mod factories; mod plugin_registry; mod plugins; use factories::{Repos, build_plugin_registry, build_process_next_handler}; #[tokio::main] async fn main() -> anyhow::Result<()> { tracing_subscriber::fmt() .with_env_filter( tracing_subscriber::EnvFilter::from_default_env().add_directive("worker=info".parse()?), ) .init(); let config = config::WorkerConfig::from_env(); info!("Worker starting"); let pool = adapters_postgres::connect(&config.database_url).await?; adapters_postgres::run_migrations(&pool).await?; let nats_client = async_nats::connect(&config.nats_url).await?; adapters_nats::ensure_stream(&nats_client).await?; info!(nats_url = %config.nats_url, "NATS connected"); let event_store: Arc = Arc::new(adapters_postgres::PostgresEventStore::new(pool.clone())); let repos = Repos::new(pool); let file_storage = Arc::new(adapters_storage::LocalFileStorage::new( &config.storage_path, )); let sidecar_writer: Arc = Arc::new(LogSidecarWriter); // Publisher transport consumes a client clone; the consumer gets another. let pub_transport = adapters_nats::NatsTransport::new(nats_client.clone()); let nats_publisher: Arc = Arc::new(event_transport::EventPublisherAdapter::new(pub_transport)); let event_pub: Arc = Arc::new(event_transport::CompositeEventPublisher::new(nats_publisher, event_store)); let registry = Arc::new(build_plugin_registry(&repos, file_storage, sidecar_writer)); let process_next = Arc::new(build_process_next_handler(&repos, registry, event_pub)); // ── Fallback sweep task ──────────────────────────────────────────── let sweep_interval = Duration::from_secs(config.fallback_sweep_secs); let sweep_handler = Arc::clone(&process_next); tokio::spawn(async move { info!(every_secs = config.fallback_sweep_secs, "fallback sweep task started"); loop { tokio::time::sleep(sweep_interval).await; info!("fallback sweep: draining queued jobs"); loop { match sweep_handler.execute(ProcessNextJobCommand).await { Ok(Some(job)) => { info!(job_id = %job.job_id, status = ?job.status, "sweep: processed job"); } Ok(None) => break, Err(e) => { error!(error = %e, "sweep: error processing job"); break; } } } } }); // ── Event-driven loop via NATS ───────────────────────────────────── let consumer_source = adapters_nats::NatsMessageSource::new(nats_client); let event_consumer = event_transport::EventConsumerAdapter::new(consumer_source); info!("event loop: listening for NATS events"); let mut stream = event_consumer.consume(); while let Some(result) = stream.next().await { let envelope = match result { Ok(env) => env, Err(e) => { error!(error = %e, "event loop: consumer error"); continue; } }; match &envelope.event { DomainEvent::JobEnqueued { job_id, job_type, .. } => { info!(job_id = %job_id, job_type = %job_type, "event loop: JobEnqueued received"); (envelope.ack)(); match process_next.execute(ProcessNextJobCommand).await { Ok(Some(job)) => { info!(job_id = %job.job_id, status = ?job.status, "event loop: processed job"); } Ok(None) => { warn!("event loop: JobEnqueued event but no queued job found"); } Err(e) => { error!(error = %e, "event loop: error processing job"); } } } other => { info!(event = ?other, "event loop: non-job event, acking"); (envelope.ack)(); } } } error!("event loop: NATS stream ended unexpectedly"); Ok(()) } struct LogSidecarWriter; #[async_trait::async_trait] impl domain::ports::SidecarWriterPort for LogSidecarWriter { fn format_name(&self) -> &str { "log_noop" } async fn write_sidecar( &self, _data: &domain::value_objects::StructuredData, path: &str, ) -> Result<(), domain::errors::DomainError> { info!(path, "sidecar write (no-op)"); Ok(()) } async fn read_sidecar( &self, path: &str, ) -> Result { info!(path, "sidecar read (no-op)"); Ok(domain::value_objects::StructuredData::new()) } }