Skip to content

Services

Go users

The code examples in this chapter are Rust. The Go service API is callback-based — handlers are registered instead of using take_request(). For Go service patterns and the typed service API, see the Go Bindings chapter.

hiroz implements ROS 2's service pattern with type-safe request-response communication over Eclipse Zenoh. This enables synchronous, point-to-point interactions between nodes using a pull-based model for full control over request processing.

Note

Services provide request-response communication for operations that need immediate feedback. Unlike topics, services are bidirectional and ensure a response for each request. hiroz uses a pull model that gives you explicit control over when to process requests.

What is a Service?

sequenceDiagram
accTitle: Service request and response between client and server
accDescr: The client sends a typed request with two integers to the server, which computes the sum and returns a typed response.
    participant C as Client
    participant S as Server

    C->>S: Request(a=3, b=5)
    Note right of S: compute sum
    S-->>C: Response(sum=8)

A named remote function call. Client sends a typed request; server returns a typed response.

  • One server per service name (multiple = undefined behavior)
  • Any number of clients can call the same service
  • Client blocks (or awaits) until the response arrives
  • Use for short operations — milliseconds, not minutes

Service vs Topic vs Action

graph TD
accTitle: Decision tree for choosing Topic, Service, or Action
accDescr: A decision tree that routes continuous data streams to Topics, fast single results to Services, and long tasks needing progress and cancellation to Actions.
    Q{What do you need?}
    Q -->|Continuous data stream| T[Topic]
    Q -->|One result, fast| S[Service]
    Q -->|Long task + progress + cancel| A[Action]
    style S fill:#3f51b5,color:#fff,stroke:#3f51b5
Topic Service Action
Direction One-way push Request → Response Request → Feedback → Result
Duration Continuous Milliseconds Seconds to minutes
Cancellation N/A No Yes
Feedback N/A No Yes

The .srv format

# Request (above the ---)
uint32 a
uint32 b
---
# Response (below the ---)
uint32 sum

Two sections, separated by ---. Each is a message definition. hiroz generates a Rust trait:

trait AddTwoInts: ZService {
    type Request  = AddTwoIntsRequest;   // { a: u32, b: u32 }
    type Response = AddTwoIntsResponse;  // { sum: u32 }
}

What happens with no server?

sequenceDiagram
accTitle: Service call timeout when no server is registered
accDescr: The client sends a request to the Zenoh router, which waits for a server and returns a timeout to the client after ten seconds.
    participant C as Client
    participant Z as Zenoh Router

    C->>Z: Request (no server registered)
    Note over Z: waits for server...
    Z-->>C: Timeout after 10 s

A hiroz service call times out after 10 seconds. The client sets that timeout on its own Zenoh querier, so it is the value that applies — not the session's queries_default_timeout.

queries_default_timeout does not change this

That setting governs queries on the raw Zenoh session. A ZClient overrides it. The 10 s figure is not configurable today. hiroz sets it in create_client and exposes no builder method for it. An action client is the one exception, and only in part: it waits without a deadline for a goal result, but it sends and cancels goals through ordinary clients that time out after 10 s.

QoS note

Services use reliable + volatile durability. Volatile means: if a server restarts, old in-flight requests are discarded — no stale requests reach the new server.

Key Concepts at a Glance

Pattern
What is a Service?
Click to flip
A named request-response channel. A client sends a typed request; the server processes it and returns a typed response. Like a remote function call.
Constraint
How many servers can a service have?
Click to flip
One. Multiple servers on the same service name is undefined behavior. Multiple clients are fine — any number can call the same service.
vs Action
Service vs Action — when to pick which?
Click to flip
Service: result in milliseconds, no progress needed.
Action: takes seconds/minutes, need feedback or cancellation.
Timeout
What happens if no server is running?
Click to flip
The call blocks until the server comes online or the client's own timeout fires, 10 seconds. That is set on the querier, so queries_default_timeout does not change it.
Pull Model
What is the pull model in hiroz?
Click to flip
Call take_request() when you are ready — you control timing. Requests queue up until you retrieve them. Contrast with a callback model where the framework calls you.
QoS
Why do services use volatile durability?
Click to flip
If a server restarts, old requests should not be replayed — stale requests could cause incorrect state. Volatile discards requests that no running server received.

Visual Flow

graph TD
accTitle: Service visual flow from context creation to response handling
accDescr: ZContextBuilder creates a ZContext that spawns both a client node and server node; the client sends a request routed through the service call to the server's handler, which returns a response the client takes.
    A[ZContextBuilder] -->|configure| B[ZContext]
    B -->|create| C[Client Node]
    B -->|create| D[Server Node]
    C -->|create_client| E[Service Client]
    D -->|create_service| F[Service Server]
    E -->|call| G[Service Call]
    G -->|route| F
    F -->|take_request| H[ServiceRequest]
    H -->|reply_blocking| G
    G -->|deliver| E

Key Features

Feature Description Benefit
Type Safety Strongly-typed service definitions with Rust structs Compile-time error detection
Pull Model Explicit control over request processing timing Predictable concurrency and backpressure
Async/Blocking Dual API for both paradigms Flexible integration patterns
Unified Request Object ServiceRequest carries message and reply method together No dangling reply tokens

Service Server Example

A server that adds two integers. Full source: crates/hiroz/examples/demo_nodes/add_two_ints_server.rs

/// AddTwoInts server node that provides a service to add two integers
///
/// # Arguments
/// * `ctx` - The hiroz context
/// * `max_requests` - Optional maximum number of requests to handle. If None, handles requests indefinitely.
pub fn run_add_two_ints_server(ctx: ZContext, max_requests: Option<usize>) -> Result<()> {
    // Create a node named "add_two_ints_server"
    let node = ctx.create_node("add_two_ints_server").build()?;

    // Create a service that will handle requests
    let mut service = node.create_service::<AddTwoInts>("add_two_ints").build()?;

    println!("AddTwoInts service server started, waiting for requests...");

    let mut request_count = 0;

    loop {
        // Wait for a request
        let req = service.take_request()?;
        println!(
            "Incoming request\na: {} b: {}",
            req.message().a,
            req.message().b
        );

        // Compute the sum
        let sum = req.message().a + req.message().b;

        // Create the response
        let resp = AddTwoIntsResponse { sum };

        println!("Sending response: {}", resp.sum);

        // Send the response
        req.reply_blocking(&resp)?;

        request_count += 1;

        // Check if we've reached the max requests
        if let Some(max) = max_requests
            && request_count >= max
        {
            break;
        }
    }

    Ok(())
}

Key points:

  • take_request() blocks until a request arrives — the pull model means you control timing
  • req.message() accesses the request fields (req.message().a, req.message().b)
  • req.reply_blocking(&resp) sends the response; the request object owns the reply channel
  • service must be mut because take_request takes &mut self
  • max_requests: Option<usize> lets tests stop the server after N requests; pass None for a perpetual server

Run it now

Jump to Complete Service Workflow below to see server and client exchange requests with one command per terminal.

Service Client Example

A client that sends an addition request and prints the result. Full source: crates/hiroz/examples/demo_nodes/add_two_ints_client.rs

/// AddTwoInts client node that calls the service to add two integers
///
/// # Arguments
/// * `ctx` - The hiroz context
/// * `a` - First number to add
/// * `b` - Second number to add
/// * `async_mode` - Whether to use async response waiting
pub fn run_add_two_ints_client(ctx: ZContext, a: i64, b: i64, async_mode: bool) -> Result<i64> {
    // Create a node named "add_two_ints_client"
    let node = ctx.create_node("add_two_ints_client").build()?;

    // Create a client for the service
    let client = node.create_client::<AddTwoInts>("add_two_ints").build()?;

    println!(
        "AddTwoInts service client started (mode: {})",
        if async_mode { "async" } else { "sync" }
    );

    // Create the request
    let req = AddTwoIntsRequest { a, b };
    println!("Sending request: {} + {}", req.a, req.b);

    // Wait for the response
    let resp = if async_mode {
        tokio::runtime::Runtime::new()
            .unwrap()
            .block_on(async { client.call(&req).await })?
    } else {
        tokio::runtime::Runtime::new()
            .unwrap()
            .block_on(async { client.call_with_timeout(&req, Duration::from_secs(5)).await })?
    };

    println!("Received response: {}", resp.sum);

    Ok(resp.sum)
}

Key points:

  • call(&req).await is the single-step async API — it sends the request and waits for the response
  • call_with_timeout(&req, duration).await enforces an explicit deadline; returns Err on timeout
  • If no server is running, the call blocks until Zenoh's query timeout fires (default: 10 minutes)

Complete Service Workflow

Note

These commands run the ready-made examples from the hiroz repository. Clone it first with git clone https://github.com/ZettaScaleLabs/hiroz.git && cd hiroz. If you're building your own project, run your binaries with cargo run instead.

Terminal 1 — Start Zenoh Router:

hu router

Terminal 2 — Start Server:

cargo run --example demo_nodes_add_two_ints_server

Terminal 3 — Send Client Requests:

# Request 1
cargo run --example demo_nodes_add_two_ints_client -- --a 10 --b 20

# Request 2
cargo run --example demo_nodes_add_two_ints_client -- --a 100 --b 200

Success

The server processes each client request immediately, demonstrating synchronous request-response communication over Zenoh.

Service Server Patterns

Service servers in hiroz follow a pull model pattern, similar to subscribers. You explicitly receive requests when ready to process them, giving you full control over request handling timing and concurrency.

Info

This pull-based approach is consistent with subscriber's recv() pattern, allowing you to control when work happens without callbacks interrupting your flow.

Pattern 1: Blocking Request Handling

Best for: Simple synchronous service implementations

use hiroz::Builder;

let mut service = node
    .create_service::<ServiceType>("service_name")
    .build()?;

loop {
    let req = service.take_request()?;
    let response = process_request(req.message());
    req.reply_blocking(&response)?;
}

take_request() blocks until a request arrives. The server variable must be mut because take_request takes &mut self. The returned ServiceRequest owns the reply channel — call req.reply_blocking(&response) to send the response. Access message fields via req.message().

Pattern 2: Async Request Handling

Best for: Services that need to await other operations

use hiroz::Builder;

let mut service = node
    .create_service::<ServiceType>("service_name")
    .build()?;

loop {
    let req = service.async_take_request().await?;
    let response = async_process_request(req.message()).await;
    req.reply(&response).await?;
}

Why Pull Model?

Aspect Pull Model (take_request) Push Model (callback)
Control Explicit control over when to accept requests Interrupts current work
Concurrency Easy to reason about Requires careful synchronization
Backpressure Natural - slow processing slows acceptance Can overwhelm if processing is slow
Consistency Same pattern as subscriber recv() Different pattern

Service Client Patterns

Service clients use a single call method — send and receive in one await.

Pattern 1: Call with Timeout

Best for: Simple request-response where you want to fail fast on a slow or absent server

use hiroz::Builder;
use std::time::Duration;

let client = node
    .create_client::<ServiceType>("service_name")
    .build()?;

let request = create_request();
let response = client.call_with_timeout(&request, Duration::from_secs(5)).await?;

Pattern 2: Call without Explicit Timeout

Best for: Integration with async codebases where you manage timeouts externally (e.g., tokio::time::timeout)

use hiroz::Builder;

let client = node
    .create_client::<ServiceType>("service_name")
    .build()?;

let request = create_request();
let response = client.call(&request).await?;

Tip

use hiroz::Builder; must be in scope to call .build(). Both patterns require an async runtime such as tokio. For logging, call zenoh::init_log_from_env_or("error") before building the context.

ROS 2 Interoperability

hiroz services interoperate with ROS 2 C++ and Python nodes when both sides share the same Zenoh transport:

Requirements:

  • ROS 2 nodes must use rmw_zenoh_cpp (export RMW_IMPLEMENTATION=rmw_zenoh_cpp)
  • Both sides must use matching service types with identical RIHS01 type hashes
  • All nodes must connect to the same Zenoh router
# List available services
ros2 service list

# Call hiroz service from ROS 2 CLI
ros2 service call /add_two_ints example_interfaces/srv/AddTwoInts "{a: 42, b: 58}"

# Show service type
ros2 service type /add_two_ints

# Get service info
ros2 service info /add_two_ints

Warning

Service interop requires rmw_zenoh_cpp on the ROS 2 side. The zenoh-bridge-ros2dds approach works for pub/sub but does not fully support services.

Error Handling

When the server is not running

call() blocks until a response arrives or Zenoh's query timeout fires (default: 10 minutes). For explicit deadline control, use call_with_timeout:

match client.call_with_timeout(&request, Duration::from_secs(5)).await {
    Ok(response) => println!("Got response: {:?}", response),
    Err(e) => eprintln!("Service not available or timed out: {}", e),
}

Client methods compared

Method Behavior
call(&req).await Waits for response; Zenoh query timeout applies (default: 10 min)
call_with_timeout(&req, duration).await Waits up to duration; Err on timeout

Resources

Start with the examples above to understand the basic service workflow, then explore custom service types for domain-specific operations.