Quick Start

Add the dependency

[dependencies]
malkuth = { git = "https://github.com/celestia-island/malkuth.git", branch = "dev" }
# features: tcp (default) | ws | ipc | signals (default) | worker | probes |
#           file-lock | lease | pg-lock | replica | leader-follower | schema

A minimal JSON-RPC service

use std::sync::Arc;
use malkuth::{Router, Supervised};
use malkuth::transport::TcpTransport;
use malkuth::Transport;
use serde_json::json;

#[tokio::main]
async fn main() -> std::io::Result<()> {
    let lis = TcpTransport.listen("tcp://127.0.0.1:0").await?;
    let supervised = Supervised::new().signals();
    let ctrl = supervised.drain_controller();

    let handler = Arc::new(
        Router::new()
            .lifecycle(ctrl, None)            // registers Lifecycle.Drain / Status / Health / Reload
            .route("ping", |_| Box::pin(async { Ok(json!("pong")) })),
    );

    supervised.serve_rpc_listener(lis, handler).await
}

Supervised races the JSON-RPC server against the OS-signal exit source (SIGINT/SIGTERM → drain, SIGHUP → reload, SIGQUIT → immediate exit), then runs any registered drain hooks. Replace .signals() with .exit(your_impl) to trigger drain from your own logic.

Call it from a client

use malkuth::Client;
use malkuth::transport::TcpTransport;
use malkuth::Transport;
use serde_json::json;

let mut c = Client::connect(&TcpTransport, "tcp://127.0.0.1:8080").await?;

// Custom method:
let r = c.call("ping", json!({})).await?;       // → "pong"

// Standard lifecycle methods (registered by Router::lifecycle):
c.notify("Lifecycle.Drain", json!({})).await?;  // → server begins graceful drain
let health = c.call("Lifecycle.Health", json!({})).await?;
// → { "alive": true, "pid": 12345, "uptime_secs": 360, "version": "0.1.0" }
let status = c.call("Lifecycle.Status", json!({})).await?;
// → { "ready": true, "draining": false, "dependencies": [], "generation": null }

JSON-RPC lifecycle protocol

Router::lifecycle(drain, probe) registers four standard methods:

MethodParamsResultEffect
Lifecycle.Drain{}{ "accepted": true, "draining": true }Begin graceful drain
Lifecycle.Reload{}nullBegin reload (no exit)
Lifecycle.Status{}ReadyStatusQuery readiness (drain bit + deps)
Lifecycle.Health{}HealthStatusQuery liveness (pid / uptime / version)

All messages are NDJSON-framed JSON-RPC 2.0 over the chosen transport.

Other transports

Swap TcpTransport for WsTransport (feature ws, address ws://host:port) or IpcTransport (feature ipc, address ipc:/tmp/sock). Or use MultiTransport, which dispatches by URL scheme (tcp:// / ws:// / ipc:).

Wrap any program with the CLI

malkuth --watch ./src --proxy 3000:3000-3999 --pod-count 3 -- cargo run

This runs 3 pods (self-assigning ports 3001–3003 via the PORT env var), probes each until it is listening, and fronts them with a sticky reverse proxy on port 3000 (consistent-hash routing by client IP). A change under ./src triggers a rolling restart, one pod at a time.