Files
k-photos/crates/worker/src/bootstrap.rs
Gabriel Kaszewski c251a5c41f perf: concurrent worker with claim/execute split + graceful shutdown
- 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)
2026-06-01 02:14:44 +02:00

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,
})
}