Restate’s
Programming Model

Restate’s programming model is designed to handle core aspects of every distributed application.

It makes it much easier to handle failover/recovery, reliability of communication, idempotency, asynchronous work, concurrency, and consistency of state.

Compute

(the application logic)

State

(transient and persistent)

Communication / Asynchrony

(between functions/services/APIs.)

1

Restate apps are just functions / services

Restate apps are simple services:

  • Functions, declared in the style of RPC handlers.

  • Once your functions are defined and registered you connect them to Restate.

(See Serving for details)

// define the functions as service handlers
export default restate.service({
  name: "hello-restate",  // the service name (or namespace)
  handlers: {
    // your functions go here
    hello: async (ctx: restate.Context, req: { who: string }) => {
      return `Hello ${req.who}!`;
    }
});

Push mode

Define an endpoint (HTTP/2, AWS Lambda, …) and deploy the application. Register the endpoint at the Restate Server.

Restate Server becomes a reverse proxy for the service and will push function invocations to the endpoint. This seamless supports FaaS and other auto-scaling deployments.

import type { HelloWorld } from "./hello";

const rs = restate.ingress.connect({ 
    uri: process.env.RESTATE_SERVER 
});

const response = await rs
  .serviceClient<HelloWorld>({ 
      name: "hello-restate" 
  })
  .greet({ who: "Ada" }); 

– or –

curl $RESTATE/hello-restate/greet \
  --json '{"who": "Ada" }'
const helloWorld = restate.service({
  name: "hello-restate",
  handlers: {
    greet: async (ctx, req) => { 
        /* ... */ 
    }
  }
});
export type HelloWorld = typeof helloWorld;

restate.endpoint()
  .bind(helloWorld)
  .listen(9080);

2

Reliable Compute: Workflows-as-Code (Durable Execution)

The core of every service is its business logic: code that decides what rules to follow and taking actions like calling databases, APIs, sending messages, etc.

That code can fail at any point in time:

Hardware / container / process crashes

Network
errors

Errors from
API calls

Timeouts

... and these are just some of the reasons for failures.

Restate uses the idea of Workflows-as-code (also called Durable Execution) to make application logic automatically resilient to many classes of failures.

Journaling

Functions called through Restate can memorize the results of statements and code blocks. This is called journaling.

async function applyRoleUpdate(ctx, update) {
  const { userID,  roleName, permissions } = update;
  
  // apply a change to external system (e.g., DB update).
  const roleID = await ctx.run("create role", () =>
  	createNewRole(roleName));

  // simply loop over the array or permission settings,
  // each step is journaled.
  for (const permission of permissions) {
    await ctx.run("apply permission", () => 
        applyPermission(roleId, permission));
  }
    
  await ctx.run("assign to user", () => 
      applyRole(userId,roleId)
  );
}

No workflow DSLs required: just code with its native control flow.

During retries, journaled results will be used, instead of re-executing the actions. Other operations managed by Restate (like state, messages, rpc, timers, etc.) are also automatically journaled.

async function applyRoleUpdate(ctx, update) {
  const { userID,  roleName, permissions } = update;
  
  // apply a change to external system (e.g., DB update).
  const roleID = await.ctx.run("create role", () => 
    createNewRole(roleName));

  // simply loop over the array or permission settings,
  // each step is journaled.
  for (const permission of permissions) {
    await ctx.run("apply permission", () => 
        applyPermission(roleId,  permission));
  }
    
  await ctx.run("assign to user", () => 
      applyRole(userId, roleId));
}

Reliable Retries

Restate Server stores invocation requests and journals durably. It retries reliably, attaching the previous journals.

This makes function executions resilient like workflows and suitable for mission-critical tasks.

3

Reliable Communication & Asynchrony: Durable Promises/Futures

Functions/handlers called through Restate run asynchronously and may run (and recover) across different processes.

Promises/Futures are the standard tool to interact with asynchronously executed code. Restate uses persistent 
versions of Promises/Futures to model interactions with functions.

async / await

future / await()

suspend fun
/ await( )

Promises/Futures for
Handler Invocations

Handlers can be called idempotently and call promises awaited synchronously.

const downloadLink = await rs
  .serviceClient(DataRetrieval)
  .prepareDownload(
      { userId: "Hedy" }, 
      opts({ idempotencyKey: "dQw4s9WgXcQ" })
   );
const dataRetrievalSvc = restate.service({
  name: "retrievedata",
  handlers: {
    prepareDownload: async (ctx, req) => {
      /* ... */
    }
  }
});

Handler call promises can be re-attached to after failures, or passed to other functions and processes.

// await only until call is enqueued
const handle = await rs
  .serviceSendClient(DataRetrieval)
  .prepareDownload({ userId: "Hedy" }, opts)

const r = await withTimeout(
    handle.result(), seconds(30)
);
if (r === Timeout) {
  sendViaEmail(JSON.stringify(callHandle))
}

“inv_18EnPmUxrhpy...”

sendViaEmail(json: string) {
  let handle = rs.result(JSON.parse(json));
  let result = await handle.result();
  sendEmail(result);
}
const dataRetrievalSvc = restate.service({
  name: "retrievedata",
  handlers: {
    prepareDownload: async (ctx, req) => {
      /* ... */
    }
  }
});

Handler calls can be scheduled into the future - milliseconds or months.

await rs
    .serviceSendClient(DataRetrieval, { 
        delay: days(1) 
     })
    .prepareDownload({ userId: "Hedy" });
const dataRetrievalSvc = restate.service({
  name: "retrievedata",
  handlers: {
    prepareDownload: async (ctx, req) => {
      /* ... */
    }
  }
});

Through this mechanism, your  functions are workflows are durable tasks are deferrable functions. They are all the same thing.

Everything you can do with Promises/Futures in code, you can also directly do via HTTP calls. Check the invocation docs for details.

curl $RESTATE/users/create --json '{...}'                   # wait for response
curl $RESTATE/users/create/send --json '{...}'              # wait until accepted
curl $RESTATE/users/create/send?delaySec=10 --json '{...}'  # schedule for later   

Promises / Futures for RPC

Handlers can also be called directly from other Restate handlers, yielding reliable exactly-once RPC. Durable Execution journals and recovers the invocation futures/promises, deduplicating calls and memorizing responses.

You write sequential RPC code, but get async event-driven execution.

The sequential request/response code runs the reliability, scalability, and decoupling of an event-driven architecture: Restate is the message queue, durable execution acts as the persistent state machine connecting requests and responses.

Promises / Futures for Signals

Durable Promises/Futures can be used as conditions and signals, and completed via webhooks, events, other handlers, human approval, etc. They work across processes boundaries and crashes.

const signup = workflow({
  name: "usersignup",
  handlers: {
    run: async (ctx: WorkflowContext, params: { ... }) => {
      // ...
      const secret = await ctx.run("gen secret", () => crypto.randomUUID());
      ctx.run("send email", () => sendEmailWithLink({ email, secret }));

      const clickSecret = await ctx.promise<string>("email-secret");
      if (clickSecret !== secret) {
        throw new TerminalError("verification failed");
      }
      // proceed with verification
    },

    linkClick: async (ctx: WorkflowSharedContext, secret: string) => {
      ctx.promise<string>("email-secret").resolve(secret);
    }
  }
});

4

State: Virtual Objects

Virtual Objects are special services that encapsulate state and concurrency on top of the workflow-as-code semantics.

Each object instance has a unique key and can store state in its context.

const greeter = restate.object({
  name: "greeter"
  handlers: {
    greet: async (ctx: restate.ObjectContext, request: {}) => {
      const count = ctx.get<number>("count") ?? 0;
      ctx.set("count", ++count);
      return `Hello ${ctx.key} for the ${count}-th time.`;
    },
    reset: async (ctx: restate.ObjectContext, request: {}) => {
      ctx.set("count", 0);
      return `Back to zero for ${ctx.key}.`;
    }
  }
});
curl -X POST restate:8080/greeter/Sonali/greet   # Hello Sonali for the 1-th time.
curl -X POST restate:8080/greeter/Peter/greet    # Hello Peter for the 1-th time.
curl -X POST restate:8080/greeter/Sonali/greet   # Hello Sonali for the 2-th time.
curl -X POST restate:8080/greeter/Peter/reset    # Back to zero for Peter.

For any object, only one handler can run at a time, giving that handler exclusive ownership of the object and state. Invocations are queued into infinitely fine-grained virtual queues. No key ever blocks another key.

Deploying Virtual Objects on FaaS like AWS Lambda gives you Stateful Serverless constructs. Durably executed code that scales (and scales to zero) around keys with safe exclusive access to state during execution.

5

Suspensions

While a handler awaits a Promise/Future, it can suspend (= stop executing) and it will be recovered (replayed back) once the awaited Promise/Future is complete or failed. This avoids occupying resources during long wait time (or any wait times on FaaS).

Handlers can also explicitly pause via  await ctx.sleep(time), for milliseconds or months. Through suspension, handlers may virtually run for many months, but actually execute only for fractions of the time.

Suspensions make it possible to do awaiting RPCs between serverless functions, like AWS Lambda, without paying for wait times.

See this post about workflows on AWS Lambda for an example.

Copyright © 2024 Restate. All rights reserved.