HORUS services provide synchronous request/response communication between nodes. Define a service with the service! macro, run a server with ServiceServerBuilder, and call it with ServiceClient or AsyncServiceClient.
Defining a Service
Use the service! macro to define request and response types:
Register the request handler (Fn(Req) -> Result<Res, String>)
poll_interval(interval)
Self
Override poll interval (default: 5ms)
build()
HorusResult<ServiceServer<S>>
Build and start the server (spawns background thread)
The handler receives the request payload and returns either a response (Ok) or an error message (Err).
ServiceServer
Method
Returns
Description
stop()
()
Stop the server (also happens automatically on drop)
The server runs in a background thread. Dropping the ServiceServer handle shuts it down.
Example
use horus::prelude::*;
service! {
AddTwoInts {
request { a: i64, b: i64 }
response { sum: i64 }
}
}
// Build and start server
let server = ServiceServerBuilder::<AddTwoInts>::new()
.on_request(|req| {
Ok(AddTwoIntsResponse { sum: req.a + req.b })
})
.poll_interval(1_u64.ms())
.build()?;
// Server runs in background thread until `server` is dropped
// ... do other work ...
// Explicit stop (optional — happens on drop)
server.stop();
ServiceRequest / ServiceResponse
Wrapper types that flow over the wire:
ServiceRequest<Req>
Field
Type
Description
request_id
u64
Unique correlation ID (auto-assigned by client)
payload
Req
The actual request data
ServiceResponse<Res>
Field
Type
Description
request_id
u64
Echoes the request's correlation ID
ok
bool
true if handled successfully
payload
Option<Res>
Response data (Some when ok == true)
error
Option<String>
Error message (Some when ok == false)
Method
Returns
Description
ServiceResponse::success(request_id, payload)
Self
Create a successful response
ServiceResponse::failure(request_id, error)
Self
Create an error response
ServiceError
Variant
Description
Transient?
Timeout
Call timed out waiting for response
Yes
ServiceFailed(String)
Server returned an error
No
NoServer
No server registered for this service
No
Transport(String)
Topic I/O error
Yes
Method
Returns
Description
is_transient()
bool
Whether a retry may succeed (Timeout and Transport are transient)
ServiceInfo
Metadata returned by horus service list:
Field
Type
Description
name
String
Service name
request_type
String
Rust type name of request
response_type
String
Rust type name of response
servers
usize
Active server count (typically 0 or 1)
clients
usize
Known client count
Complete Example
use horus::prelude::*;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
// Define a key-value store service
service! {
/// Get or set values in a shared store.
KeyValueStore {
request {
key: String,
value: Option<String>, // None = get, Some = set
}
response {
value: Option<String>,
found: bool,
}
}
}
fn main() -> HorusResult<()> {
let store = Arc::new(Mutex::new(HashMap::<String, String>::new()));
let store_clone = store.clone();
// Start server
let _server = ServiceServerBuilder::<KeyValueStore>::new()
.on_request(move |req| {
let mut map = store_clone.lock().unwrap();
match req.value {
Some(val) => {
map.insert(req.key, val.clone());
Ok(KeyValueStoreResponse { value: Some(val), found: true })
}
None => {
let val = map.get(&req.key).cloned();
let found = val.is_some();
Ok(KeyValueStoreResponse { value: val, found })
}
}
})
.build()?;
// Client: set a value
let mut client = ServiceClient::<KeyValueStore>::new()?;
client.call(
KeyValueStoreRequest { key: "robot_id".into(), value: Some("arm_01".into()) },
1_u64.secs(),
)?;
// Client: get it back
let res = client.call(
KeyValueStoreRequest { key: "robot_id".into(), value: None },
1_u64.secs(),
)?;
assert_eq!(res.value, Some("arm_01".into()));
assert!(res.found);
Ok(())
}