Monorepo consolidation: workspace, shared types, transport plans, docker/swam assets
This commit is contained in:
315
runner/src/main.rs
Normal file
315
runner/src/main.rs
Normal file
@@ -0,0 +1,315 @@
|
||||
use runner::config::Settings;
|
||||
use runner::effects::run_effect_worker;
|
||||
use runner::http;
|
||||
use runner::observability::Observability;
|
||||
use runner::outbox::OutboxRelay;
|
||||
use runner::saga::{run_saga_worker, SagaPrograms, SagaRuntime};
|
||||
use runner::schedule::Scheduler;
|
||||
use runner::storage::KvClient;
|
||||
use runner::stream::JetStreamClient;
|
||||
use runner::tenant_placement::{start_tenant_filter, TenantGate};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
match std::env::args().nth(1).as_deref() {
|
||||
Some("-h") | Some("--help") => {
|
||||
print_help();
|
||||
return;
|
||||
}
|
||||
Some("serve") | None => serve().await,
|
||||
Some(other) => {
|
||||
eprintln!("Unknown command: {}", other);
|
||||
print_help();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn serve() {
|
||||
let settings = load_settings();
|
||||
if let Err(e) = settings.validate() {
|
||||
eprintln!("Invalid configuration: {}", e);
|
||||
std::process::exit(2);
|
||||
}
|
||||
|
||||
let observability = Observability::default();
|
||||
observability.init();
|
||||
let metrics = observability.metrics();
|
||||
|
||||
tracing::info!(settings = ?settings, "Runner starting");
|
||||
|
||||
let shutdown = Arc::new(tokio::sync::Notify::new());
|
||||
let reload = Arc::new(tokio::sync::Notify::new());
|
||||
let draining = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let storage = match KvClient::open(settings.storage_path.clone()) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to open storage: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let tenant_filter = match start_tenant_filter(&settings).await {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
tracing::error!(error = %e, "Failed to initialize tenant filter");
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let tenant_gate = Arc::new(TenantGate::new(tenant_filter.clone()));
|
||||
|
||||
let state = Arc::new(http::AppState::new(
|
||||
settings.clone(),
|
||||
draining.clone(),
|
||||
tenant_gate.clone(),
|
||||
metrics.clone(),
|
||||
storage.clone(),
|
||||
reload.clone(),
|
||||
));
|
||||
|
||||
let http_listener = tokio::net::TcpListener::bind(settings.http_addr.as_str())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let http_shutdown = shutdown.clone();
|
||||
let http_state = state.clone();
|
||||
let http_task = tokio::spawn(async move {
|
||||
http::serve(http_listener, http_state, async move {
|
||||
http_shutdown.notified().await
|
||||
})
|
||||
.await
|
||||
});
|
||||
|
||||
let signal_shutdown = shutdown.clone();
|
||||
let signal_draining = draining.clone();
|
||||
tokio::spawn(async move {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use tokio::signal::unix::{signal, SignalKind};
|
||||
let mut sigterm = signal(SignalKind::terminate()).ok();
|
||||
let mut sigint = signal(SignalKind::interrupt()).ok();
|
||||
tokio::select! {
|
||||
_ = tokio::signal::ctrl_c() => {},
|
||||
_ = async { if let Some(s) = &mut sigterm { let _ = s.recv().await; } } => {},
|
||||
_ = async { if let Some(s) = &mut sigint { let _ = s.recv().await; } } => {},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
let _ = tokio::signal::ctrl_c().await;
|
||||
}
|
||||
|
||||
signal_draining.store(true, Ordering::Relaxed);
|
||||
signal_shutdown.notify_waiters();
|
||||
});
|
||||
|
||||
let mut tasks = Vec::new();
|
||||
|
||||
match settings.mode {
|
||||
runner::config::RunnerMode::Saga => {
|
||||
let programs = Arc::new(match SagaPrograms::load(&settings) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
tracing::error!(error = %e, "Failed to load saga manifest/programs");
|
||||
std::process::exit(1);
|
||||
}
|
||||
});
|
||||
let saga_runtime = SagaRuntime::default();
|
||||
tasks.push(tokio::spawn(run_saga_worker(
|
||||
settings.clone(),
|
||||
storage.clone(),
|
||||
programs.clone(),
|
||||
saga_runtime.clone(),
|
||||
metrics.clone(),
|
||||
tenant_gate.clone(),
|
||||
tenant_filter.clone(),
|
||||
shutdown.clone(),
|
||||
draining.clone(),
|
||||
)));
|
||||
let outbox_settings = settings.clone();
|
||||
let outbox_storage = storage.clone();
|
||||
let outbox_shutdown = shutdown.clone();
|
||||
let outbox_draining = draining.clone();
|
||||
let outbox_metrics = metrics.clone();
|
||||
let outbox_tenant_gate = tenant_gate.clone();
|
||||
tasks.push(tokio::spawn(async move {
|
||||
let js = JetStreamClient::connect(&outbox_settings)
|
||||
.await
|
||||
.map_err(|e| runner::types::RunnerError::StreamError(e.to_string()))?;
|
||||
OutboxRelay
|
||||
.run(
|
||||
outbox_settings,
|
||||
outbox_storage,
|
||||
js,
|
||||
outbox_metrics,
|
||||
outbox_tenant_gate,
|
||||
outbox_shutdown,
|
||||
outbox_draining,
|
||||
)
|
||||
.await
|
||||
}));
|
||||
let scheduler_settings = settings.clone();
|
||||
let scheduler_storage = storage.clone();
|
||||
let scheduler_shutdown = shutdown.clone();
|
||||
let scheduler_draining = draining.clone();
|
||||
let scheduler_metrics = metrics.clone();
|
||||
let scheduler_tenant_gate = tenant_gate.clone();
|
||||
tasks.push(tokio::spawn(async move {
|
||||
Scheduler
|
||||
.run(
|
||||
scheduler_settings,
|
||||
scheduler_storage,
|
||||
programs,
|
||||
saga_runtime,
|
||||
scheduler_metrics,
|
||||
scheduler_tenant_gate,
|
||||
scheduler_shutdown,
|
||||
scheduler_draining,
|
||||
)
|
||||
.await
|
||||
}));
|
||||
}
|
||||
runner::config::RunnerMode::Effect => {
|
||||
tasks.push(tokio::spawn(run_effect_worker(
|
||||
settings.clone(),
|
||||
storage.clone(),
|
||||
metrics.clone(),
|
||||
tenant_gate.clone(),
|
||||
tenant_filter.clone(),
|
||||
reload.clone(),
|
||||
shutdown.clone(),
|
||||
draining.clone(),
|
||||
)));
|
||||
}
|
||||
runner::config::RunnerMode::Combined => {
|
||||
let programs = Arc::new(match SagaPrograms::load(&settings) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
tracing::error!(error = %e, "Failed to load saga manifest/programs");
|
||||
std::process::exit(1);
|
||||
}
|
||||
});
|
||||
let saga_runtime = SagaRuntime::default();
|
||||
tasks.push(tokio::spawn(run_saga_worker(
|
||||
settings.clone(),
|
||||
storage.clone(),
|
||||
programs.clone(),
|
||||
saga_runtime.clone(),
|
||||
metrics.clone(),
|
||||
tenant_gate.clone(),
|
||||
tenant_filter.clone(),
|
||||
shutdown.clone(),
|
||||
draining.clone(),
|
||||
)));
|
||||
tasks.push(tokio::spawn(run_effect_worker(
|
||||
settings.clone(),
|
||||
storage.clone(),
|
||||
metrics.clone(),
|
||||
tenant_gate.clone(),
|
||||
tenant_filter.clone(),
|
||||
reload.clone(),
|
||||
shutdown.clone(),
|
||||
draining.clone(),
|
||||
)));
|
||||
let outbox_settings = settings.clone();
|
||||
let outbox_storage = storage.clone();
|
||||
let outbox_shutdown = shutdown.clone();
|
||||
let outbox_draining = draining.clone();
|
||||
let outbox_metrics = metrics.clone();
|
||||
let outbox_tenant_gate = tenant_gate.clone();
|
||||
tasks.push(tokio::spawn(async move {
|
||||
let js = JetStreamClient::connect(&outbox_settings)
|
||||
.await
|
||||
.map_err(|e| runner::types::RunnerError::StreamError(e.to_string()))?;
|
||||
OutboxRelay
|
||||
.run(
|
||||
outbox_settings,
|
||||
outbox_storage,
|
||||
js,
|
||||
outbox_metrics,
|
||||
outbox_tenant_gate,
|
||||
outbox_shutdown,
|
||||
outbox_draining,
|
||||
)
|
||||
.await
|
||||
}));
|
||||
let scheduler_settings = settings.clone();
|
||||
let scheduler_storage = storage.clone();
|
||||
let scheduler_shutdown = shutdown.clone();
|
||||
let scheduler_draining = draining.clone();
|
||||
let scheduler_metrics = metrics.clone();
|
||||
let scheduler_tenant_gate = tenant_gate.clone();
|
||||
tasks.push(tokio::spawn(async move {
|
||||
Scheduler
|
||||
.run(
|
||||
scheduler_settings,
|
||||
scheduler_storage,
|
||||
programs,
|
||||
saga_runtime,
|
||||
scheduler_metrics,
|
||||
scheduler_tenant_gate,
|
||||
scheduler_shutdown,
|
||||
scheduler_draining,
|
||||
)
|
||||
.await
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
let mut failed = None;
|
||||
for task in tasks {
|
||||
match task.await {
|
||||
Ok(Ok(())) => {}
|
||||
Ok(Err(e)) => {
|
||||
failed = Some(e);
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
failed = Some(runner::types::RunnerError::RuntimeError(e.to_string()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
draining.store(true, Ordering::Relaxed);
|
||||
shutdown.notify_waiters();
|
||||
let _ = http_task.await;
|
||||
|
||||
if let Some(e) = failed {
|
||||
tracing::error!(error = %e, "Runner terminated with error");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn print_help() {
|
||||
println!(
|
||||
"runner\n\nUSAGE:\n runner [COMMAND]\n\nCOMMANDS:\n serve Start the HTTP server (default)\n\nOPTIONS:\n -h, --help Print help\n"
|
||||
);
|
||||
}
|
||||
|
||||
fn load_settings() -> Settings {
|
||||
if let Ok(path) = std::env::var("RUNNER_CONFIG_PATH") {
|
||||
if let Ok(settings) = Settings::load_from_file_with_env_overrides(path) {
|
||||
return settings;
|
||||
}
|
||||
}
|
||||
|
||||
Settings::from_env().unwrap_or_default()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn test_harness_runs() {
|
||||
let settings = runner::Settings::default();
|
||||
assert_eq!(settings.aggregate_events_stream, "AGGREGATE_EVENTS");
|
||||
assert!(settings
|
||||
.saga_trigger_subject_filters
|
||||
.iter()
|
||||
.any(|s| s == "tenant.*.aggregate.*.*"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user