Files
cloudlysis/aggregate/src/main.rs
Vlad Durnea 1298d9a3df
Some checks failed
ci / rust (push) Failing after 2m34s
ci / ui (push) Failing after 30s
Monorepo consolidation: workspace, shared types, transport plans, docker/swam assets
2026-03-30 11:40:42 +03:00

214 lines
6.6 KiB
Rust

use aggregate::config::Settings;
use aggregate::gateway::server::GrpcCommandServer;
use aggregate::http_server;
use aggregate::observability::Observability;
use aggregate::runtime::RuntimeExecutor;
use aggregate::server::AdminServer;
use aggregate::storage::StorageClient;
use aggregate::stream::StreamClient;
use aggregate::swarm::TenantPlacementKvClient;
use aggregate::{aggregate::AggregateHandler, placement::TenantPlacementManager};
use futures::StreamExt;
use std::sync::Arc;
use std::time::Duration;
#[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();
let observability = Observability::default();
let health_checker = aggregate::server::HealthChecker::new();
let admin = Arc::new(AdminServer::new(
observability,
health_checker,
settings.shard_id.clone(),
));
spawn_health_probe(admin.clone(), settings.clone());
spawn_placement_watcher(admin.placement_manager(), settings.clone());
let storage = StorageClient::open(settings.storage_path.clone()).unwrap();
let stream = StreamClient::new(settings.nats_url.clone()).await.unwrap();
let _ = stream.setup_stream().await;
let executor = RuntimeExecutor::new();
let handler = AggregateHandler::new(
storage,
stream,
executor,
settings.decide_program.clone(),
settings.apply_program.clone(),
)
.with_snapshot_threshold(settings.snapshot_threshold)
.with_max_retries(settings.max_retries);
let grpc_addr: std::net::SocketAddr = settings.grpc_addr.parse().unwrap();
let grpc_service = GrpcCommandServer::new(
handler,
admin.placement_manager(),
admin.observability(),
settings.multi_tenant_enabled,
settings
.default_tenant_id
.as_ref()
.map(aggregate::types::TenantId::new),
)
.service();
let addr = std::env::var("AGGREGATE_HTTP_ADDR").unwrap_or_else(|_| "0.0.0.0:8080".to_string());
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
let (shutdown_tx, _) = tokio::sync::broadcast::channel::<()>(1);
let mut http_shutdown = shutdown_tx.subscribe();
let mut grpc_shutdown = shutdown_tx.subscribe();
let http_task = tokio::spawn(async move {
http_server::serve(listener, admin, async move {
let _ = http_shutdown.recv().await;
})
.await;
});
let grpc_task = tokio::spawn(async move {
tonic::transport::Server::builder()
.add_service(grpc_service)
.serve_with_shutdown(grpc_addr, async move {
let _ = grpc_shutdown.recv().await;
})
.await
.unwrap();
});
let _ = tokio::signal::ctrl_c().await;
let _ = shutdown_tx.send(());
let _ = tokio::join!(http_task, grpc_task);
}
fn print_help() {
println!(
"aggregate\n\nUSAGE:\n aggregate [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("AGGREGATE_CONFIG_PATH") {
if let Ok(settings) = Settings::load_from_file_with_env_overrides(path) {
return settings;
}
}
Settings::from_env().unwrap_or_default()
}
fn spawn_health_probe(admin: Arc<AdminServer>, settings: Settings) {
tokio::spawn(async move {
loop {
let storage_ok = StorageClient::open(settings.storage_path.clone()).is_ok();
admin.health_checker().set_storage_healthy(storage_ok);
let stream_ok = tokio::time::timeout(Duration::from_secs(1), async {
let stream = StreamClient::new(settings.nats_url.clone()).await?;
let _ = stream.setup_stream().await;
Ok::<_, aggregate::types::AggregateError>(())
})
.await
.is_ok_and(|r| r.is_ok());
admin.health_checker().set_stream_healthy(stream_ok);
tokio::time::sleep(Duration::from_secs(5)).await;
}
});
}
fn spawn_placement_watcher(placement: Arc<TenantPlacementManager>, settings: Settings) {
tokio::spawn(async move {
loop {
let client = TenantPlacementKvClient::connect(
settings.nats_url.clone(),
settings.placement_bucket.clone(),
)
.await;
let client = match client {
Ok(c) => c,
Err(_) => {
tokio::time::sleep(Duration::from_secs(1)).await;
continue;
}
};
if let Ok(Some(value)) = client.get_json(&settings.placement_key).await {
apply_placement_value(&placement, &settings.shard_id, value).await;
}
let watch = client.watch_json(&settings.placement_key).await;
let mut stream = match watch {
Ok(s) => s,
Err(_) => {
tokio::time::sleep(Duration::from_secs(1)).await;
continue;
}
};
while let Some(update) = stream.next().await {
if let Ok(value) = update {
apply_placement_value(&placement, &settings.shard_id, value).await;
}
}
tokio::time::sleep(Duration::from_secs(1)).await;
}
});
}
async fn apply_placement_value(
placement: &TenantPlacementManager,
shard_id: &str,
value: serde_json::Value,
) {
if let Some(map) = value.as_object() {
let placement_map = map
.iter()
.filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
.collect::<std::collections::HashMap<_, _>>();
placement
.apply_placement_map(shard_id, &placement_map)
.await;
return;
}
if let Some(map) = value.get("placement").and_then(|v| v.as_object()) {
let placement_map = map
.iter()
.filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
.collect::<std::collections::HashMap<_, _>>();
placement
.apply_placement_map(shard_id, &placement_map)
.await;
}
}
#[cfg(test)]
mod tests {
#[test]
fn binary_exists() {
assert!(std::env::current_exe().is_ok());
}
}