- JobRepository::claim_next() — atomic SELECT FOR UPDATE SKIP LOCKED + UPDATE status=processing in one query, no duplicate claims - ExecutePipelineHandler skips start() for already-claimed jobs - Sweep spawns N concurrent tasks via JoinSet, claims are fast+sequential, execution is slow+concurrent - Graceful shutdown: stop claiming, await all in-flight JoinSet tasks - WORKER_CONCURRENCY env (default: CPU cores) - DB_MAX_CONNECTIONS env (default: 20, was hardcoded 10) - VolumeFileResolver impl for InMemoryFileStorage (test fix)
88 lines
3.3 KiB
Rust
88 lines
3.3 KiB
Rust
use std::sync::Arc;
|
|
|
|
use application::catalog::DeleteAssetHandler;
|
|
use application::processing::{EnqueueJobHandler, ExecutePipelineHandler};
|
|
use domain::ports::{AssetRepository, JobRepository};
|
|
|
|
use crate::config::WorkerConfig;
|
|
use crate::factories::{
|
|
Repos, build_enqueue_handler, build_executor, build_plugin_registry,
|
|
};
|
|
|
|
pub struct WorkerServices {
|
|
pub executor: Arc<ExecutePipelineHandler>,
|
|
pub enqueue: Arc<EnqueueJobHandler>,
|
|
pub job_repo: Arc<dyn JobRepository>,
|
|
pub asset_repo: Arc<dyn AssetRepository>,
|
|
pub delete_handler: Arc<DeleteAssetHandler>,
|
|
pub trash_retention_days: u64,
|
|
pub event_consumer:
|
|
adapters_event_transport::EventConsumerAdapter<adapters_nats::NatsMessageSource>,
|
|
}
|
|
|
|
pub async fn build(config: &WorkerConfig) -> anyhow::Result<WorkerServices> {
|
|
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?;
|
|
tracing::info!(nats_url = %config.nats_url, "NATS connected");
|
|
|
|
let event_store: Arc<dyn domain::ports::EventStore> =
|
|
Arc::new(adapters_postgres::PostgresEventStore::new(pool.clone()));
|
|
let repos = Repos::new(pool);
|
|
let file_storage: Arc<dyn domain::ports::FileStoragePort> =
|
|
Arc::new(adapters_storage::LocalFileStorage::new(&config.storage_path));
|
|
let sidecar_writer: Arc<dyn domain::ports::SidecarWriterPort> =
|
|
Arc::new(adapters_sidecar::XmpSidecarWriter);
|
|
|
|
let pub_transport = adapters_nats::NatsTransport::new(nats_client.clone());
|
|
let nats_publisher: Arc<dyn domain::ports::EventPublisher> = Arc::new(
|
|
adapters_event_transport::EventPublisherAdapter::new(pub_transport),
|
|
);
|
|
let event_pub: Arc<dyn domain::ports::EventPublisher> = Arc::new(
|
|
adapters_event_transport::CompositeEventPublisher::new(nats_publisher, event_store),
|
|
);
|
|
|
|
let extractor: Arc<dyn domain::ports::MetadataExtractorPort> =
|
|
Arc::new(adapters_exif::NomExifExtractor);
|
|
let thumbnail_gen: Arc<dyn domain::ports::ThumbnailGeneratorPort> =
|
|
Arc::new(adapters_thumbnail::ImageThumbnailGenerator);
|
|
let registry = Arc::new(build_plugin_registry(
|
|
&repos,
|
|
file_storage.clone(),
|
|
sidecar_writer,
|
|
extractor,
|
|
thumbnail_gen,
|
|
event_pub.clone(),
|
|
));
|
|
|
|
let executor = Arc::new(build_executor(&repos, registry, event_pub.clone()));
|
|
let job_repo: Arc<dyn JobRepository> = repos.job.clone();
|
|
let asset_repo: Arc<dyn AssetRepository> = repos.asset.clone();
|
|
let enqueue = Arc::new(build_enqueue_handler(&repos, event_pub.clone()));
|
|
|
|
let sidecar_repo: Arc<dyn domain::ports::SidecarRepository> = repos.sidecar.clone();
|
|
let delete_handler = Arc::new(DeleteAssetHandler::new(
|
|
repos.asset.clone(),
|
|
repos.volume.clone(),
|
|
repos.derivative.clone(),
|
|
sidecar_repo,
|
|
file_storage,
|
|
event_pub,
|
|
));
|
|
|
|
let consumer_source = adapters_nats::NatsMessageSource::new(nats_client);
|
|
let event_consumer = adapters_event_transport::EventConsumerAdapter::new(consumer_source);
|
|
|
|
Ok(WorkerServices {
|
|
executor,
|
|
enqueue,
|
|
job_repo,
|
|
asset_repo,
|
|
delete_handler,
|
|
trash_retention_days: config.trash_retention_days,
|
|
event_consumer,
|
|
})
|
|
}
|