feat: worker plugin system — domain ports, pipeline executor, built-in plugins

- PluginExecutor + PluginRegistry ports in domain
- ExecutePipelineCommand orchestrates job→pipeline→plugin steps
- ProcessNextJobCommand polls + executes next queued job
- InMemoryPluginRegistry, NoOp/MetadataExtractor/SidecarSync plugins
- Worker main rewritten with poll loop, factories module for DI
- Deleted template job/runner/jobs remnants
This commit is contained in:
2026-05-31 11:35:05 +02:00
parent 6c88ac344c
commit dacfc3d453
22 changed files with 587 additions and 90 deletions

View File

@@ -1,14 +1,14 @@
use std::sync::Arc;
use std::time::Duration;
use tracing::info;
use tracing::{error, info};
mod config;
mod job;
mod jobs;
mod runner;
mod factories;
mod plugin_registry;
mod plugins;
use jobs::ExampleJob;
use runner::JobRunner;
use application::processing::ProcessNextJobCommand;
use factories::{Repos, build_plugin_registry, build_process_next_handler};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
@@ -21,13 +21,69 @@ async fn main() -> anyhow::Result<()> {
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 pool = adapters_postgres::connect(&config.database_url).await?;
adapters_postgres::run_migrations(&pool).await?;
let interval = Duration::from_secs(config.example_job_interval_secs);
let runner = JobRunner::new().register(Arc::new(ExampleJob), interval);
let repos = Repos::new(pool);
let file_storage = Arc::new(adapters_storage::LocalFileStorage::new(
&config.storage_path,
));
let sidecar_writer: Arc<dyn domain::ports::SidecarWriterPort> = Arc::new(LogSidecarWriter);
let event_pub: Arc<dyn domain::ports::EventPublisher> = Arc::new(LogEventPublisher);
info!("Worker running");
runner.run().await;
Ok(())
let registry = Arc::new(build_plugin_registry(&repos, file_storage, sidecar_writer));
let process_next = build_process_next_handler(&repos, registry, event_pub);
let poll_interval = Duration::from_secs(config.poll_interval_secs);
info!(poll_secs = config.poll_interval_secs, "Worker running");
loop {
match process_next.execute(ProcessNextJobCommand).await {
Ok(Some(job)) => info!(job_id = %job.job_id, status = ?job.status, "processed job"),
Ok(None) => tokio::time::sleep(poll_interval).await,
Err(e) => {
error!(error = %e, "worker error");
tokio::time::sleep(poll_interval).await;
}
}
}
}
struct LogEventPublisher;
#[async_trait::async_trait]
impl domain::ports::EventPublisher for LogEventPublisher {
async fn publish(
&self,
event: domain::events::DomainEvent,
) -> Result<(), domain::errors::DomainError> {
info!(event = ?event, "domain event");
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<domain::value_objects::StructuredData, domain::errors::DomainError> {
info!(path, "sidecar read (no-op)");
Ok(domain::value_objects::StructuredData::new())
}
}