Skip to main content

Tour of Restate

This tutorial guides you through the development of an end-to-end Restate application, and covers all the essential features. After this tutorial, you should have a firm understanding of how Restate can help you and feel comfortable to tackle your next application on your own.

This tutorial implements a ticket reservation application for a theatre. It allows users to add tickets for specific seats to their shopping cart. After the user adds a ticket, the seat gets reserved for 15 minutes. If the user doesn't pay for the ticket within that time interval, the reservation is released and the ticket becomes available to other users.

Restate applications are made up of services that expose methods to each other and the outside world. Services communicate with one another using Remote Procedure Calls (RPC). Our ticket example consists of three services having the following responsibilities:

  1. User session: keeps track of the tickets in the shopping cart.
  2. Ticket service: manages the ticket allocation.
  3. Checkout: handles the checkout process of the ticket sale.

Tour of Restate diagram

As we go, you will discover how Restate can help you with some intricacies in this application.

Prerequisites​

  • Latest stable version of NodeJS >= v18.17.1 and npm CLI >= 9.6.7 installed.
  • Docker Engine or Podman to launch the Restate runtime (not needed for the app implementation itself).
  • curl

This guide is written for:

  • TypeScript SDK version: @restatedev/restate-sdk:0.8.1
  • Restate runtime Docker image: docker.io/restatedev/restate:0.8

Getting started​

Setting up the tutorial​

Download the tutorial:

restate example typescript-tour-of-restate && cd typescript-tour-of-restate

This GitHub repository contains the basic skeleton of the NodeJS/TypeScript services that you develop in this tutorial.

First, get all dependencies and build tools. Then build the app:

npm install && npm run build

Starting the components​

Let's run the services that we will be implementing. For now, most functions are empty placeholders.

npm run app

Next, let's launch Restate itself. Restate can be deployed as a single standalone binary. To run Restate with Docker:

docker run --name restate_dev --rm \
-p 8080:8080 -p 9070:9070 -p 9071:9071 \
--add-host=host.docker.internal:host-gateway \
docker.io/restatedev/restate:0.8

When you stop the container, any persisted state is dropped because we start the container with the --rm flag.

Registering services with Restate​

When Restate first comes up, it doesn't know anything about our services. Let's tell it where they are running. You can register services by calling the Restate Admin API (default port 9070) and supplying it the service endpoint URI:

curl localhost:9070/deployments \
-H 'content-type: application/json' \
-d '{"uri": "http://host.docker.internal:9080"}'

Once registered, Restate knows which services run behind the endpoint (including of the method signatures) and can invoke them.

You should now see the registered services printed in your terminal:

Output
{
"id": "bG9jYWxob3N0OjgwODAv",
"services": [
{
"name": "TicketService",
"revision": 1
/* ... Additional information on registered methods ...*/
},
{
"name": "UserSession",
"revision": 1
/* ... Additional information on registered methods ...*/
},
{
"name": "Checkout",
"revision": 1
/* ... Additional information on registered methods ...*/
}
]
}

Calling services from outside Restate​

Add a ticket to a cart by calling UserSession/addTicket.

curl localhost:8080/UserSession/addTicket \
-H 'content-type: application/json' \
-d '{"key": "123", "request": "456"}'

If this prints out true, then you have a working setup.

You can call the UserSession/checkout function to proceed with the purchase, as follows:

curl localhost:8080/UserSession/checkout \
-H 'content-type: application/json' \
-d '{"key": "123"}'

In src/app you will find the skeletons of the various services to help you start implementing the app. The app.ts file contains the definition of the endpoint that hosts the services.

Services and concurrency​

Restate supports two distinct kinds of services:

  1. Keyed service: All service invocations are sharded on a user-defined key. Function invocations of keyed services are serialized on the user-defined key. Therefore, at most one function can run at a time for a given key within such service.
  2. Unkeyed service: No key defined. No concurrency guarantees or limitations. Invocations are processed as they come in. You would get the same behavior with a keyed service with random keys.

In this example app, the Checkout service is an unkeyed service, while the TicketService and UserSession services are keyed services. The TicketService is keyed by the ticket ID, while the UserSession service is keyed by the user ID. So at most one invocation can run at a time for the same ticket ID or user ID respectively.

Specifying the service type​

Specifying unkeyed services

You specify an unkeyed service by creating a router with its set of functions. For example, for the Checkout service, you create a router with the handle function:

export const checkoutRouter = restate.router({
async handle(ctx: restate.Context, request: { userId: string; tickets: string[] }){
return true;
},
});

The functions of an unkeyed router have two parameters:

  • A ctx of type restate.Context which allows to interact with Restate.
  • A request of type JSON object which represents additional invocation data.

The Checkout service only has a single function, but you can add more functions to the router if you want.

Specifying keyed services

You specify a keyed service by creating a keyed router with its set of functions. For example, the UserSession service has the following keyed router:

export const userSessionRouter = restate.keyedRouter({
async addTicket(ctx: restate.KeyedContext, userId: string, ticketId: string){
return true;
},

async expireTicket(ctx: restate.KeyedContext, userId: string, ticketId: string){},

async checkout(ctx: restate.KeyedContext, userId: string){
return true;
},
});

The functions of a keyed router have three parameters

  • A ctx of type restate.KeyedContext which allows to interact with Restate.
  • A key parameter of type string which represents the key of the invoked service.
  • A request parameter of type JSON object which represents additional invocation data.

You can choose the parameter names differently.

As mentioned, for keyed services there can be at most one concurrent invocation to any method of the service. This means that for the same value of key, an unfinished call to addTicket() will block subsequent calls to expireTicket() or checkout() until addTicket() has finished.

Exporting the service API

You export the service API by creating a service API instance which specifies under which path the service is reachable.

For example, for the UserSession service, you export the following service API:

export const userSessionApi: restate.ServiceApi<typeof userSessionRouter> = {
path: "UserSession",
};

You can now call the service at the endpoint http://localhost:8080/<servicePath>/<functionName>. For example, you call the addTicket function of the UserSession service at http://localhost:8080/UserSession/addTicket, as you did earlier.

tip

The concurrency guarantees of keyed services simplify reasoning about interaction with external systems, because you are sure that there is only a single ongoing invocation for a single key. For example, imagine you have a keyed service which is the single writer to a database and every key only interacts with an isolated set of database rows. Then you can scale your application and never have concurrent invocations writing to the same database row. This resolves common data consistency issues such as lost updates or non-repeatable reads.

caution

Take into account the concurrency limitations when designing your applications!

  • Time-consuming operations in a keyed service, for example sleeps, lock that key/service for the entire operation. Other invocations for that key/service are enqueued, until the invocation has completed.
  • Deadlocks: Watch out for cyclical request-response calls in your application. For example, if A calls B, and B calls C, and C calls A again. Each service waits on the response of the other and none of them can progress. The keys remain locked and the system can't process any more requests.

Service calls​

Implement it yourself or follow along by looking at the code under part1.

One of the key parts of distributed applications is service-to-service communication. Calls are implemented as RPC messages. Restate makes service-to-service communication reliable by ensuring that messages do not get lost.

Reliable message sending without queues​

The tour begins with sending messages between services.

In this example, when you add a seat to your shopping cart, it gets reserved for 15 minutes. When a user didn't proceed with the payment before the timeout, you need to call the UserSession/expireTicket function. Let the expireTicket function call the TicketService/unreserve function.

async expireTicket(ctx: restate.KeyedContext, userId: string, ticketId: string){
ctx.send(ticketServiceApi).unreserve(ticketId);
},

Specify that you want to call the TicketService by supplying ticketServiceApi to the send function. Then call the unreserve function on the TicketService.

Once you have added this to the code, try it out by calling the UserSession/expireTicket function:

curl localhost:8080/UserSession/expireTicket \
-H 'content-type: application/json' \
-d '{"key": "123", "request": "456"}'

In the service logs, you see that the expireTicket function gets executed, after which the unreserve function gets executed. The call to expireTicket finishes earlier than the unreserve function because it didn't wait for the response of the unreserve function.

Show the logs
[restate] [UserSession/expireTicket][inv_1gdJBtdVEcM942bjcDmb1c1khoaJe11Hbz][2024-03-18T16:14:24.671Z] DEBUG: Invoking function.
[restate] [UserSession/expireTicket][inv_1gdJBtdVEcM942bjcDmb1c1khoaJe11Hbz][2024-03-18T16:14:24.672Z] DEBUG: Adding message to journal and sending to Restate ; BackgroundInvokeEntryMessage
[restate] [UserSession/expireTicket][inv_1gdJBtdVEcM942bjcDmb1c1khoaJe11Hbz][2024-03-18T16:14:24.673Z] DEBUG: Journaled and sent output message ; OutputStreamEntryMessage
[restate] [UserSession/expireTicket][inv_1gdJBtdVEcM942bjcDmb1c1khoaJe11Hbz][2024-03-18T16:14:24.673Z] DEBUG: Function completed successfully.
[restate] [TicketService/unreserve][inv_1k78Krj3GqrK3GuJXkgaXBbg69R47TCeAN][2024-03-18T16:14:24.677Z] DEBUG: Invoking function.
[restate] [TicketService/unreserve][inv_1k78Krj3GqrK3GuJXkgaXBbg69R47TCeAN][2024-03-18T16:14:24.677Z] DEBUG: Journaled and sent output message ; OutputStreamEntryMessage
[restate] [TicketService/unreserve][inv_1k78Krj3GqrK3GuJXkgaXBbg69R47TCeAN][2024-03-18T16:14:24.677Z] DEBUG: Function completed successfully.
Good news!

Restate persists and retries failed one-way invocations. There is no need to set up message queues to ensure delivery!

Calling services within Restate​

In the previous section, we covered sending messages to other services, without waiting for a response. With Restate, you can also make request-response calls (RPCs) where you do wait for the response.

When the UserSession/addTicket function is called, it first needs to reserve the ticket for the user. It does that by calling the TicketService/reserve function. Add the highlighted code snippet to the addTicket function in the UserSession service.

async addTicket(ctx: restate.KeyedContext, userId: string, ticketId: string){
const reservationSuccess = await ctx.rpc(ticketServiceApi).reserve(ticketId);
return true;
},

You do the call via ctx.rpc and supply the exported ticketServiceApi to specify which service to invoke. Then you call the reserve function, which returns a Promise that will get resolved with the boolean response.

Run the services via npm run app and send a request to UserSession/addTicket, as we did previously.

In the service logs, you see that the addTicket function gets executed, after which the reserve function gets executed. The call to addTicket finishes earlier than the reserve function because it didn't wait for the response of the reserve function.

Show the logs
[restate] [UserSession/addTicket][inv_1gdJBtdVEcM95mw1LLMMJY1Y0thJ9ugFGN][2024-03-18T16:30:28.790Z] DEBUG: Invoking function.
[restate] [UserSession/addTicket][inv_1gdJBtdVEcM95mw1LLMMJY1Y0thJ9ugFGN][2024-03-18T16:30:28.792Z] DEBUG: Adding message to journal and sending to Restate ; InvokeEntryMessage
[restate] [UserSession/addTicket][inv_1gdJBtdVEcM95mw1LLMMJY1Y0thJ9ugFGN][2024-03-18T16:30:28.792Z] DEBUG: Scheduling suspension in 30000 ms
[restate] [TicketService/reserve][inv_1k78Krj3GqrK6odGaRe866kHZilkVf1H4l][2024-03-18T16:30:28.796Z] DEBUG: Invoking function.
[restate] [TicketService/reserve][inv_1k78Krj3GqrK6odGaRe866kHZilkVf1H4l][2024-03-18T16:30:28.796Z] DEBUG: Journaled and sent output message ; OutputStreamEntryMessage
[restate] [TicketService/reserve][inv_1k78Krj3GqrK6odGaRe866kHZilkVf1H4l][2024-03-18T16:30:28.796Z] DEBUG: Function completed successfully.
[restate] [UserSession/addTicket][inv_1gdJBtdVEcM95mw1LLMMJY1Y0thJ9ugFGN][2024-03-18T16:30:28.799Z] DEBUG: Received completion message from Restate, adding to journal. ; CompletionMessage
[restate] [UserSession/addTicket][inv_1gdJBtdVEcM95mw1LLMMJY1Y0thJ9ugFGN][2024-03-18T16:30:28.800Z] DEBUG: Journaled and sent output message ; OutputStreamEntryMessage
[restate] [UserSession/addTicket][inv_1gdJBtdVEcM95mw1LLMMJY1Y0thJ9ugFGN][2024-03-18T16:30:28.800Z] DEBUG: Function completed successfully.

Reliable delivery and suspension​

The call you just added seems like a regular RPC call as you might know them from other service frameworks. This similarity is superficial; under the hood the Restate runtime and SDK cooperate to deliver resiliency and efficiency.

When an incoming request is sent to Restate, the runtime first persists it. It establishes a connection to the user session service, and sends the request over to begin a new invocation. All communications between the service and other Restate components goes over this connection.

When the user session service makes an RPC to the ticket service, this request goes over the open connection to the runtime. Once again, this is durably persisted in the Restate journal, before it is passed on (by Restate) to the ticket service. In case of failures, Restate takes care of retries. When the ticket service responds, its response is then sent back to the runtime, which persists it and forwards it to the user session service to continue execution.

When services take a while to respond, Restate suspends the calling execution to free up the resources for other invocations. When the calling service is unblocked to resume, Restate invokes it again and sends over a replay log. This replay log contains all the state that the previously completed steps generated prior to suspension. The service replays the log, and the service resumes execution from the point where it left off prior to suspending.

tip

The suspension mechanism is especially beneficial if you deploy your services to a FaaS platform such as AWS Lambda. This way you don't pay for the idle time a caller spends waiting for a response when making synchronous service calls within Restate!

Waiting for slow services​

To see this work in practice, let's pretend that ticket reservations take a while by making the TicketService/reserve function sleep for a while.

caution

This is not the best way to sleep in a Restate service! The Restate SDK offers a native way to suspend execution for a period of time. We will cover suspendable sleep later on.

Add the import:

import { setTimeout } from "timers/promises";

Add the sleep:

async reserve(ctx: restate.KeyedContext){
await setTimeout(35000);
return true;
},

Call UserSession/addTicket again and have a look at the SDK logs. Now that the ticket service responds after 35 seconds, you see that the addTicket function suspends after 30 seconds. Once the runtime has received the response, it invokes the addTicket function again and the original call finishes.

Show the logs
[restate] [UserSession/addTicket][inv_1gdJBtdVEcM968NomaJHFQH9MgVlD4B5AZ][2024-03-19T07:49:43.849Z] DEBUG: Invoking function.
[restate] [UserSession/addTicket][inv_1gdJBtdVEcM968NomaJHFQH9MgVlD4B5AZ][2024-03-19T07:49:43.850Z] DEBUG: Adding message to journal and sending to Restate ; InvokeEntryMessage
[restate] [UserSession/addTicket][inv_1gdJBtdVEcM968NomaJHFQH9MgVlD4B5AZ][2024-03-19T07:49:43.851Z] DEBUG: Scheduling suspension in 30000 ms
[restate] [TicketService/reserve][inv_1k78Krj3GqrK4tGbnNcKlrKTRsIDu7R6xz][2024-03-19T07:49:43.855Z] DEBUG: Invoking function.
[restate] [UserSession/addTicket][inv_1gdJBtdVEcM968NomaJHFQH9MgVlD4B5AZ][2024-03-19T07:50:13.853Z] DEBUG: Writing suspension message to journal. ; SuspensionMessage
[restate] [UserSession/addTicket][inv_1gdJBtdVEcM968NomaJHFQH9MgVlD4B5AZ][2024-03-19T07:50:13.853Z] DEBUG: Suspending function.
[restate] [TicketService/reserve][inv_1k78Krj3GqrK4tGbnNcKlrKTRsIDu7R6xz][2024-03-19T07:50:18.860Z] DEBUG: Journaled and sent output message ; OutputStreamEntryMessage
[restate] [TicketService/reserve][inv_1k78Krj3GqrK4tGbnNcKlrKTRsIDu7R6xz][2024-03-19T07:50:18.861Z] DEBUG: Function completed successfully.
[restate] [UserSession/addTicket][inv_1gdJBtdVEcM968NomaJHFQH9MgVlD4B5AZ][2024-03-19T07:50:18.906Z] DEBUG: Resuming (replaying) function.
[restate] [UserSession/addTicket][inv_1gdJBtdVEcM968NomaJHFQH9MgVlD4B5AZ][2024-03-19T07:50:18.906Z] DEBUG: Matched and replayed message from journal ; InvokeEntryMessage
[restate] [UserSession/addTicket][inv_1gdJBtdVEcM968NomaJHFQH9MgVlD4B5AZ][2024-03-19T07:50:18.906Z] DEBUG: Journaled and sent output message ; OutputStreamEntryMessage
[restate] [UserSession/addTicket][inv_1gdJBtdVEcM968NomaJHFQH9MgVlD4B5AZ][2024-03-19T07:50:18.906Z] DEBUG: Function completed successfully.
Cancelling and killing invocations

By default, Restate will keep retrying failed invocations until they succeed. If at any point during testing you create an invocation that you want to permanently terminate, you can cancel (docs) or kill (docs) it its invocation ID. Each invocation gets an ID assigned by Restate. You can find the ID printed in the service logs.

πŸ“ Try it out​

Make the UserSession/checkout function call the Checkout/handle function.

For the request field, you can use a hard-coded string array for now: ["456"]. You will fix this later on.

Solution

Add the following code to the UserSession/checkout function:

Import the checkout API:

import { checkoutApi } from "./checkout";

Then call the Checkout/handle function:

async checkout(ctx: restate.KeyedContext, userId: string) {
const checkoutRequest = { userId: userId, tickets: ["456"] };
const success = await ctx.rpc(checkoutApi).handle(checkoutRequest);

return success;
},

Call the UserSession/checkout function as you did earlier and have a look at the logs again to see what happened:

[restate] [UserSession/checkout][inv_1gdJBtdVEcM919dOBhoVBm3fUMlaIHnANr][2024-03-19T07:57:24.018Z] DEBUG: Invoking function.
[restate] [UserSession/checkout][inv_1gdJBtdVEcM919dOBhoVBm3fUMlaIHnANr][2024-03-19T07:57:24.019Z] DEBUG: Adding message to journal and sending to Restate ; InvokeEntryMessage
[restate] [UserSession/checkout][inv_1gdJBtdVEcM919dOBhoVBm3fUMlaIHnANr][2024-03-19T07:57:24.020Z] DEBUG: Scheduling suspension in 30000 ms
[restate] [Checkout/handle][inv_16WnWCiCVV5G2gUUevDM4uIli4v7TN9k2d][2024-03-19T07:57:24.023Z] DEBUG: Invoking function.
[restate] [Checkout/handle][inv_16WnWCiCVV5G2gUUevDM4uIli4v7TN9k2d][2024-03-19T07:57:24.024Z] DEBUG: Journaled and sent output message ; OutputStreamEntryMessage
[restate] [Checkout/handle][inv_16WnWCiCVV5G2gUUevDM4uIli4v7TN9k2d][2024-03-19T07:57:24.024Z] DEBUG: Function completed successfully.
[restate] [UserSession/checkout][inv_1gdJBtdVEcM919dOBhoVBm3fUMlaIHnANr][2024-03-19T07:57:24.026Z] DEBUG: Received completion message from Restate, adding to journal. ; CompletionMessage
[restate] [UserSession/checkout][inv_1gdJBtdVEcM919dOBhoVBm3fUMlaIHnANr][2024-03-19T07:57:24.027Z] DEBUG: Journaled and sent output message ; OutputStreamEntryMessage
[restate] [UserSession/checkout][inv_1gdJBtdVEcM919dOBhoVBm3fUMlaIHnANr][2024-03-19T07:57:24.027Z] DEBUG: Function completed successfully.

Notice that the UserSession didn't get suspended because the response came before the suspension timeout hit.

info

🚩 Explore the intermediate solution in part1, and run it with:

npm run part1

Durable timers​

Implement it yourself or follow along by looking at the code under part2.

Restate suspends a function invocation when it waits on external input. The partial progress of the service invocation is durably stored in the log and can be resumed once the external input has arrived. Restate offers the same mechanism for timers.

Suspendable sleep​

Earlier in this tutorial, you saw how Restate caller suspension works when you make an RPC request to another service. We artificially slowed down the target service by using platform-native sleep. Sometimes you intentionally want to suspend execution for a period of time, for example to back off from overloading a dependency or simply you are implementing a long-running business workflow. In those situations you can explicitly suspend execution for arbitrarily long periods of time using the Restate context's sleep function. Adapt the TicketService/reserve function to use ctx.sleep:

async reserve(ctx: restate.KeyedContext){
await ctx.sleep(35000);
return true;
},

Send an addTicket request, as you did earlier. Now you see in the logs that both services get suspended. The user session service gets suspended because it waits for a response from the reserve function and the ticket service gets suspended because it waits for the sleep to be completed. Restate keeps track of how long the service should sleep and then triggers it to resume the invocation. Finally, you see the responses of both functions coming in.

Show the logs
restate] [UserSession/addTicket][inv_1gdJBtdVEcM914R2BTwQ8Ioyfjy4ap9PxL][2024-03-19T08:01:18.538Z] DEBUG: Invoking function.
[restate] [UserSession/addTicket][inv_1gdJBtdVEcM914R2BTwQ8Ioyfjy4ap9PxL][2024-03-19T08:01:18.539Z] DEBUG: Adding message to journal and sending to Restate ; InvokeEntryMessage
[restate] [UserSession/addTicket][inv_1gdJBtdVEcM914R2BTwQ8Ioyfjy4ap9PxL][2024-03-19T08:01:18.539Z] DEBUG: Scheduling suspension in 30000 ms
[restate] [TicketService/reserve][inv_1k78Krj3GqrK5VGOBoinuc1fiN1eIwKGWZ][2024-03-19T08:01:18.543Z] DEBUG: Invoking function.
[restate] [TicketService/reserve][inv_1k78Krj3GqrK5VGOBoinuc1fiN1eIwKGWZ][2024-03-19T08:01:18.543Z] DEBUG: Adding message to journal and sending to Restate ; SleepEntryMessage
[restate] [TicketService/reserve][inv_1k78Krj3GqrK5VGOBoinuc1fiN1eIwKGWZ][2024-03-19T08:01:18.543Z] DEBUG: Scheduling suspension in 30000 ms
[restate] [UserSession/addTicket][inv_1gdJBtdVEcM914R2BTwQ8Ioyfjy4ap9PxL][2024-03-19T08:01:48.568Z] DEBUG: Writing suspension message to journal. ; SuspensionMessage
[restate] [UserSession/addTicket][inv_1gdJBtdVEcM914R2BTwQ8Ioyfjy4ap9PxL][2024-03-19T08:01:48.568Z] DEBUG: Suspending function.
[restate] [TicketService/reserve][inv_1k78Krj3GqrK5VGOBoinuc1fiN1eIwKGWZ][2024-03-19T08:01:48.569Z] DEBUG: Writing suspension message to journal. ; SuspensionMessage
[restate] [TicketService/reserve][inv_1k78Krj3GqrK5VGOBoinuc1fiN1eIwKGWZ][2024-03-19T08:01:48.569Z] DEBUG: Suspending function.
[restate] [TicketService/reserve][inv_1k78Krj3GqrK5VGOBoinuc1fiN1eIwKGWZ][2024-03-19T08:01:53.554Z] DEBUG: Resuming (replaying) function.
[restate] [TicketService/reserve][inv_1k78Krj3GqrK5VGOBoinuc1fiN1eIwKGWZ][2024-03-19T08:01:53.554Z] DEBUG: Matched and replayed message from journal ; SleepEntryMessage
[restate] [TicketService/reserve][inv_1k78Krj3GqrK5VGOBoinuc1fiN1eIwKGWZ][2024-03-19T08:01:53.554Z] DEBUG: Journaled and sent output message ; OutputStreamEntryMessage
[restate] [TicketService/reserve][inv_1k78Krj3GqrK5VGOBoinuc1fiN1eIwKGWZ][2024-03-19T08:01:53.554Z] DEBUG: Function completed successfully.
[restate] [UserSession/addTicket][inv_1gdJBtdVEcM914R2BTwQ8Ioyfjy4ap9PxL][2024-03-19T08:01:53.598Z] DEBUG: Resuming (replaying) function.
[restate] [UserSession/addTicket][inv_1gdJBtdVEcM914R2BTwQ8Ioyfjy4ap9PxL][2024-03-19T08:01:53.598Z] DEBUG: Matched and replayed message from journal ; InvokeEntryMessage
[restate] [UserSession/addTicket][inv_1gdJBtdVEcM914R2BTwQ8Ioyfjy4ap9PxL][2024-03-19T08:01:53.598Z] DEBUG: Journaled and sent output message ; OutputStreamEntryMessage
[restate] [UserSession/addTicket][inv_1gdJBtdVEcM914R2BTwQ8Ioyfjy4ap9PxL][2024-03-19T08:01:53.598Z] DEBUG: Function completed successfully.

Reduce the time the TicketService/reserve function sleeps to a second, so you won't see the suspensions anymore.

caution

In keyed services, sleeping blocks all further processing (for the specific single key / all invocations, respectively).

Delayed calls​

Take a look at a slightly different usage of timers. In the application, a ticket gets reserved for 15 minutes. If the user doesn't pay within that time interval, then it becomes available again to other users.

To do this, you can use a delayed call. This is a one-way call that gets delayed by a specified duration. This would make the UserSession/addTicket function look as follows:

async addTicket(ctx: restate.KeyedContext, userId: string, ticketId: string){
const reservationSuccess = await ctx.rpc(ticketServiceApi).reserve(ticketId);

if (reservationSuccess) {
ctx.sendDelayed(userSessionApi, 15 * 60 * 1000).expireTicket(userId, ticketId);
}

return reservationSuccess;
},

To test it out, put the delay to a lower value, for example 5 seconds, call the addTicket function, and see in the logs how the call to UserSession/expireTicket is executed 5 seconds later.

Show the logs
... logs from reserve call ...
[restate] [UserSession/addTicket][inv_1gdJBtdVEcM90xbqbDEnOzNgilf2WmjZTP][2024-03-19T08:49:43.081Z] DEBUG: Received completion message from Restate, adding to journal. ; CompletionMessage
[restate] [UserSession/addTicket][inv_1gdJBtdVEcM90xbqbDEnOzNgilf2WmjZTP][2024-03-19T08:49:43.081Z] DEBUG: Adding message to journal and sending to Restate ; BackgroundInvokeEntryMessage
[restate] [UserSession/addTicket][inv_1gdJBtdVEcM90xbqbDEnOzNgilf2WmjZTP][2024-03-19T08:49:43.081Z] DEBUG: Journaled and sent output message ; OutputStreamEntryMessage
[restate] [UserSession/addTicket][inv_1gdJBtdVEcM90xbqbDEnOzNgilf2WmjZTP][2024-03-19T08:49:43.081Z] DEBUG: Function completed successfully.
[restate] [UserSession/expireTicket][inv_1gdJBtdVEcM93r8tce9IfwnbiAsk8lCevD][2024-03-19T08:49:48.092Z] DEBUG: Invoking function.
[restate] [UserSession/expireTicket][inv_1gdJBtdVEcM93r8tce9IfwnbiAsk8lCevD][2024-03-19T08:49:48.093Z] DEBUG: Adding message to journal and sending to Restate ; BackgroundInvokeEntryMessage
[restate] [UserSession/expireTicket][inv_1gdJBtdVEcM93r8tce9IfwnbiAsk8lCevD][2024-03-19T08:49:48.093Z] DEBUG: Journaled and sent output message ; OutputStreamEntryMessage
[restate] [UserSession/expireTicket][inv_1gdJBtdVEcM93r8tce9IfwnbiAsk8lCevD][2024-03-19T08:49:48.093Z] DEBUG: Function completed successfully.
[restate] [TicketService/unreserve][inv_1k78Krj3GqrK529L4BRmz8ntFtiw2DkahH][2024-03-19T08:49:48.141Z] DEBUG: Invoking function.
[restate] [TicketService/unreserve][inv_1k78Krj3GqrK529L4BRmz8ntFtiw2DkahH][2024-03-19T08:49:48.141Z] DEBUG: Journaled and sent output message ; OutputStreamEntryMessage
[restate] [TicketService/unreserve][inv_1k78Krj3GqrK529L4BRmz8ntFtiw2DkahH][2024-03-19T08:49:48.141Z] DEBUG: Function completed successfully.

Don't forget to set the delay back to 15 minutes.

caution

You can implement this pattern in different ways. You could also sleep for 15 minutes at the end of the addTicket function and then call the TicketService/unreserve function:

await ctx.sleep(15 * 60 * 1000);
ctx.send(ticketServiceApi).unreserve(ticketId);

Be aware that sleeping in a keyed service blocks any invocations for that key. In this case, the user would not be able to add any other tickets, nor buy the tickets.

If you do a sleep operation, the invocation is ongoing. If you do a delayed call, the invocation isn't ongoing until the delay has passed, so no key is locked.

info

🚩 Explore the intermediate solution in part2, and run it with:

npm run part2

Persistent application state​

Implement it yourself or follow along by looking at the code under part3.

Applications often need to keep state. For example, the user session service needs to track the shopping cart items.

Restate offers a key-value store to persistently store application state.

Good news!

Restate's state is guaranteed to be consistent across retries and invocations. This eliminates the need for a session database.

The isolation level of the application state depends on the service type:

  1. Keyed service: Application state is isolated per key. All the invocations for the same key have access to the same application state. Restate's state feature is most useful for this service type.
  2. Unkeyed service: State is isolated per invocation. Using state isn't useful for this service type.

Getting and setting K/V state​

Adapt the UserSession/addTicket function to keep track of the cart items. After reserving the product, you add the ticket to the shopping cart. Have a look at the highlighted code:

async addTicket(ctx: restate.KeyedContext, userId: string, ticketId: string){
const reservationSuccess = await ctx.rpc(ticketServiceApi).reserve(ticketId);

if (reservationSuccess) {
const tickets = (await ctx.get<string[]>("tickets")) ?? [];
tickets.push(ticketId);
ctx.set("tickets", tickets);

ctx.sendDelayed(userSessionApi, 15 * 60 * 1000).expireTicket(userId, ticketId);
}

return reservationSuccess;
},

To retrieve the cart, you use ctx.get. This returns null if the value has never been set.

You can store multiple key-value pairs, by using different state keys. Here, you get the value under the key "tickets". Restate ensures that you get the cart belonging to the current user ID (key of the service). After you added the ticket to the cart array, you set the state to the new value with ctx.set.

Run the services and call the addTicket function, to see the interaction with state.

Show the logs
... logs from reserve call ...
[restate] [UserSession/addTicket][inv_1gdJBtdVEcM97yGYEG5NWYtbMlnSAGHGY9][2024-03-19T08:55:20.941Z] DEBUG: Adding message to journal and sending to Restate ; GetStateEntryMessage
[restate] [UserSession/addTicket][inv_1gdJBtdVEcM97yGYEG5NWYtbMlnSAGHGY9][2024-03-19T08:55:20.941Z] DEBUG: Adding message to journal and sending to Restate ; SetStateEntryMessage
... logs from expireTicket call ...

You see that when you request the state, it gets logged in the journal.

Serialization

You can store any object in the state as long as the value can be serialized with Buffer.from(JSON.stringify(yourObject)).

When starting the invocation, Restate attaches the application state to the request. So when you operate on the state in your function with ctx.get and ctx.set, you get access to a local copy of the state for fast access.

Inspecting K/V state​

Restate exposes information on invocations and application state via its Introspection SQL API. You can use this to gain insight into the status of invocations and the service state that is stored. You can inspect the application state of the UserSession service by using psql, as follows:

psql -h localhost -p 9071 -c "SELECT * FROM state WHERE service like '%UserSession%';"

Or watch how the state changes with:

watch -n 1 'psql -h localhost -p 9071 -c "SELECT * FROM state WHERE service like '\''%UserSession%'\''";'

Add several tickets to the state to see how the query result gets updated.

You can also stop and relaunch the service, to see that this has no influence on the tickets.

Now, adapt the UserSession/checkout function, to use the tickets:

async checkout(ctx: restate.KeyedContext, userId: string){
const tickets = (await ctx.get<string[]>("tickets")) ?? [];

if (tickets.length === 0) {
return false;
}

const checkoutSuccess = await ctx.rpc(checkoutApi)
.handle({ userId: userId, tickets: tickets });

if (checkoutSuccess) {
ctx.clear("tickets");
}

return checkoutSuccess;
},

After the tickets are checked out, you clear the state with ctx.clear. Send a checkout request as earlier, and check via psql that the state is empty.

tip

Have a look at the documentation on introspection to learn more about inspecting the invocation status and application state.

πŸ“ Try it out​

Finishing UserSession/expireTicket​

You have almost fully implemented the user session service. After checkout, let's finish UserSession/expireTicket. Before you call unreserve, you first need to check if the ticket is still held by the user. Retrieve the state and check if the ticket ID is in there. If this is the case, then you call TicketService/unreserve and remove it from the state.

Solution
async expireTicket(ctx: restate.KeyedContext, userId: string, ticketId: string){
const tickets = (await ctx.get<string[]>("tickets")) ?? [];

const ticketIndex = tickets.findIndex((ticket) => ticket === ticketId);

if (ticketIndex != -1) {
tickets.splice(ticketIndex, 1);
ctx.set("tickets", tickets);

ctx.send(ticketServiceApi).unreserve(ticketId);
}
},

Call the expireTicket function with:

curl localhost:8080/UserSession/expireTicket \
-H 'content-type: application/json' \
-d '{"key": "123", "request": "456"}'

Implementing the ticket service​

You can track the status of the tickets in the ticket service by storing it in the state. Implement the TicketService/reserve, TicketService/unreserve, and TicketService/markAsSold functions to do this.

While you are developing them, you can use psql to monitor the state of the TicketService:

watch -n 1 'psql -h localhost -p 9071 -c "select service, service_key, key, value_utf8 from state where service like '\''%TicketService%'\''";'
  1. Implement the TicketService/reserve function. The function first retrieves the value for the "status" state key. If the value is set to TicketStatus.Available, then change it to TicketStatus.Reserved and return true (reservation successful). If the status isn't set to TicketStatus.Available, then return false. Remove the sleep that you added before.
Solution
async reserve(ctx: restate.KeyedContext){
const status =
(await ctx.get<TicketStatus>("status")) ?? TicketStatus.Available;

if (status === TicketStatus.Available) {
ctx.set("status", TicketStatus.Reserved);
return true;
} else {
return false;
}
},

Now, you can't reserve the same ticket multiple times anymore. Call addTicket multiple times for the same ID. The first time it returns true, afterwards false.

  1. Implement the TicketService/unreserve function. The function should clear the value of the "status" key if it's not on status TicketStatus.Sold.
Solution
async unreserve(ctx: restate.KeyedContext) {
const status =
(await ctx.get<TicketStatus>("status")) ?? TicketStatus.Available;

if (status !== TicketStatus.Sold) {
ctx.clear("status");
}
},

Now, the ticket reservation status is cleared when the delayed expireTicket call triggers. Play around with reducing the delay of the expireTicket call in the addTicket function. Try to reserve the same ticket ID multiple times, and see how you are able to reserve it again after the unreserve function executed.

  1. Implement the TicketService/markAsSold function. The function should set the value of the "status" key to TicketStatus.Sold if it's reserved.
Solution
async markAsSold(ctx: restate.KeyedContext){
const status =
(await ctx.get<TicketStatus>("status")) ?? TicketStatus.Available;

if (status === TicketStatus.Reserved) {
ctx.set("status", TicketStatus.Sold);
}
},

In the next section, you implement the Checkout/handle function that calls markAsSold. This ties the final parts together.

info

🚩 Explore the intermediate solution in part3, and run it with:

npm run part3

Determinism & side effects​

Implement it yourself or follow along by looking at the code under part4.

Restate's replay mechanism makes applications resilient to failures. For the replay to work, code needs to be deterministic, otherwise the replayed entries do not line up with the code execution on retries. For non-deterministic code, the SDK offers side effects, for example, to log the result of communication with external systems. The SDK also offers helper functions for creating UUIDs and generating random numbers.

In Checkout/handle, we first want to trigger the payment. This happens in two steps:

  1. First, we use the SDK helper functions to generate an idempotency token for the payment. This token will be supplied in the payment request to ensure that a customer never pays multiple times.
  2. Then, we use a side effect to trigger the payment via an external payment provider (PaymentClient). You can find a payment client stub in auxiliary/PaymentClient. Do the following:
    1. Create a new payment client in Checkout/handle.
    2. From within a side effect, execute the payment via paymentClient.call(idempotencyKey, amount). Assume every ticket costs 40 dollars. Store the boolean result of the call as the return value of the side effect.

The side effect executes the supplied function and stores the return value in Restate. Upon replay, the stored value gets inserted and the function doesn't get re-executed.

Add the following code snippet to the Checkout/handle function:

const totalPrice = request.tickets.length * 40;

const idempotencyKey = ctx.rand.uuidv4();
const success = await ctx.sideEffect(() => PaymentClient.get().call(idempotencyKey, totalPrice));

The idempotency token will be the same during retries. To experiment with this, you can print the idempotency key to the console and then throw an error to see how it gets replayed:

const idempotencyKey = ctx.rand.uuidv4();
console.info("My idempotency key: " + idempotencyKey);
throw new Error("Something happened!");

Call UserSession/checkout and have a look at the logs to see what happens. Don't forget to remove the throwing of the error again before you continue.

Show the logs
... logs of `UserSessionService/Checkout` ...
[restate] [Checkout/handle][inv_1jhuapyO2Bpg3prqzrAJOFs99mt7jv5x3r][2024-03-19T09:15:30.498Z] DEBUG: Invoking function.
My idempotency key: e25b747f-ecfb-443b-8939-1935392aab6b
Trace: [restate] [Checkout/handle][inv_1jhuapyO2Bpg3prqzrAJOFs99mt7jv5x3r][2024-03-19T09:15:30.499Z] TRACE: Function completed with an error: Something happened! Error: Something happened!
... rest of trace ...
[restate] [Checkout/handle][inv_1jhuapyO2Bpg3prqzrAJOFs99mt7jv5x3r][2024-03-19T09:15:30.512Z] DEBUG: Invocation ended with retryable error. ; ErrorMessage
[restate] [Checkout/handle][inv_1jhuapyO2Bpg3prqzrAJOFs99mt7jv5x3r][2024-03-19T09:15:30.568Z] DEBUG: Invoking function.
My idempotency key: e25b747f-ecfb-443b-8939-1935392aab6b
Trace: [restate] [Checkout/handle][inv_1jhuapyO2Bpg3prqzrAJOFs99mt7jv5x3r][2024-03-19T09:15:30.568Z] TRACE: Function completed with an error: Something happened! Error: Something happened!
... rest of trace ...
[restate] [Checkout/handle][inv_1jhuapyO2Bpg3prqzrAJOFs99mt7jv5x3r][2024-03-19T09:15:30.568Z] DEBUG: Invocation ended with retryable error. ; ErrorMessage
... retries continue ...
tip

You can use a side effect to avoid re-execution during replay for any arbitrary piece of user code. This includes pieces of time-consuming, deterministic user code.

caution

You can't execute any context calls from within a side effect. This is invalid code.

πŸ“ Try it out​

Once this works, you implement the rest of the checkout workflow:

  • If the payment call was successful, then:
    • use the EmailClient to notify the users of the payment success. Prevent duplicate emails during retries by using a side effect.
    • let the calling UserSession mark all tickets as sold via TicketService/markAsSold.
  • If the payment call was unsuccessful, then:
    • use the EmailClient to notify the users of the payment failure. Prevent duplicate emails during retries by using a side effect.
Solution

The Checkout/handle function now looks like the following:

async handle(ctx: restate.Context, request: { userId: string; tickets: string[] }){
// <start_side_effects>
const totalPrice = request.tickets.length * 40;

const idempotencyKey = ctx.rand.uuidv4();
const success = await ctx.sideEffect(() => PaymentClient.get().call(idempotencyKey, totalPrice));
// <end_side_effects>

if (success) {
await ctx.sideEffect(() => EmailClient.get().notifyUserOfPaymentSuccess(request.userId));
} else {
await ctx.sideEffect(() => EmailClient.get().notifyUserOfPaymentFailure(request.userId));
}

return success;
},

The UserSession/checkout function now looks like the following:

async checkout(ctx: restate.KeyedContext, userId: string){
const tickets = (await ctx.get<string[]>("tickets")) ?? [];

if (tickets.length === 0) {
return false;
}

const checkoutSuccess = await ctx.rpc(checkoutApi)
.handle({ userId: userId, tickets: tickets! });

if (checkoutSuccess) {
for (const ticketId of tickets) {
ctx.send(ticketServiceApi).markAsSold(ticketId);
}
ctx.clear("tickets");
}

return checkoutSuccess;
},

πŸ₯³ Except for a few resiliency tweaks, you have now fully implemented the ticket reservation system! Try it out by reserving some new tickets and buying them by checking out the cart.

info

🚩 Explore the intermediate solution in part4, and run it with:

npm run part4

Resiliency​

Implement it yourself or follow along by looking at the code under part5.

As you have discovered throughout this tutorial, Restate makes your applications resilient out-of-the-box by:

  • Making sure messages can't get lost by durably storing all calls in the log. No more queues needed for asynchronous calls.
  • Retrying failed invocations. No more retry logic required for inter-service communication.
  • Restoring partial progress of invocations, via its durable execution mechanism. For example, if a service goes down due to an infrastructure failure, the ongoing invocations resume at the point in the user code where they left off.
  • Ensuring consistent application state with its key-value store.
  • Providing end-to-end exactly-once guarantees for incoming invocations.
Good news!

These features are switched on by default. No need to do anything.

Side effect retries​

Also side effects are retried until they succeed. You can observe this behavior by using the paymentClient.failingCall in Checkout/handle:

const success = await ctx.sideEffect(() => PaymentClient.get().failingCall(idempotencyKey, totalPrice));

You now call paymentClient.failingCall which fails two times before succeeding. A failing side effect is retried until it succeeds. Have a look at the logs to see the retries.

Show the logs
[restate] [Checkout/handle][inv_1eVZwghxruIa0YY4RtWg1viemwoyZ7Q6Ot][2024-03-19T10:19:02.676Z] DEBUG: Invoking function.
Payment call failed for idempotency key 608e3977-7c91-4fc2-bf56-3c9c0c2cbabd and amount 40. Retrying...
[restate] [Checkout/handle][inv_1eVZwghxruIa0YY4RtWg1viemwoyZ7Q6Ot][2024-03-19T10:19:02.678Z] DEBUG: Adding message to journal and sending to Restate ; SideEffectEntryMessage
[restate] [Checkout/handle][inv_1eVZwghxruIa0YY4RtWg1viemwoyZ7Q6Ot][2024-03-19T10:19:02.678Z] DEBUG: Scheduling suspension in 30000 ms
[restate] [Checkout/handle][inv_1eVZwghxruIa0YY4RtWg1viemwoyZ7Q6Ot][2024-03-19T10:19:02.679Z] DEBUG: Received entry ack message from Restate, adding to journal. ; EntryAckMessage
[restate] [Checkout/handle][inv_1eVZwghxruIa0YY4RtWg1viemwoyZ7Q6Ot][2024-03-19T10:19:02.680Z] DEBUG: Error while executing side effect 'side-effect': Error - Payment call failed
[restate] [Checkout/handle][inv_1eVZwghxruIa0YY4RtWg1viemwoyZ7Q6Ot][2024-03-19T10:19:02.692Z] DEBUG: Error: Payment call failed
... rest of trace ...
[restate] [Checkout/handle][inv_1eVZwghxruIa0YY4RtWg1viemwoyZ7Q6Ot][2024-03-19T10:19:02.692Z] DEBUG: Retrying in 10 ms
[restate] [Checkout/handle][inv_1eVZwghxruIa0YY4RtWg1viemwoyZ7Q6Ot][2024-03-19T10:19:02.692Z] DEBUG: Adding message to journal and sending to Restate ; SleepEntryMessage
[restate] [Checkout/handle][inv_1eVZwghxruIa0YY4RtWg1viemwoyZ7Q6Ot][2024-03-19T10:19:02.692Z] DEBUG: Scheduling suspension in 30000 ms
[restate] [Checkout/handle][inv_1eVZwghxruIa0YY4RtWg1viemwoyZ7Q6Ot][2024-03-19T10:19:02.705Z] DEBUG: Received completion message from Restate, adding to journal. ; CompletionMessage
Payment call failed for idempotency key 608e3977-7c91-4fc2-bf56-3c9c0c2cbabd and amount 40. Retrying...
[restate] [Checkout/handle][inv_1eVZwghxruIa0YY4RtWg1viemwoyZ7Q6Ot][2024-03-19T10:19:02.705Z] DEBUG: Adding message to journal and sending to Restate ; SideEffectEntryMessage
[restate] [Checkout/handle][inv_1eVZwghxruIa0YY4RtWg1viemwoyZ7Q6Ot][2024-03-19T10:19:02.705Z] DEBUG: Scheduling suspension in 30000 ms
[restate] [Checkout/handle][inv_1eVZwghxruIa0YY4RtWg1viemwoyZ7Q6Ot][2024-03-19T10:19:02.706Z] DEBUG: Received entry ack message from Restate, adding to journal. ; EntryAckMessage
[restate] [Checkout/handle][inv_1eVZwghxruIa0YY4RtWg1viemwoyZ7Q6Ot][2024-03-19T10:19:02.706Z] DEBUG: Error while executing side effect 'side-effect': Error - Payment call failed
[restate] [Checkout/handle][inv_1eVZwghxruIa0YY4RtWg1viemwoyZ7Q6Ot][2024-03-19T10:19:02.707Z] DEBUG: Error: Payment call failed
... rest of trace ...
[restate] [Checkout/handle][inv_1eVZwghxruIa0YY4RtWg1viemwoyZ7Q6Ot][2024-03-19T10:19:02.707Z] DEBUG: Retrying in 20 ms
[restate] [Checkout/handle][inv_1eVZwghxruIa0YY4RtWg1viemwoyZ7Q6Ot][2024-03-19T10:19:02.707Z] DEBUG: Adding message to journal and sending to Restate ; SleepEntryMessage
[restate] [Checkout/handle][inv_1eVZwghxruIa0YY4RtWg1viemwoyZ7Q6Ot][2024-03-19T10:19:02.707Z] DEBUG: Scheduling suspension in 30000 ms
[restate] [Checkout/handle][inv_1eVZwghxruIa0YY4RtWg1viemwoyZ7Q6Ot][2024-03-19T10:19:02.730Z] DEBUG: Received completion message from Restate, adding to journal. ; CompletionMessage
Payment call succeeded for idempotency key 608e3977-7c91-4fc2-bf56-3c9c0c2cbabd and amount 40
[restate] [Checkout/handle][inv_1eVZwghxruIa0YY4RtWg1viemwoyZ7Q6Ot][2024-03-19T10:19:02.730Z] DEBUG: Adding message to journal and sending to Restate ; SideEffectEntryMessage
[restate] [Checkout/handle][inv_1eVZwghxruIa0YY4RtWg1viemwoyZ7Q6Ot][2024-03-19T10:19:02.730Z] DEBUG: Scheduling suspension in 30000 ms
[restate] [Checkout/handle][inv_1eVZwghxruIa0YY4RtWg1viemwoyZ7Q6Ot][2024-03-19T10:19:02.731Z] DEBUG: Received entry ack message from Restate, adding to journal. ; EntryAckMessage
Notifying user 123 of payment success
[restate] [Checkout/handle][inv_1eVZwghxruIa0YY4RtWg1viemwoyZ7Q6Ot][2024-03-19T10:19:02.731Z] DEBUG: Adding message to journal and sending to Restate ; SideEffectEntryMessage
[restate] [Checkout/handle][inv_1eVZwghxruIa0YY4RtWg1viemwoyZ7Q6Ot][2024-03-19T10:19:02.732Z] DEBUG: Scheduling suspension in 30000 ms
[restate] [Checkout/handle][inv_1eVZwghxruIa0YY4RtWg1viemwoyZ7Q6Ot][2024-03-19T10:19:02.732Z] DEBUG: Received entry ack message from Restate, adding to journal. ; EntryAckMessage
[restate] [Checkout/handle][inv_1eVZwghxruIa0YY4RtWg1viemwoyZ7Q6Ot][2024-03-19T10:19:02.733Z] DEBUG: Journaled and sent output message ; OutputStreamEntryMessage
[restate] [Checkout/handle][inv_1eVZwghxruIa0YY4RtWg1viemwoyZ7Q6Ot][2024-03-19T10:19:02.733Z] DEBUG: Function completed successfully.
tip

Have a look at the error handling documentation(for TypeScript or Java) to learn how to make a call fail terminally (without retries).

Tracing​

Restate exposes OpenTelemetry traces of your invocations.

Run the Jaeger container with:

docker run -d --name jaeger \
-e COLLECTOR_OTLP_ENABLED=true \
-p 4317:4317 \
-p 16686:16686 \
jaegertracing/all-in-one:1.46

Then stop the Restate container and start it again with the tracing endpoint defined as an environment variable:

docker run --name restate_dev --rm \
-e RESTATE_OBSERVABILITY__TRACING__ENDPOINT=http://host.docker.internal:4317 \
-p 8080:8080 -p 9070:9070 -p 9071:9071 \
--add-host=host.docker.internal:host-gateway \
docker.io/restatedev/restate:0.8

Register the services again (required because the state got wiped when the runtime was restarted with the --rm flag):

curl localhost:9070/deployments -H 'content-type: application/json' -d '{"uri": "http://host.docker.internal:9080"}'

Send requests to the runtime by adding tickets and checking out.

Go to the Jaeger UI at http://localhost:16686.

In the Jaeger UI, select the UserSession service from the service dropdown. You should see the addTicket and checkout requests listed:

Jaeger traces tour

Have a look at the traces of the checkout call:

Checkout call traces

You can see the calls that were done to Restate, for example invoke, sleep, one way call, get state, etc., and their timings. If you expand one of the traces, you can see tags describing some metadata of the context call, for example invocation id and call arguments.

For more information, have a look at the tracing docs.

🏁 The end​

You reached the end of this tutorial!

🚩 Have a look at the fully implemented app in part5, and run it with:
npm run part5

Let's recap what you've covered:

  • reliable, suspendable request-response calls
  • one-way calls without the need for queues
  • suspensions for external communication
  • durable timers for sleep or for calling other services
  • concurrency guarantees for keyed/unkeyed services
  • persistent application state
  • storing the results of non-deterministic operations or external calls as side effects
  • resiliency and retries
  • tracing with Jaeger

Next steps​