Crate restate_sdk

Source
Expand description

§Restate Rust SDK

Restate is a system for easily building resilient applications. This crate is the Restate SDK for writing Restate services using Rust.

§New to Restate?

If you are new to Restate, we recommend the following resources:

§Features

Have a look at the following SDK capabilities:

  • SDK Overview: Overview of the SDK and how to implement services, virtual objects, and workflows.
  • Service Communication: Durable RPC and messaging between services (optionally with a delay).
  • Journaling Results: Persist results in Restate’s log to avoid re-execution on retries
  • State: read and write: Store and retrieve state in Restate’s key-value store
  • Scheduling & Timers: Let a handler pause for a certain amount of time. Restate durably tracks the timer across failures.
  • Awakeables: Durable Futures to wait for events and the completion of external tasks.
  • Error Handling: Restate retries failures infinitely. Use TerminalError to stop retries.
  • Serialization: The SDK serializes results to send them to the Server.
  • Serving: Start an HTTP server to expose services.

§SDK Overview

The Restate Rust SDK lets you implement durable handlers. Handlers can be part of three types of services:

  • Services: a collection of durable handlers
  • Virtual Objects: an object consists of a collection of durable handlers and isolated K/V state. Virtual Objects are useful for modeling stateful entities, where at most one handler can run at a time per object.
  • Workflows: Workflows have a run handler that executes exactly once per workflow instance, and executes a set of steps durably. Workflows can have other handlers that can be called multiple times and interact with the workflow.

§Services

Services and their handlers are defined as follows:

// The prelude contains all the imports you need to get started
use restate_sdk::prelude::*;

// Define the service using Rust traits
#[restate_sdk::service]
trait MyService {
    async fn my_handler(greeting: String) -> Result<String, HandlerError>;
}

// Implement the service
struct MyServiceImpl;
impl MyService for MyServiceImpl {

    async fn my_handler(&self, ctx: Context<'_>, greeting: String) -> Result<String, HandlerError> {
        Ok(format!("{greeting}!"))
    }

}

// Start the HTTP server to expose services
#[tokio::main]
async fn main() {
    HttpServer::new(Endpoint::builder().bind(MyServiceImpl.serve()).build())
        .listen_and_serve("0.0.0.0:9080".parse().unwrap())
        .await;
}
  • Specify that you want to create a service by using the #[restate_sdk::service] macro.
  • Create a trait with the service handlers.
    • Handlers can accept zero or one parameter and return a Result.
    • The type of the input parameter of the handler needs to implement Serialize and Deserialize. See crate::serde.
    • The Result contains the return value or a HandlerError, which can be a TerminalError or any other Rust’s std::error::Error.
    • The service handler can now be called at <RESTATE_INGRESS_URL>/MyService/myHandler. You can optionally override the handler name used via #[name = "myHandler"]. More details on handler invocations can be found in the docs.
  • Implement the trait on a concrete type, for example on a struct.
  • The first parameter of a handler after &self is always a Context to interact with Restate. The SDK stores the actions you do on the context in the Restate journal to make them durable.
  • Finally, create an HTTP endpoint and bind the service(s) to it. Listen on the specified port (here 9080) for connections and requests.

§Virtual Objects

Virtual Objects and their handlers are defined similarly to services, with the following differences:

use restate_sdk::prelude::*;

#[restate_sdk::object]
pub trait MyVirtualObject {
    async fn my_handler(name: String) -> Result<String, HandlerError>;
    #[shared]
    async fn my_concurrent_handler(name: String) -> Result<String, HandlerError>;
}

pub struct MyVirtualObjectImpl;

impl MyVirtualObject for MyVirtualObjectImpl {

    async fn my_handler(
        &self,
        ctx: ObjectContext<'_>,
        greeting: String,
    ) -> Result<String, HandlerError> {
        Ok(format!("{} {}", greeting, ctx.key()))
    }

    async fn my_concurrent_handler(
        &self,
        ctx: SharedObjectContext<'_>,
        greeting: String,
    ) -> Result<String, HandlerError> {
        Ok(format!("{} {}", greeting, ctx.key()))
    }

}

#[tokio::main]
async fn main() {
    HttpServer::new(
        Endpoint::builder()
            .bind(MyVirtualObjectImpl.serve())
            .build(),
    )
    .listen_and_serve("0.0.0.0:9080".parse().unwrap())
    .await;
}
  • Specify that you want to create a Virtual Object by using the #[restate_sdk::object] macro.
  • The first argument of each handler must be the ObjectContext parameter. Handlers with the ObjectContext parameter can write to the K/V state store. Only one handler can be active at a time per object, to ensure consistency.
  • You can retrieve the key of the object you are in via [ObjectContext.key].
  • If you want to have a handler that executes concurrently to the others and doesn’t have write access to the K/V state, add #[shared] to the handler definition in the trait. Shared handlers need to use the SharedObjectContext. You can use these handlers, for example, to read K/V state and expose it to the outside world, or to interact with the blocking handler and resolve awakeables etc.

§Workflows

Workflows are a special type of Virtual Objects, their definition is similar but with the following differences:

use restate_sdk::prelude::*;

#[restate_sdk::workflow]
pub trait MyWorkflow {
    async fn run(req: String) -> Result<String, HandlerError>;
    #[shared]
    async fn interact_with_workflow() -> Result<(), HandlerError>;
}

pub struct MyWorkflowImpl;

impl MyWorkflow for MyWorkflowImpl {

    async fn run(&self, ctx: WorkflowContext<'_>, req: String) -> Result<String, HandlerError> {
        //! implement workflow logic here

        Ok(String::from("success"))
    }

    async fn interact_with_workflow(&self, ctx: SharedWorkflowContext<'_>) -> Result<(), HandlerError> {
        //! implement interaction logic here
        //! e.g. resolve a promise that the workflow is waiting on

        Ok(())
    }

}

#[tokio::main]
async fn main() {
    HttpServer::new(Endpoint::builder().bind(MyWorkflowImpl.serve()).build())
        .listen_and_serve("0.0.0.0:9080".parse().unwrap())
        .await;
}
  • Specify that you want to create a Workflow by using the #[restate_sdk::workflow] macro.
  • The workflow needs to have a run handler.
  • The first argument of the run handler must be the WorkflowContext parameter. The WorkflowContext parameter is used to interact with Restate. The run handler executes exactly once per workflow instance.
  • The other handlers of the workflow are used to interact with the workflow: either query it, or signal it. They use the SharedWorkflowContext to interact with the SDK. These handlers can run concurrently with the run handler and can still be called after the run handler has finished.
  • Have a look at the workflow docs to learn more.

Learn more about each service type here:

§Logging

This crate uses the tracing crate to emit logs, so you’ll need to configure a tracing subscriber to get logs. For example, to configure console logging using tracing_subscriber::fmt:

#[tokio::main]
async fn main() {
    //! To enable logging
    tracing_subscriber::fmt::init();

    // Start http server etc...
}

For more information, have a look at the tracing subscriber doc.

Next, have a look at the other SDK features.

Modules§

Attribute Macros§