restate-sdk 0.12.0

Restate SDK for Rust
Documentation

Documentation crates.io Examples Discord Twitter

Restate Rust SDK

Restate is a system for easily building resilient applications using distributed durable async/await. This repository contains the Restate SDK for writing services using Rust.

Community

Using the SDK

Add Restate and Tokio as dependencies:

[dependencies]
restate-sdk = "0.8"
tokio = { version = "1", features = ["full"] }

Then you're ready to develop your Restate service using Rust:

use restate_sdk::prelude::*;

struct Greeter;

#[service]
impl Greeter {
    #[handler]
    async fn greet(&self, _ctx: Context<'_>, name: String) -> HandlerResult<String> {
        Ok(format!("Greetings {name}"))
    }
}

#[tokio::main]
async fn main() {
    // To enable logging/tracing
    // tracing_subscriber::fmt::init();
    HttpServer::new(
        Endpoint::builder()
            .bind(Greeter)
            .build(),
    )
    .listen_and_serve("0.0.0.0:9080".parse().unwrap())
    .await;
}

Calling services through ingress

The impl-block service macros also generate a typed ingress client for each service. Enable the reqwest-client feature to use the SDK's built-in HTTP transport:

[dependencies]
restate-sdk = { version = "0.11", features = ["reqwest-client"] }

ReqwestClient is an alias for the transport-neutral Client<reqwest::Client>. Connect it to a Restate ingress base URI, then wrap it in the generated <Service>IngressClient:

use restate_sdk::ingress::ReqwestClient;

let client = ReqwestClient::connect("http://localhost:8080".parse().unwrap()).unwrap();
let greeter = GreeterIngressClient::from_client(client);

// Wait for the invocation to complete and decode its typed result.
let response = greeter
    .greet("Ada".to_owned())
    .idempotency_key("greet-ada")
    .call()
    .await
    .unwrap();
let invocation = response.invocation_handle();
assert_eq!(response.into_body().unwrap(), "Greetings Ada");
println!("invocation ID: {}", invocation.invocation_id());

// Enqueue an invocation without waiting for its result.
let response = greeter
    .greet("Grace".to_owned())
    .send()
    .await
    .unwrap();
println!("send status: {:?}", response.send_status());

Generated object and workflow ingress clients additionally take their key in from_client. The generic Client<E> and generated clients are available without reqwest-client; implement RequestExecutor to use another buffered HTTP transport.

Connecting through a Restate Cloud tunnel

The optional Unix-only in-process tunnel lets a service connect outbound to Restate Cloud, so the service does not need to expose an inbound HTTP port. Enable the tunnel feature:

[dependencies]
restate-sdk = { version = "0.11", features = ["tunnel"] }
tokio = { version = "1", features = ["full"] }

When default features are disabled, select a crypto provider explicitly by adding either rust_crypto or aws_lc_rs alongside tunnel. If both are enabled, rust_crypto is selected deterministically.

The Restate operator injects the discovery and authentication settings, so an operator-managed deployment needs no application-side tunnel configuration:

use restate_sdk::prelude::*;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let endpoint = Endpoint::builder().bind(Greeter).build();
    Tunnel::new(endpoint).run().await?;
    Ok(())
}

See the complete tunnel example.

Running on Lambda

The Restate Rust SDK supports running services on AWS Lambda using Lambda Function URLs. This allows you to deploy your Restate services as serverless functions.

Setup

First, enable the lambda feature in your Cargo.toml:

[dependencies]
restate-sdk = { version = "0.8", features = ["lambda"] }
tokio = { version = "1", features = ["full"] }

Basic Lambda Service

Here's how to create a simple Lambda service:

use restate_sdk::prelude::*;

struct Greeter;

#[service]
impl Greeter {
    #[handler]
    async fn greet(&self, _ctx: Context<'_>, name: String) -> HandlerResult<String> {
        Ok(format!("Greetings {name}"))
    }
}

#[tokio::main]
async fn main() {
    // To enable logging/tracing
    // check https://docs.aws.amazon.com/lambda/latest/dg/rust-logging.html#rust-logging-tracing

    // Build and run the Lambda endpoint
    LambdaEndpoint::run(
        Endpoint::builder()
            .bind(Greeter)
            .build(),
    )
    .await
    .unwrap();
}

Deployment

  1. Install cargo-lambda

    cargo install cargo-lambda
    
  2. Build your Lambda function:

    cargo lambda build --release --arm64 --output-format zip
    
  3. Create a Lambda function with the following configuration:

    • Runtime: Amazon Linux 2023
    • Architecture: arm64
  4. Upload your zip file to the Lambda function.

Logging

The SDK uses tokio's tracing crate to generate logs. Just configure it as usual through tracing_subscriber to get your logs.

Testing

The SDK uses Testcontainers to support integration testing using a Docker-deployed restate server. The restate-sdk-testcontainers crate provides a framework for initializing the test environment, and an integration test example in testcontainers/tests/test_container.rs.

use restate_sdk::ingress::ReqwestClient;

#[tokio::test]
async fn test_container() {
    tracing_subscriber::fmt::fmt()
        .with_max_level(tracing::Level::INFO) // Set the maximum log level
        .init();

    let endpoint = Endpoint::builder().bind(MyService).build();

    // simple test environment initialization with default configuration
    // let test_environment = TestEnvironment::default().start(endpoint).await.unwrap();

    // custom test environment initialization
    let test_environment = TestEnvironment::new()
        // optional passthrough logging from the Restate server testcontainer
        // prints container logs to tracing::info level
        .with_container_logging()
        .with_container(
            "docker.restate.dev/restatedev/restate".to_string(),
            "1.7.2".to_string(),
        )
        .start(endpoint)
        .await
        .unwrap();

    let ingress_url = test_environment.ingress_url();

    let client = ReqwestClient::connect(ingress_url.parse().unwrap()).unwrap();
    let client = MyServiceIngressClient::from_client(client);

    let response = client
        .my_handler()
        .idempotency_key("abc")
        .call()
        .await
        .unwrap();
    let output = response.into_body().unwrap();

    assert_eq!(output, "hello!");
    info!("MyService/my_handler response: {output:?}");
}

Versions

The Rust SDK is currently in active development, and might break across releases.

The compatibility with Restate is described in the following table:

Restate Server\sdk-rust 0.7 - 0.10 0.11
1.6 βœ… βœ…
1.7 βœ… βœ…

Some features require a minimum version of both Restate and the SDK:

  • Typed ingress client and the new /restate/ invocation routes: requires Restate >= 1.7 with sdk-rust >= 0.11
  • Scope and limit key: requires Restate >= 1.7 with sdk-rust >= 0.11

Contributing

We’re excited if you join the Restate community and start contributing! Whether it is feature requests, bug reports, ideas & feedback or PRs, we appreciate any and all contributions. We know that your time is precious and, therefore, deeply value any effort to contribute!

Building the SDK locally

Prerequisites:

To build and test the SDK:

just verify

Releasing

You need the Rust toolchain. To verify:

just verify

To release you must be part of the owners team.

To release we use cargo-release.

cargo install cargo-release

Before releasing you need to log into crates.io for which you have to create an API token on https://crates.io/me

cargo login

You might have to use the +nightly toolchain because of releasing multiple crates at once. First try the dry-run:

cargo +nightly release <VERSION> --exclude test-services --workspace

If everything looks good run with --execute

cargo +nightly release <VERSION> --exclude test-services --workspace --execute