Files
alknet/docs/research/references/nats.rs/nats-server/09-service-api-and-abstractions.md

7.6 KiB

Service API and Higher-Level Abstractions

This document covers the Service API and other higher-level abstractions built on top of the core NATS client.

Service API

Location: service/ (feature: service)

The Service API provides a framework for building NATS-based microservices with built-in monitoring, health checks, and statistics.

Service

#[derive(Debug)]
pub struct Service {
    client: Client,
    info: Info,
    endpoints: HashMap<String, Endpoint>,
    started: DateTime,
    stats_handler: Arc<dyn Fn(&str, &Stats) -> serde_json::Value + Send + Sync>,
    stop_sender: mpsc::Sender<()>,
    stop_receiver: Option<mpsc::Receiver<()>>,
}

Creating a Service

use async_nats::service::ServiceExt;

let mut service = client
    .service_builder()
    .description("Product service")
    .stats_handler(|endpoint, stats| {
        serde_json::json!({
            "endpoint": endpoint,
            "requests": stats.num_requests,
            "errors": stats.num_errors,
        })
    })
    .start("products", "1.0.0")
    .await?;

ServiceBuilder

impl ServiceBuilder {
    pub fn description(mut self, description: impl Into<String>) -> Self
    pub fn stats_handler<F>(mut self, handler: F) -> Self
    pub async fn start(self, name: impl Into<String>, version: impl Into<String>) -> Result<Service, ServiceError>
}

Endpoints

A service exposes one or more endpoints, each handling requests on a specific subject:

// Add an endpoint
let mut endpoint = service
    .endpoint("get_product")
    .await?;

// Process requests
while let Some(request) = endpoint.next().await {
    let request = request?;
    // Handle the request
    request.respond(serde_json::json!({ "id": 1, "name": "Widget" })).await?;
}

Endpoint

Location: service/endpoint.rs

pub struct Endpoint {
    subject: Subject,
    queue_group: Option<String>,
    info: EndpointInfo,
    stats: Stats,
    subscriber: Subscriber,
}

Implements futures::Stream yielding ServiceRequest objects.

ServiceRequest

pub struct ServiceRequest {
    pub subject: Subject,
    pub payload: Bytes,
    pub headers: Option<HeaderMap>,
    pub reply: Option<Subject>,
    pub client: Client,
}

Methods:

  • respond(payload) — send a response to the requester
  • respond_with_headers(payload, headers) — send a response with headers

Monitoring Subjects

The Service API automatically creates monitoring endpoints:

Subject Description
$SRV.PING Ping all services (returns service info)
$SRV.PING.<name> Ping specific service by name
$SRV.PING.<name>.<id> Ping specific service instance
$SRV.INFO Get service info
$SRV.STATS Get service statistics

Service Info

pub struct Info {
    pub name: String,
    pub id: String,
    pub version: String,
    pub description: String,
    pub endpoints: Vec<EndpointInfo>,
}

Stats

pub struct Stats {
    pub num_requests: u64,
    pub num_errors: u64,
    pub last_error: Option<String>,
    pub processing_time: Duration,
    pub average_processing_time: Duration,
}

ID Generation

Location: id_generator.rs

The client needs unique IDs for inbox subjects and other purposes.

With nuid Feature (Default)

Uses the NUID library for high-performance, cryptographically strong, collision-resistant IDs:

pub(crate) fn next() -> String {
    nuid::next().to_string()
}

NUID generates 22-character alphanumeric strings using a combination of a random prefix and a sequential counter.

Without nuid Feature

Falls back to rand-based generation:

pub(crate) fn next() -> String {
    rng()
        .sample_iter(Alphanumeric)
        .take(22)
        .map(char::from)
        .collect()
}

Both approaches produce 22-character alphanumeric strings, but NUID is more performant and has better collision resistance.

Inbox Generation

The Client::new_inbox() method generates globally unique inbox subjects for request-reply:

pub fn new_inbox(&self) -> String {
    format!("{}.{}", self.inbox_prefix, crate::id_generator::next())
}

Default prefix is _INBOX, producing subjects like _INBOX.UaBG3f3q5NxX3KdNcRmF2f.

Custom prefix via ConnectOptions::custom_inbox_prefix():

let client = ConnectOptions::new()
    .custom_inbox_prefix("MYAPP")
    .connect("demo.nats.io")
    .await?;
// Inbox subjects: MYAPP.UaBG3f3q5KdNcRmF2f

DateTime Helpers

Location: datetime.rs (feature: jetstream or service or chrono)

Provides date/time types for JetStream and Service API timestamps:

  • Uses the time crate by default
  • Optionally uses chrono via the chrono feature flag
  • Supports RFC 3339 formatting and parsing
  • DateTime type wraps either time::OffsetDateTime or chrono::DateTime<Utc>

Crypto Module

Location: crypto.rs (feature: crypto)

Provides encryption/decryption support used by the Object Store for server-side encryption.

Subject Validation

Location: lib.rs

The client provides two levels of subject validation:

is_valid_publish_subject

pub(crate) fn is_valid_publish_subject<T: AsRef<str>>(subject: T) -> bool

Checks for protocol safety only:

  • Not empty
  • No whitespace (space, tab, CR, LF) which would break protocol framing

Used for publish operations. Can be disabled with skip_subject_validation.

is_valid_subject

pub(crate) fn is_valid_subject<T: AsRef<str>>(subject: T) -> bool

Checks structural validity:

  • Not empty
  • No leading/trailing dots
  • No consecutive dots (..)
  • No whitespace

Used for subscribe operations (always runs, matching Go/Java behavior).

is_valid_queue_group

pub(crate) fn is_valid_queue_group(queue_group: &str) -> bool

Checks:

  • Not empty
  • No whitespace

JetStream Name Validation

Location: jetstream/mod.rs

pub(crate) fn is_valid_name(name: &str) -> bool {
    !name.is_empty()
        && name.bytes().all(|c| !c.is_ascii_whitespace() && c != b'.' && c != b'*' && c != b'>')
}

JetStream names (stream names, consumer names) must not contain:

  • Whitespace
  • Dots (.) — would conflict with subject delimiters
  • Wildcards (*, >) — would conflict with subject wildcards

CallbackArg1

Location: options.rs

A type-erased async callback wrapper used throughout the crate:

pub(crate) type AsyncCallbackArg1<A, T> =
    Arc<dyn Fn(A) -> Pin<Box<dyn Future<Output = T> + Send + Sync + 'static>> + Send + Sync>;

#[derive(Clone)]
pub(crate) struct CallbackArg1<A, T>(AsyncCallbackArg1<A, T>);

impl<A, T> CallbackArg1<A, T> {
    pub(crate) async fn call(&self, arg: A) -> T {
        (self.0.as_ref())(arg).await
    }
}

Used for:

  • event_callbackCallbackArg1<Event, ()>
  • auth_callbackCallbackArg1<Vec<u8>, Result<Auth, AuthError>>
  • reconnect_to_server_callbackCallbackArg1<(Vec<Server>, ServerInfo), Option<ReconnectToServer>>
  • signature_callbackCallbackArg1<String, Result<String, AuthError>>

Version Compatibility Checking

The Client::is_server_compatible method checks if the server version meets a minimum requirement:

pub fn is_server_compatible(&self, major: i64, minor: i64, patch: i64) -> bool

This parses the server version string from ServerInfo::version using a regex and compares major/minor/patch components. Note: this checks the directly-connected server, not necessarily the JetStream leader.

The server_2_10, server_2_11, server_2_12, and server_2_14 feature flags enable version-specific API fields and methods without runtime checks.