Every service has a home.
A node supervises modular services. Typed events connect them through EventHub, with lifecycle and configuration in one place.
Node → Services → EventHubNodes find each other. A leader emerges. Work finds a home. Follow the messages that make a cluster tick — then try it for real.
A seed is an introduction. Gossip carries membership through the rest of the cluster.
Six real nodes. Your own temporary cluster.
Explore the system, then put it to the test.
Checking availability…
Launch three control plane nodes and three workers. Follow discovery, form a quorum, and explore the admin panel.
The building blocks of a distributed system,
composed into a single supervised runtime.
A node supervises modular services. Typed events connect them through EventHub, with lifecycle and configuration in one place.
Node → Services → EventHubSWIM gossip tracks membership while QUIC connects peers. Inspect the topology and observe node state as your cluster changes.
SWIM + QUICAn embedded Raft control plane coordinates cluster metadata and shard placement. Local state lives in the Fjall storage engine.
OpenRaft + FjallOne set of primitives. Many possible systems. Bring your application logic; compose the runtime around it.
Build a partitioned key-value service, a persistent cache, or a storage layer for application state.
Fjall stores local data. The control plane coordinates shard placement, while QUIC connects the services that route and handle requests.
Collect sensor readings close to the source, buffer them locally, and build a service that forwards batches upstream.
Combine embedded storage with supervised ingestion services and peer discovery. Your service decides when to retry, flush, and retain data.
Distribute background jobs, media processing, or scheduled tasks across a pool of worker nodes.
Membership tracks available peers and shard placement assigns service ownership. Build scheduling, job acknowledgements, and retries into your service.
Host game sessions, collaborative canvases, or shared rooms with a service instance responsible for each session.
Supervised services own the session logic. QUIC carries messages between nodes, EventHub connects local services, and storage keeps application state.
Explore ledger services, bookkeeping workflows, and settlement coordinators with explicit application rules.
Use local persistence, typed events, and cluster coordination as building blocks. Implement transaction invariants, audit records, and business-state replication in the application.
Compose ingestion, transformation, and delivery services for event processing or media workflows.
EventHub connects stages within a node; QUIC connects nodes. Combine service placement with local keyspaces to build your buffering and delivery policies.
A standalone supervisor, a peer-to-peer mesh, or a stateful cluster: start with the services your system needs and add capabilities as it grows.
Start with a node. Compose the services you need. Explore the existing code models for coordinators, workers, and standalone applications.
$ cargo add aaron// 1. Primary Coordinator: OpenRaft Consensus, Shard Orchestrator & Admin Console
use aaron::{
admin::AdminService,
control_plane::ControlPlaneService,
membership::MembershipService,
shard::ShardService,
tracing::TracingService,
Node,
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize cluster subsystems on documented default ports
let (membership_svc, membership_handle) = MembershipService::pair();
let (control_svc, control_handle) = ControlPlaneService::pair();
let (shard_svc, shard_handle) = ShardService::coordinator(control_handle.clone());
let node = Node::new("orders-coordinator")
.with(membership_svc) // SWIM Gossip on default port 7946
.with(TracingService::new()) // Dynamic log filter reload
.with(control_svc) // OpenRaft consensus RPC on port 18946
.with(shard_svc) // Shard placement coordinator
.with(
AdminService::new() // Embedded Admin Web UI on port 8080
.with_membership_handle(membership_handle)
.with_control_plane_handle(control_handle)
.with_shard_handle(shard_handle),
);
println!("Orders coordinator online. Admin console available at http://127.0.0.1:8080");
node.run().await?;
Ok(())
}// 2. Application Worker: Receives ShardEvents from Control Plane & streams telemetry back
use aaron::{
membership::{MembershipConfig, MembershipService},
service_fn,
shard::{MemberRole, ShardEvent, ShardService},
tracing::TracingService,
Context, Node,
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Join SWIM gossip mesh (port 7947 to avoid collision on localhost)
let config = MembershipConfig::lan()
.bind_addr("0.0.0.0:7947")
.add_seed("127.0.0.1:7946");
let (membership_svc, _membership_handle) = MembershipService::pair_with_config(config);
// ShardService::worker() binds QUIC (18946), receives commands from Control Plane,
// publishes ShardEvents to EventHub, and streams telemetry back to Control Plane
let (shard_svc, shard_handle) = ShardService::worker();
let node = Node::new("inventory-worker")
.with(membership_svc) // Gossip mesh discovery
.with(TracingService::new()) // Observability service
.with(shard_svc) // Shard listener & telemetry reporter
.with(service_fn("inventory-storage", move |ctx: Context| {
let shards = shard_handle.clone();
async move {
// Subscribe to reactive shard lifecycle events dispatched by ShardService
let mut sub = ctx.event_hub.subscribe::<ShardEvent>().await;
while let Ok(event) = sub.recv().await {
match event {
ShardEvent::Join { shard_id, members, role } => {
// Initialize local LSM keyspace partition for assigned shard
let store = ctx.store.keyspace(&format!("inventory_shard_{shard_id}"))?;
store.insert(b"status", b"active_partition")?;
ctx.store.persist()?;
// Notify local state & acknowledge role in shard quorum
shards.announce_role(shard_id, role).await;
println!("Assigned shard {shard_id} as {role:?} (Quorum: {} members)", members.len());
}
ShardEvent::RoleChanged { shard_id, role } => {
shards.announce_role(shard_id, role).await;
println!("Shard {shard_id} role updated -> {role:?}");
}
ShardEvent::Leave { shard_id } => {
println!("Shard {shard_id} decommissioned. Detaching keyspace.");
}
ShardEvent::Bootstrap { shards: batch } => {
println!("Bootstrapping {} partitions from Control Plane.", batch.len());
}
}
}
Ok(())
}
}));
println!("Worker running. QUIC listener on :18946 • Telemetry streaming to Control Plane");
node.run().await?;
Ok(())
}// 3. Standalone Supervisor: Local actor lifecycle & embedded LSM storage (No network required)
use aaron::{service_fn, Context, Node};
use std::time::Duration;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Supervise local services with isolated LSM storage and graceful drain on SIGINT
let node = Node::new("local-runner")
.with(service_fn("metrics-collector", |ctx: Context| async move {
let stats = ctx.store.keyspace("telemetry")?;
let mut seq: u64 = 0;
// Service loop drains cleanly when supervisor receives SIGINT/SIGTERM
while !ctx.cancellation_token.is_cancelled() {
tokio::time::sleep(Duration::from_millis(500)).await;
seq += 1;
stats.insert("heartbeat:ticks", seq.to_le_bytes())?;
println!("Supervised collector heartbeat [seq={seq}]");
}
Ok(())
}));
println!("Standalone supervisor running. Press Ctrl+C for graceful drain.");
node.run().await?;
Ok(())
}// 4. EventHub: Decoupled in-memory async pub/sub between supervised services
use aaron::{service_fn, Context, Node};
#[derive(Clone, Debug)]
struct OrderEvent {
order_id: &'static str,
amount_cents: u64,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let node = Node::new("event-driven-node")
// Service 1: Asynchronous subscriber reacting to OrderEvent broadcast
.with(service_fn("billing-worker", |ctx: Context| async move {
let mut stream = ctx.event_hub.subscribe::<OrderEvent>().await;
tokio::spawn(async move {
while let Ok(ev) = stream.recv().await {
println!("Billing processed order {} for ${:.2}", ev.order_id, ev.amount_cents as f64 / 100.0);
}
});
Ok(())
}))
// Service 2: Publisher emitting strongly-typed domain events
.with(service_fn("order-gateway", |ctx: Context| async move {
let hub = ctx.event_hub.clone();
hub.publish(OrderEvent { order_id: "ORD-9901", amount_cents: 9950 }).await;
Ok(())
}));
println!("EventHub node online. Services communicating over lockless bus.");
node.run().await?;
Ok(())
}