Tins · restate

API reference

The restate package, from restate 0.4.1, as published on mojoshelf (commit e391b04b0de1), by mojo doc from Mojo 1.2.0. Source.

restate — durable execution flows in Mojo, via Restate (restate.dev).

The Rust shim (ffi/ -> librestatemojo.dylib) embeds the Restate Rust SDK: it owns the HTTP/2 endpoint, the event loop, and the journal. Mojo owns the business logic, driven by a synchronous loop:

from restate import App

def main() raises:
    var stats = Stats(0)
    _ = App.run[Stats, __functions_in_module()]("Counter", stats)

That loop is single-threaded, and a handler that calls another handler served by the same process therefore deadlocks. App.serve is the concurrent form — the same loop on N threads, via threads-mojo's TypedPool:

from restate import App, Invocation, OpaquePtr

def handle(app: App, inv: Invocation, worker: Int, ctx: OpaquePtr) raises:
    app.complete(inv, "ok")

def main() raises:
    var app = App("Counter", ["add", "get"], object=True)
    _ = app.serve[handle](num_workers=4)   # until app.stop()

With two or more workers one thread can wait for a call while another runs the callee, so self-calls complete. See HandlerFn for the handler's obligations and App.stop for how a worker parked in next() is released.

Semantics: on retry or resume, Restate re-invokes the handler and replays the journal — ctx operations (sleep, call, get_state, run_*, ...) return their journaled results instantly, so handler logic must be deterministic between them; wrap non-deterministic work in run_enter/run_exit. When Restate suspends an invocation (e.g. a long sleep), the pending operation raises a suspension error: abandon the invocation and keep serving — Restate re-delivers it later.

Payloads are raw bytes on the wire; the String helpers below treat them as UTF-8 (use the _bytes variants for binary data).

Structs

Invocation

struct Invocation

One in-flight invocation: which handler was called, the virtual-object key (empty for plain services), and the request payload.

Implements Copyable, Movable

Fields
handle: Int
handler: String
key: String
id: String
input: List[UInt8]

input_string

def input_string(self) -> String

App

struct App

A Restate endpoint serving one service with byte/string handlers, driven by next().

Implements Movable

Fields
lib: OwnedDLHandle

__init__

def __init__(out self, service: String, handlers: List[String], object: Bool = True, workflow: Bool = False, host: String = "0.0.0.0", port: Int = Int(9080))

Start the endpoint (background thread in the shim). object=True registers a Virtual Object — keyed, with state and single-writer concurrency per key; False a plain (stateless) Service. workflow=True registers a Workflow: the handler named "run" is the workflow handler (runs once per key), the rest are shared and may use the promise operations to signal it.

sleep_ms

def sleep_ms(self, inv: Invocation, millis: Int)

Durable sleep. Journaled; long sleeps suspend the invocation.

get_state_bytes

def get_state_bytes(self, inv: Invocation, key: String) -> Optional[List[UInt8]]

get_state

def get_state(self, inv: Invocation, key: String) -> Optional[String]

get_state_int

def get_state_int(self, inv: Invocation, key: String, default: Int) -> Int

set_state_bytes

def set_state_bytes(self, inv: Invocation, key: String, value: List[UInt8])

set_state

def set_state(self, inv: Invocation, key: String, value: String)

set_state_int

def set_state_int(self, inv: Invocation, key: String, value: Int)

clear_state

def clear_state(self, inv: Invocation, key: String)

call

def call(self, inv: Invocation, service: String, handler: String, payload: String, key: String = "") -> String

Durable request/response call to another handler. A non-empty key targets a virtual object; empty targets a plain service.

send

def send(self, inv: Invocation, service: String, handler: String, payload: String, key: String = "", delay_ms: Int = Int(0))

Durable one-way message (optionally delayed).

awakeable_create

def awakeable_create(self, inv: Invocation) -> String

Create an awakeable — a durable promise resolvable from outside (another service, or the ingress HTTP API). Returns its id: hand it to the outside world (inside run_enter/run_exit or via call), then awakeable_await it.

awakeable_await

def awakeable_await(self, inv: Invocation, id: String) -> String

Await an awakeable created in this invocation; returns its resolved payload. Suspends if unresolved.

awakeable_resolve

def awakeable_resolve(self, inv: Invocation, id: String, value: String)

Resolve another invocation's awakeable with a payload.

awakeable_reject

def awakeable_reject(self, inv: Invocation, id: String, message: String)

Reject another invocation's awakeable with a terminal error.

promise_await

def promise_await(self, inv: Invocation, name: String) -> String

Await a named workflow promise (workflow services only).

promise_peek

def promise_peek(self, inv: Invocation, name: String) -> Optional[String]

Peek a named workflow promise: its value, or None if unresolved.

promise_resolve

def promise_resolve(self, inv: Invocation, name: String, value: String)

Resolve a named workflow promise (from a shared handler).

promise_reject

def promise_reject(self, inv: Invocation, name: String, message: String)

Reject a named workflow promise with a terminal error.

cancel_invocation

def cancel_invocation(self, inv: Invocation, invocation_id: String)

Cancel another invocation by id.

run_enter

def run_enter(self, inv: Invocation) -> Optional[String]

Enter a journaled side-effect block. Returns the journaled value on replay; None means: execute the side effect now, then close the block with run_exit (it succeeded) or run_fail (it did not).

Exactly one of those must follow. Leaving the block open makes the invocation unreplayable: every later attempt raises "protocol error: expected rst_run_exit".

run_exit

def run_exit(self, inv: Invocation, value: String) -> String

Journal the side-effect result produced after run_enter returned None. Returns the journaled value.

run_enter_policy

def run_enter_policy(self, inv: Invocation, initial_delay_ms: Int = Int(0), factor: Float64 = 0, max_delay_ms: Int = Int(0), max_attempts: Int = Int(0), max_duration_ms: Int = Int(0)) -> Optional[String]

run_enter, with a retry policy for this block's non-terminal failures.

Plain run_enter leaves retries to Restate's server-side invoker policy, which retries indefinitely. Set any of these to bound it; 0 means "leave that knob alone", and max_attempts or max_duration_ms of 0 mean unbounded.

When attempts or duration run out the SDK fails the block terminally, which arrives here the same way run_fail(terminal=True) does — as a raised error the handler can compensate for.

step

def step[F: def() raises -> String](self, inv: Invocation, f: F, initial_delay_ms: Int = Int(0), factor: Float64 = 0, max_delay_ms: Int = Int(0), max_attempts: Int = Int(0), max_duration_ms: Int = Int(0)) -> String

One journaled step: replay it, or run f and journal what it returned.

def charge() raises {imm reservation_id} -> String: return charge_card(reservation_id)

var charge_id = app.step(inv, charge)

f is passed by value with an explicit capture list, which keeps what it captures alive until step returns. Its type is a parameter, so each closure still gets its own monomorphised step.

This is run_enter / run_exit / run_fail with the protocol handled. It matters less for brevity than for the fact that a block cannot be left open: if f raises, the step is closed as a non-terminal failure and Restate runs it again, which is what the hand-written form has to remember to do every time.

The retry arguments are run_enter_policy's, and bound the retries of f itself. Passing none leaves them to Restate's invoker policy, which does not give up.

A step that should fail terminally — a downstream that is not coming back, where the handler compensates and finishes — wants the explicit protocol instead: run_enter, then run_fail(terminal=True).

run_fail

def run_fail(self, inv: Invocation, message: String, terminal: Bool = True) -> Optional[String]

Abort the run block opened by run_enter, rather than closing it with run_exit.

Every run_enter that returns None must be closed by exactly one of run_exit or run_fail. Leaving one open makes the invocation unreplayable: the next attempt raises "protocol error: expected rst_run_exit" and keeps doing so forever.

terminal=True journals the failure. The step is recorded as failed, replay reproduces the failure instead of executing again, and this raises so the handler can compensate and finish.

terminal=False journals nothing and lets Restate re-run the block under its own retry policy. The return value is then the next execute slot: None means "run the side effect again", and a value means the journal answered in the meantime.

complete

def complete(self, inv: Invocation, output: String)

Finish the invocation successfully with output.

fail

def fail(self, inv: Invocation, message: String)

Finish the invocation with a terminal error (not retried).

abandon

def abandon(self, inv: Invocation)

Drop an invocation without completing it (after suspension or an internal error). Restate retries/resumes it with journal replay.

stop

def stop(self)

Unblock every next() — in this thread or any other — so driver loops can end.

Setting a flag in Mojo cannot reach a thread parked inside the shim's blocking receiver, so this goes through the shim (rst_stop), which sets a process-wide flag its receive loop rechecks. Idempotent, and safe to call from inside a handler: that is how a service gives itself a shutdown endpoint.

The endpoint itself keeps listening — this ends the Mojo driver, not the HTTP server. There is no un-stop.

is_stopping

def is_stopping(self) -> Bool

Whether stop() has been called.

run

def run[T: AnyType, funcs: Tuple[funcs.Ts], /](service: String, ref state: T, object: Bool = True, workflow: Bool = False, host: String = "0.0.0.0", port: Int = Int(9080), num_workers: Int = Int(0)) -> Int

Discover the module's handlers, serve them, and block until stopped.

The whole driver:

@fieldwise_init
struct Stats(Copyable, Movable):
    var seen: Int64

def handle_add(app: App, inv: Invocation, w: Int, ctx: Ctx[Stats]) raises:
    app.complete(inv, String("ok"))

def main() raises:
    var stats = Stats(0)
    _ = App.run[Stats, __functions_in_module()]("Counter", stats)

Every handle_* in the calling module is registered under its name without the prefix and dispatched to by that name. There is no list to keep in step with the handlers, because there is no list.

Pass __functions_in_module() for funcs. state is process-local and shared by every worker — see Ctx.

Returns how many invocations completed without raising.

serve_with

def serve_with[T: AnyType, handler: def(Self, Invocation, Int, Ctx[T]) raises thin -> None](self, ref state: T, num_workers: Int = Int(0)) -> Int

serve, with the process-local state carried as a typed Ctx[T].

The same driver; the only difference is that the handler receives Ctx[T] instead of a raw OpaquePtr, so it can say ctx[].field rather than computing offsets. state must outlive the call, which it does: serve joins every worker before returning.

Prefer this over serve. The untyped form remains for handlers whose state is genuinely not a single struct.

serve

def serve[handler: def(Self, Invocation, Int, Pointer[UInt8, MutUntrackedOrigin]) raises thin -> None](self, num_workers: Int = Int(0), ctx: Pointer[UInt8, MutUntrackedOrigin] = opaque_ptr(Int(0))) -> Int

Run handler on num_workers threads until stop() is called.

The concurrent counterpart to the while True: app.next() loop, and the reason a handler may now call another handler served by this same process: with two or more workers, one thread waits for the callee while another picks it up. With one worker that is still a deadlock, exactly as before.

Blocks until every worker has left its loop, then returns.

Parameters
handler: The handler body — def(App, Invocation, Int, OpaquePtr) thin raises -> None, receiving the app, the invocation, its worker index, and ctx. Thin (non-capturing), because it is reached through a thread start routine; see the module docstring for the shape and the obligations.
Args
num_workers: How many driver threads. 0 (the default) means num_cpus(). Use at least 2 if any handler calls back into this process.
ctx: Process-local state shared by every handler invocation, passed straight through. Must outlive the serve call — which it does automatically, since serve joins before returning. Defaults to a null pointer for handlers that need none.

Raises Error: If the workers cannot be started or joined.

Unit

struct Unit

No process-local state.

A service that keeps everything in Restate state still has to name a type for Ctx. Unit is that type, and saying it out loud is worth more than it costs: it tells a reader the handlers share nothing across invocations.

Implements Copyable, Movable

Ctx

struct Ctx[T: AnyType]

A typed view of the process-local state serve hands to each handler.

Handlers must be thin, so state cannot be captured — it travels through a pointer instead. Ctx is that pointer with its type still attached, so a handler writes ctx[].live rather than reckoning byte offsets into an untyped block.

It does not make the state safe to share. Every worker thread gets the same T, concurrently. Fields that more than one handler touches have to be atomics, exactly as they would through a raw pointer.

When Mojo can carry captures across a thread boundary this whole parameter disappears from the signature and the state is simply captured; the body keeps working with ctx[].x renamed to x.

Implements Copyable, Movable

__getitem__

def __getitem__(self) -> ref[MutUntrackedOrigin] T

to

def to(ref state: T) -> Self

A Ctx over state, the one erasure served mode still performs: state is an argument of serve_with, which joins every worker before it returns, so it outlives the workers by construction even though the pointer no longer carries the origin that would say so.

from_opaque

def from_opaque(ptr: Pointer[UInt8, MutUntrackedOrigin]) -> Self

Rebuild the typed view inside a worker, from what serve passed.

opaque

def opaque(self) -> OpaquePtr

Functions

is_suspended

def is_suspended(e: Error) -> Bool

True if e is the suspension signal (abandon the invocation and keep serving).

is_stopped

def is_stopped(e: Error) -> Bool

True if e means stop() was called: next() will not deliver anything more, so leave the loop.

Aliases

comptime STATUS_OK = 0
comptime STATUS_GONE = 1
comptime STATUS_TERMINAL = 2
comptime STATUS_ERROR = 3
comptime STATUS_EMPTY = 4
comptime STATUS_EXECUTE = 5
comptime SUSPENDED_MSG = "restate: invocation suspended"
comptime STOPPED_MSG = "restate: driver stopped"
comptime HandlerFn = def(App, Invocation, Int, Pointer[UInt8, MutUntrackedOrigin]) raises thin -> None

What App.serve runs for each invocation: (app, inv, worker, ctx).

Thin, i.e. non-capturing — it is reached through a pthread start routine, which takes a C function pointer and one void *. Anything the handler needs beyond the app and the invocation travels through ctx, exactly as in threads.parallel_for.

It may raise, unlike a raw thread body: serve wraps each call, so a raise is caught, the invocation is abandoned (Restate re-delivers it), and the worker carries on. Suspension arrives this way too — is_suspended(e) — and is not logged, because it is normal.

A handler must finish its invocation: complete, fail, or raise. Returning without doing any of those leaks the invocation handle in the shim and leaves Restate waiting.