Tour of Restate
- TypeScript
- Java
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 handlers. Handlers are functions that implement business logic. Restate manages their invocation and execution. Services communicate with one another using Remote Procedure Calls (RPC). Our ticket example consists of three services:
As we go, you will discover how Restate can help you with some intricacies in this application.
- TypeScript
- Java
- Latest stable version of NodeJS >= v18.17.1 and npm CLI >= 9.6.7 installed.
- Restate CLI: Installation instructions
- Optional: Docker Engine or Podman, if you want to run the Restate Server with Docker. And to run Jaeger.
This guide is written for:
- TypeScript SDK version:
1.0.0
- Restate runtime Docker image:
docker.io/restatedev/restate:1.0
- JDK >= 11
- Restate CLI: Installation instructions
- Optional: Docker Engine or Podman, if you want to run the Restate Server with Docker. And to run Jaeger.
This guide is written for:
- Java SDK version:
1.0.1
- Restate runtime Docker image:
docker.io/restatedev/restate:1.0
Getting Startedβ
Set up the services
- TypeScript
- Java
Download the example and run locally with an IDE:
Install the dependencies and build the app:
npm install && npm run build
Run the services
npm run app-dev
Run the services
./gradlew run
This GitHub repository contains the basic skeleton of the Java services that you develop in this tutorial.
Launch Restate
Restate is a single self-contained binary. No external dependencies needed. Check out our download page for other ways to run Restate.
Register the services with Restate
Now, we need to tell Restate where our services are running.
You can register services by calling the Restate Admin API (default port 9070
) and supplying it the service endpoint URI:
Output
If you run Restate with Docker, replace http://localhost:9080
by http://host.docker.internal:9080
.
All set up!
- TypeScript
- Java
In src/app
you will find the skeletons of the various services to help you start implementing the app.
For example:
Restate handlers have the Restate Context supplied as the first argument. This is the entrypoint to the SDK.
The app.ts
file contains the definition of the endpoint that hosts the services.
In src/main/java/dev/restate/tour/app
you will find the skeletons of the various services to help you start implementing the app.
For example:
Services are annotated by @Service
. A service consists of a set of handlers, and each handler is annotated by @Handler
.
Restate handlers have the Restate Context supplied as the first argument. This is the entrypoint to the SDK.
The AppMain.java
file contains the definition of the endpoint that hosts the services.
Invoking Handlersβ
Handlers can be invoked in several ways: via HTTP requests, programmatically with the SDK, or via Kafka events.
Request-response calls over HTTPβ
Let's start with invoking our handler over HTTP using curl
.
For example, add a ticket seat2B
to the cart of Mary by calling the addTicket
handler of the CartObject
:
curl localhost:8080/CartObject/Mary/addTicket -H 'content-type: application/json' -d '"seat2B"'
If this prints out true
, then you have a working setup.
When Mary wants to proceed with the purchase, call the checkout
handler of the CartObject
:
curl -X POST localhost:8080/CartObject/Mary/checkout
We will use these two curl
commands often when developing the code, so keep them handy.
Restate acts as a proxy for your services. It forwards the request to the correct service and handler. Therefore, the request is sent to Restate and not directly to the service.
Mary
?Handlers are either a part of plain services or Virtual Objects.
Virtual Objects are a special type of service that allows you to group handlers together, share state between them, and control concurrency.
Each Virtual Object has a unique key.
We will cover the difference in more detail later.
For now, it's important to note that when invoking a handler within a Virtual Object, you need to specify its key.
In our example, the CartObject
and TicketObject
are Virtual Objects, while the CheckoutService
is a plain service.
To add the ticket to Mary's cart, we need to specify the key Mary
in the path to reach her Virtual Object.
We can do the same programmatically within a handler by using the SDK. Let's try this out!
Request-response calls between handlersβ
You can also call other handlers programmatically by using the clients generated by the Restate SDK. Let's try this out!
When we add a ticket to the cart, the CartObject/addTicket
handler first needs to reserve the ticket for the user.
It does that by calling the TicketObject/reserve
handler:
- TypeScript
- Java
Service logs
[restate] [CartObject/addTicket][inv_1gdJBtdVEcM95mw1LLMMJY1Y0thJ9ugFGN][2024-03-18T16:30:28.790Z] DEBUG: Invoking function.[restate] [CartObject/addTicket][inv_1gdJBtdVEcM95mw1LLMMJY1Y0thJ9ugFGN][2024-03-18T16:30:28.792Z] DEBUG: Adding message to journal and sending to Restate ; InvokeEntryMessage[restate] [CartObject/addTicket][inv_1gdJBtdVEcM95mw1LLMMJY1Y0thJ9ugFGN][2024-03-18T16:30:28.792Z] DEBUG: Scheduling suspension in 30000 ms[restate] [TicketObject/reserve][inv_1k78Krj3GqrK6odGaRe866kHZilkVf1H4l][2024-03-18T16:30:28.796Z] DEBUG: Invoking function.[restate] [TicketObject/reserve][inv_1k78Krj3GqrK6odGaRe866kHZilkVf1H4l][2024-03-18T16:30:28.796Z] DEBUG: Journaled and sent output message ; OutputStreamEntryMessage[restate] [TicketObject/reserve][inv_1k78Krj3GqrK6odGaRe866kHZilkVf1H4l][2024-03-18T16:30:28.796Z] DEBUG: Function completed successfully.[restate] [CartObject/addTicket][inv_1gdJBtdVEcM95mw1LLMMJY1Y0thJ9ugFGN][2024-03-18T16:30:28.799Z] DEBUG: Received completion message from Restate, adding to journal. ; CompletionMessage[restate] [CartObject/addTicket][inv_1gdJBtdVEcM95mw1LLMMJY1Y0thJ9ugFGN][2024-03-18T16:30:28.800Z] DEBUG: Journaled and sent output message ; OutputStreamEntryMessage[restate] [CartObject/addTicket][inv_1gdJBtdVEcM95mw1LLMMJY1Y0thJ9ugFGN][2024-03-18T16:30:28.800Z] DEBUG: Function completed successfully.
- Create the client via
ctx.serviceClient
orctx.objectClient
(for Virtual Objects). Specify the service definition (TicketObject
) and optionally the Virtual Object key (ticketId
). - Specify the handler you want to call and supply the request. Here
reserve()
. - Await the response of the call.
Send a request to CartObject/addTicket
as we did previously, and have a look at the service logs.
Service logs
2024-04-16 17:18:59 DEBUG [CartObject/addTicket] dev.restate.sdk.http.vertx.RequestHttpServerHandler - Handling request to CartObject/addTicket2024-04-16 17:18:59 INFO [CartObject/addTicket] dev.restate.sdk.core.ResolvedEndpointHandlerImpl - Start processing invocation2024-04-16 17:19:00 DEBUG [CartObject/addTicket][inv_1aiqX0vFEFNH5LkrvugCbFBq1VKcNrjuzn] dev.restate.sdk.core.InvocationStateMachine - Transitioning state machine to REPLAYING2024-04-16 17:19:00 DEBUG [CartObject/addTicket][inv_1aiqX0vFEFNH5LkrvugCbFBq1VKcNrjuzn] dev.restate.sdk.core.InvocationStateMachine - Current journal entry [1](): InvokeEntryMessage2024-04-16 17:19:00 DEBUG [TicketObject/reserve] dev.restate.sdk.http.vertx.RequestHttpServerHandler - Handling request to TicketObject/reserve2024-04-16 17:19:00 INFO [TicketObject/reserve] dev.restate.sdk.core.ResolvedEndpointHandlerImpl - Start processing invocation2024-04-16 17:19:00 DEBUG [TicketObject/reserve][inv_1aAMfXkieWDz1fARH9n1r2H1YjjsTZxei5] dev.restate.sdk.core.InvocationStateMachine - Transitioning state machine to REPLAYING2024-04-16 17:19:00 DEBUG [TicketObject/reserve][inv_1aAMfXkieWDz1fARH9n1r2H1YjjsTZxei5] dev.restate.sdk.core.InvocationStateMachine - Current journal entry [1](): OutputEntryMessage2024-04-16 17:19:00 INFO [TicketObject/reserve][inv_1aAMfXkieWDz1fARH9n1r2H1YjjsTZxei5] dev.restate.sdk.core.InvocationStateMachine - End invocation2024-04-16 17:19:00 DEBUG [TicketObject/reserve][inv_1aAMfXkieWDz1fARH9n1r2H1YjjsTZxei5] dev.restate.sdk.core.InvocationStateMachine - Transitioning state machine to CLOSED2024-04-16 17:19:00 INFO [TicketObject/reserve][inv_1aAMfXkieWDz1fARH9n1r2H1YjjsTZxei5] dev.restate.sdk.core.InvocationStateMachine - End invocation2024-04-16 17:19:00 DEBUG [CartObject/addTicket][inv_1aiqX0vFEFNH5LkrvugCbFBq1VKcNrjuzn] dev.restate.sdk.core.InvocationStateMachine - Current journal entry [2](): OutputEntryMessage2024-04-16 17:19:00 INFO [CartObject/addTicket][inv_1aiqX0vFEFNH5LkrvugCbFBq1VKcNrjuzn] dev.restate.sdk.core.InvocationStateMachine - End invocation2024-04-16 17:19:00 DEBUG [CartObject/addTicket][inv_1aiqX0vFEFNH5LkrvugCbFBq1VKcNrjuzn] dev.restate.sdk.core.InvocationStateMachine - Transitioning state machine to CLOSED2024-04-16 17:19:00 INFO [CartObject/addTicket][inv_1aiqX0vFEFNH5LkrvugCbFBq1VKcNrjuzn] dev.restate.sdk.core.InvocationStateMachine - End invocation
- Use the pre-generated client (
TicketObject
): This gets generated when you compile the code for the first time. So if you haven't done that yet, run./gradlew build
to generate the client. - Supply the context (and specify the Virtual Object) key via
fromContext(ctx, ticketId)
. - Specify the handler you want to call and supply the request. Here
reserve()
. - Await the response of the call.
Once you have added this to the code, restart the service, call the CartObject/addTicket
method as we did previously, and have a look at the service logs.
Sending messages between handlersβ
We can also let handlers send messages to other handlers without waiting for a response.
In the example, when a seat gets added to the shopping cart, it gets reserved for 15 minutes.
When a user didn't proceed with the payment before the timeout, the CartObject/expireTicket
handler is triggered.
Let the expireTicket
handler call the TicketObject/unreserve
handler.
- TypeScript
- Java
Specify that you want to call the TicketObject
by supplying ticketObjectApi
to the send
function.
Then call the unreserve
handler on the TicketObject
.
Once you have added this to the code, call the CartObject/expireTicket
handler:
curl localhost:8080/CartObject/Mary/expireTicket -H 'content-type: application/json' -d '"seat2B"'
Service logs
[restate] [CartObject/expireTicket][inv_1gdJBtdVEcM942bjcDmb1c1khoaJe11Hbz][2024-03-18T16:14:24.671Z] DEBUG: Invoking function.[restate] [CartObject/expireTicket][inv_1gdJBtdVEcM942bjcDmb1c1khoaJe11Hbz][2024-03-18T16:14:24.672Z] DEBUG: Adding message to journal and sending to Restate ; BackgroundInvokeEntryMessage[restate] [CartObject/expireTicket][inv_1gdJBtdVEcM942bjcDmb1c1khoaJe11Hbz][2024-03-18T16:14:24.673Z] DEBUG: Journaled and sent output message ; OutputStreamEntryMessage[restate] [CartObject/expireTicket][inv_1gdJBtdVEcM942bjcDmb1c1khoaJe11Hbz][2024-03-18T16:14:24.673Z] DEBUG: Function completed successfully.[restate] [TicketObject/unreserve][inv_1k78Krj3GqrK3GuJXkgaXBbg69R47TCeAN][2024-03-18T16:14:24.677Z] DEBUG: Invoking function.[restate] [TicketObject/unreserve][inv_1k78Krj3GqrK3GuJXkgaXBbg69R47TCeAN][2024-03-18T16:14:24.677Z] DEBUG: Journaled and sent output message ; OutputStreamEntryMessage[restate] [TicketObject/unreserve][inv_1k78Krj3GqrK3GuJXkgaXBbg69R47TCeAN][2024-03-18T16:14:24.677Z] DEBUG: Function completed successfully.
You now call send()
on the generated client to send a message instead of doing a request-response call.
You also don't need to await the response, as you don't expect one.
Call the handler via:
curl localhost:8080/CartObject/Mary/expireTicket-H 'content-type: application/json' -d '"seat2B"'
Service logs
2024-04-16 17:27:45 DEBUG [CartObject/expireTicket] dev.restate.sdk.http.vertx.RequestHttpServerHandler - Handling request to CartObject/expireTicket2024-04-16 17:27:45 INFO [CartObject/expireTicket] dev.restate.sdk.core.ResolvedEndpointHandlerImpl - Start processing invocation2024-04-16 17:27:45 DEBUG [CartObject/expireTicket][inv_1aiqX0vFEFNH0T0mRlvCk7xTVSB5xQIaKR] dev.restate.sdk.core.InvocationStateMachine - Transitioning state machine to REPLAYING2024-04-16 17:27:45 DEBUG [CartObject/expireTicket][inv_1aiqX0vFEFNH0T0mRlvCk7xTVSB5xQIaKR] dev.restate.sdk.core.InvocationStateMachine - Current journal entry [1](): BackgroundInvokeEntryMessage2024-04-16 17:27:45 DEBUG [CartObject/expireTicket][inv_1aiqX0vFEFNH0T0mRlvCk7xTVSB5xQIaKR] dev.restate.sdk.core.InvocationStateMachine - Current journal entry [2](): OutputEntryMessage2024-04-16 17:27:45 INFO [CartObject/expireTicket][inv_1aiqX0vFEFNH0T0mRlvCk7xTVSB5xQIaKR] dev.restate.sdk.core.InvocationStateMachine - End invocation2024-04-16 17:27:45 DEBUG [CartObject/expireTicket][inv_1aiqX0vFEFNH0T0mRlvCk7xTVSB5xQIaKR] dev.restate.sdk.core.InvocationStateMachine - Transitioning state machine to CLOSED2024-04-16 17:27:45 INFO [CartObject/expireTicket][inv_1aiqX0vFEFNH0T0mRlvCk7xTVSB5xQIaKR] dev.restate.sdk.core.InvocationStateMachine - End invocation2024-04-16 17:27:45 DEBUG [TicketObject/unreserve] dev.restate.sdk.http.vertx.RequestHttpServerHandler - Handling request to TicketObject/unreserve2024-04-16 17:27:45 INFO [TicketObject/unreserve] dev.restate.sdk.core.ResolvedEndpointHandlerImpl - Start processing invocation2024-04-16 17:27:45 DEBUG [TicketObject/unreserve][inv_1aAMfXkieWDz6IcQAkXhOPoZ3T9A9KTC3D] dev.restate.sdk.core.InvocationStateMachine - Transitioning state machine to REPLAYING2024-04-16 17:27:45 DEBUG [TicketObject/unreserve][inv_1aAMfXkieWDz6IcQAkXhOPoZ3T9A9KTC3D] dev.restate.sdk.core.InvocationStateMachine - Current journal entry [1](): OutputEntryMessage2024-04-16 17:27:45 INFO [TicketObject/unreserve][inv_1aAMfXkieWDz6IcQAkXhOPoZ3T9A9KTC3D] dev.restate.sdk.core.InvocationStateMachine - End invocation2024-04-16 17:27:45 DEBUG [TicketObject/unreserve][inv_1aAMfXkieWDz6IcQAkXhOPoZ3T9A9KTC3D] dev.restate.sdk.core.InvocationStateMachine - Transitioning state machine to CLOSED2024-04-16 17:27:45 INFO [TicketObject/unreserve][inv_1aAMfXkieWDz6IcQAkXhOPoZ3T9A9KTC3D] dev.restate.sdk.core.InvocationStateMachine - End invocation
The service logs show how the expireTicket
handler gets executed and then the unreserve
handler.
The call to expireTicket
finishes earlier than the unreserve
handler because expireTicket
didn't wait for the response of the unreserve
handler.
Restate persists and retries failed one-way invocations. There is no need to set up message queues to ensure delivery!
To send messages via curl
, add /send
to the handler path:
curl localhost:8080/CartObject/Mary/addTicket/send -H 'content-type: application/json' -d '"seat2B"'
Output
{"invocationId":"inv_1aiqX0vFEFNH1Umgre58JiCLgHfTtztYK5","status":"Accepted"}
This returns the invocation ID. This is a unique identifier for the invocation. You can use it to track the progress of the invocation via the CLI, and to correlate logs and metrics.
π Try it outβ
Make the CartObject/checkout
handler call the CheckoutService/handle
handler.
For the request field, you can use a hard-coded string array for now: ["seat2B"]
.
You will fix this later on. Note that the CheckoutService
is not a Virtual Object, so you don't need to specify a key.
Solution
Add the following code to the CartObject/checkout
handler:
- TypeScript
- Java
Call CartObject/checkout
as you did earlier and have a look at the logs again to see what happened:
[restate] [CartObject/checkout][inv_1gdJBtdVEcM919dOBhoVBm3fUMlaIHnANr][2024-03-19T07:57:24.018Z] DEBUG: Invoking function.[restate] [CartObject/checkout][inv_1gdJBtdVEcM919dOBhoVBm3fUMlaIHnANr][2024-03-19T07:57:24.019Z] DEBUG: Adding message to journal and sending to Restate ; InvokeEntryMessage[restate] [CartObject/checkout][inv_1gdJBtdVEcM919dOBhoVBm3fUMlaIHnANr][2024-03-19T07:57:24.020Z] DEBUG: Scheduling suspension in 30000 ms[restate] [CheckoutService/handle][inv_16WnWCiCVV5G2gUUevDM4uIli4v7TN9k2d][2024-03-19T07:57:24.023Z] DEBUG: Invoking function.[restate] [CheckoutService/handle][inv_16WnWCiCVV5G2gUUevDM4uIli4v7TN9k2d][2024-03-19T07:57:24.024Z] DEBUG: Journaled and sent output message ; OutputStreamEntryMessage[restate] [CheckoutService/handle][inv_16WnWCiCVV5G2gUUevDM4uIli4v7TN9k2d][2024-03-19T07:57:24.024Z] DEBUG: Function completed successfully.[restate] [CartObject/checkout][inv_1gdJBtdVEcM919dOBhoVBm3fUMlaIHnANr][2024-03-19T07:57:24.026Z] DEBUG: Received completion message from Restate, adding to journal. ; CompletionMessage[restate] [CartObject/checkout][inv_1gdJBtdVEcM919dOBhoVBm3fUMlaIHnANr][2024-03-19T07:57:24.027Z] DEBUG: Journaled and sent output message ; OutputStreamEntryMessage[restate] [CartObject/checkout][inv_1gdJBtdVEcM919dOBhoVBm3fUMlaIHnANr][2024-03-19T07:57:24.027Z] DEBUG: Function completed successfully.
Restart the service and call the CartObject/checkout
handler as you did earlier and have a look at the logs again to see what happened.
2024-04-16 17:32:10 DEBUG [CartObject/checkout] dev.restate.sdk.http.vertx.RequestHttpServerHandler - Handling request to CartObject/checkout2024-04-16 17:32:10 INFO [CartObject/checkout] dev.restate.sdk.core.ResolvedEndpointHandlerImpl - Start processing invocation2024-04-16 17:32:10 DEBUG [CartObject/checkout][inv_1aiqX0vFEFNH7u1kZjvyH4KJpuO9j4njCp] dev.restate.sdk.core.InvocationStateMachine - Transitioning state machine to REPLAYING2024-04-16 17:32:10 DEBUG [CartObject/checkout][inv_1aiqX0vFEFNH7u1kZjvyH4KJpuO9j4njCp] dev.restate.sdk.core.InvocationStateMachine - Current journal entry [1](): InvokeEntryMessage2024-04-16 17:32:10 DEBUG [CheckoutService/handle] dev.restate.sdk.http.vertx.RequestHttpServerHandler - Handling request to CheckoutService/handle2024-04-16 17:32:10 INFO [CheckoutService/handle] dev.restate.sdk.core.ResolvedEndpointHandlerImpl - Start processing invocation2024-04-16 17:32:10 DEBUG [CheckoutService/handle][inv_12rkfeAcppNY3cI4F6DBZK8fir8uaIrIBP] dev.restate.sdk.core.InvocationStateMachine - Transitioning state machine to REPLAYING2024-04-16 17:32:11 DEBUG [CheckoutService/handle][inv_12rkfeAcppNY3cI4F6DBZK8fir8uaIrIBP] dev.restate.sdk.core.InvocationStateMachine - Current journal entry [1](): OutputEntryMessage2024-04-16 17:32:11 INFO [CheckoutService/handle][inv_12rkfeAcppNY3cI4F6DBZK8fir8uaIrIBP] dev.restate.sdk.core.InvocationStateMachine - End invocation2024-04-16 17:32:11 DEBUG [CheckoutService/handle][inv_12rkfeAcppNY3cI4F6DBZK8fir8uaIrIBP] dev.restate.sdk.core.InvocationStateMachine - Transitioning state machine to CLOSED2024-04-16 17:32:11 INFO [CheckoutService/handle][inv_12rkfeAcppNY3cI4F6DBZK8fir8uaIrIBP] dev.restate.sdk.core.InvocationStateMachine - End invocation2024-04-16 17:32:11 DEBUG [CartObject/checkout][inv_1aiqX0vFEFNH7u1kZjvyH4KJpuO9j4njCp] dev.restate.sdk.core.InvocationStateMachine - Current journal entry [2](): OutputEntryMessage2024-04-16 17:32:11 INFO [CartObject/checkout][inv_1aiqX0vFEFNH7u1kZjvyH4KJpuO9j4njCp] dev.restate.sdk.core.InvocationStateMachine - End invocation2024-04-16 17:32:11 DEBUG [CartObject/checkout][inv_1aiqX0vFEFNH7u1kZjvyH4KJpuO9j4njCp] dev.restate.sdk.core.InvocationStateMachine - Transitioning state machine to CLOSED2024-04-16 17:32:11 INFO [CartObject/checkout][inv_1aiqX0vFEFNH7u1kZjvyH4KJpuO9j4njCp] dev.restate.sdk.core.InvocationStateMachine - End invocation
Durable Executionβ
The calls we just did seem like regular RPC calls as you might know them from other service frameworks. But under the hood a lot more is happening.
Restate makes RPC calls resilient by letting the Restate Server and SDK cooperate. Restate tracks the execution of code in a journal and can replay it in case of a failure. This is called Durable Execution.
Have a look at the animation to understand what happened under-the-hood:
Journals
addTicket ( Mary, seat2B )
reserve ( seat2B )
This animation shows you what happened under the hood when we did the reserve call from the `CartObject` to the `TicketObject`. The animation uses the TypeScript SDK.
async function addTicket(ctx, ticketId){
const success = await ctx
.objectClient(ticketObject)
.reserve(ticketId);
return success;
}
Journal:
{ seat2B }
{ success }
{ success }
async function reserve(ctx, ticketId){
...
return success;
}
Journal:
{ success }
Whenever a failure would happen, Restate would be able to recover the latest state of the handler by sending over the journal. The code would fast-forward to the point where it crashed, and continue executing from there on.
To see the recovery of partial progress in practice, let's make the CartObject/addTicket
handler crash right after the call.
- TypeScript
- Java
Add the following code to line 4 of the snippet, to let the code throw an error after the call:
throw new Error("Failing");
Service logs
[restate] [CartObject/addTicket][inv_1aiqX0vFEFNH0TF1pLRFBDFosQCCTAN1M5][2024-04-16T13:28:20.245Z] DEBUG: Adding message to journal and sending to Restate ; InvokeEntryMessage[restate] [CartObject/addTicket][inv_1aiqX0vFEFNH0TF1pLRFBDFosQCCTAN1M5][2024-04-16T13:28:20.246Z] DEBUG: Scheduling suspension in 30000 ms[restate] [TicketObject/reserve][inv_1iGFK6hGrtOf3jcD8PupmCOJz1SDzvfPi1][2024-04-16T13:28:20.296Z] DEBUG: Invoking function.[restate] [TicketObject/reserve][inv_1iGFK6hGrtOf3jcD8PupmCOJz1SDzvfPi1][2024-04-16T13:28:20.296Z] DEBUG: Journaled and sent output message ; OutputEntryMessage[restate] [TicketObject/reserve][inv_1iGFK6hGrtOf3jcD8PupmCOJz1SDzvfPi1][2024-04-16T13:28:20.296Z] DEBUG: Function completed successfully.[restate] [CartObject/addTicket][inv_1aiqX0vFEFNH0TF1pLRFBDFosQCCTAN1M5][2024-04-16T13:28:20.362Z] DEBUG: Received completion message from Restate, adding to journal.Trace: [restate] [CartObject/addTicket][inv_1aiqX0vFEFNH0TF1pLRFBDFosQCCTAN1M5][2024-04-16T13:28:20.363Z] TRACE: Function completed with an error: Failing Error: Failing... rest of trace ...[restate] [CartObject/addTicket][inv_1aiqX0vFEFNH0TF1pLRFBDFosQCCTAN1M5][2024-04-16T13:28:20.372Z] DEBUG: Invocation ended with retryable error. ; ErrorMessage[restate] [CartObject/addTicket][inv_1aiqX0vFEFNH0TF1pLRFBDFosQCCTAN1M5][2024-04-16T13:28:20.437Z] DEBUG: Resuming (replaying) function.[restate] [CartObject/addTicket][inv_1aiqX0vFEFNH0TF1pLRFBDFosQCCTAN1M5][2024-04-16T13:28:20.437Z] DEBUG: Matched and replayed message from journal ; InvokeEntryMessageTrace: [restate] [CartObject/addTicket][inv_1aiqX0vFEFNH0TF1pLRFBDFosQCCTAN1M5][2024-04-16T13:28:20.437Z] TRACE: Function completed with an error: Failing Error: Failing... rest of trace ...
Instead of returning true, let the code fail after the call:
throw new IllegalStateException("The handler failed");
Service logs
2024-04-16 17:33:59 DEBUG [CartObject/addTicket] dev.restate.sdk.http.vertx.RequestHttpServerHandler - Handling request to CartObject/addTicket2024-04-16 17:33:59 INFO [CartObject/addTicket] dev.restate.sdk.core.ResolvedEndpointHandlerImpl - Start processing invocation2024-04-16 17:33:59 DEBUG [CartObject/addTicket][inv_1aiqX0vFEFNH5uLBb8M6CjbRkVUcVScH1T] dev.restate.sdk.core.InvocationStateMachine - Transitioning state machine to REPLAYING2024-04-16 17:33:59 DEBUG [CartObject/addTicket][inv_1aiqX0vFEFNH5uLBb8M6CjbRkVUcVScH1T] dev.restate.sdk.core.InvocationStateMachine - Current journal entry [1](): InvokeEntryMessage2024-04-16 17:33:59 DEBUG [TicketObject/reserve] dev.restate.sdk.http.vertx.RequestHttpServerHandler - Handling request to TicketObject/reserve2024-04-16 17:33:59 INFO [TicketObject/reserve] dev.restate.sdk.core.ResolvedEndpointHandlerImpl - Start processing invocation2024-04-16 17:33:59 DEBUG [TicketObject/reserve][inv_1aAMfXkieWDz6Dn3DPBWPXOWCarIhmgCSl] dev.restate.sdk.core.InvocationStateMachine - Transitioning state machine to REPLAYING2024-04-16 17:33:59 DEBUG [TicketObject/reserve][inv_1aAMfXkieWDz6Dn3DPBWPXOWCarIhmgCSl] dev.restate.sdk.core.InvocationStateMachine - Current journal entry [1](): OutputEntryMessage2024-04-16 17:33:59 INFO [TicketObject/reserve][inv_1aAMfXkieWDz6Dn3DPBWPXOWCarIhmgCSl] dev.restate.sdk.core.InvocationStateMachine - End invocation2024-04-16 17:33:59 DEBUG [TicketObject/reserve][inv_1aAMfXkieWDz6Dn3DPBWPXOWCarIhmgCSl] dev.restate.sdk.core.InvocationStateMachine - Transitioning state machine to CLOSED2024-04-16 17:33:59 INFO [TicketObject/reserve][inv_1aAMfXkieWDz6Dn3DPBWPXOWCarIhmgCSl] dev.restate.sdk.core.InvocationStateMachine - End invocation2024-04-16 17:33:59 WARN [CartObject/addTicket][inv_1aiqX0vFEFNH5uLBb8M6CjbRkVUcVScH1T] dev.restate.sdk.core.ResolvedEndpointHandlerImpl - Error when processing the invocationjava.lang.IllegalStateException: The handler failed... rest of trace ...2024-04-16 17:33:59 WARN [CartObject/addTicket][inv_1aiqX0vFEFNH5uLBb8M6CjbRkVUcVScH1T] dev.restate.sdk.core.InvocationStateMachine - Invocation failedjava.lang.IllegalStateException: The handler failed... rest of trace ...2024-04-16 17:33:59 DEBUG [CartObject/addTicket][inv_1aiqX0vFEFNH5uLBb8M6CjbRkVUcVScH1T] dev.restate.sdk.core.InvocationStateMachine - Transitioning state machine to CLOSED2024-04-16 17:33:59 INFO [CartObject/addTicket][inv_1aiqX0vFEFNH5uLBb8M6CjbRkVUcVScH1T] dev.restate.sdk.core.InvocationStateMachine - End invocation2024-04-16 17:33:59 DEBUG [CartObject/addTicket] dev.restate.sdk.http.vertx.RequestHttpServerHandler - Handling request to CartObject/addTicket2024-04-16 17:33:59 INFO [CartObject/addTicket] dev.restate.sdk.core.ResolvedEndpointHandlerImpl - Start processing invocation2024-04-16 17:33:59 DEBUG [CartObject/addTicket][inv_1aiqX0vFEFNH5uLBb8M6CjbRkVUcVScH1T] dev.restate.sdk.core.InvocationStateMachine - Transitioning state machine to REPLAYING2024-04-16 17:33:59 WARN [CartObject/addTicket][inv_1aiqX0vFEFNH5uLBb8M6CjbRkVUcVScH1T] dev.restate.sdk.core.ResolvedEndpointHandlerImpl - Error when processing the invocationjava.lang.IllegalStateException: The handler failed... rest of trace ...2024-04-16 17:33:59 WARN [CartObject/addTicket][inv_1aiqX0vFEFNH5uLBb8M6CjbRkVUcVScH1T] dev.restate.sdk.core.InvocationStateMachine - Invocation failedjava.lang.IllegalStateException: The handler failed... rest of trace ...
Call CartObject/addTicket
again and have a look at the service logs.
You see the retries taking place. And you see that only the first time the call to the CheckoutService
was made.
The other times, the call was skipped and the journaled response was replayed.
By default, Restate will keep retrying failed invocations until they succeed. If you want to cancel an invocation in a retry loop, you can use the CLI to do this. Let's have a look at that next.
Debugging with the CLIβ
Now that we have a failing invocation, let's take the opportunity to show you how you can get more information about what is going on with the CLI. The CLI is a management tool that lets you interact with the Restate Server. You can use it to boostrap a new project, but also to get information about the services and invocations.
Have a look at some useful commands and try them out yourself:
Remove the throwing of the exception from your code before you continue.
π© Explore the intermediate solution in part1
, and run it with:
- TypeScript
- Java
npm run part1
./gradlew -PmainClass=dev.restate.tour.part1.AppMain run
Scheduling Async Tasksβ
When a handler calls another handler, Restate registers the call and makes sure it happens. You can also ask Restate to execute the call at a later point in the future, by adding a delay parameter to the call. Restate then registers the call and triggers it after the delay has passed.
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.
Let the CartObject/addTicket
handler call the CartObject/expireTicket
handler with a delay of 15 minutes:
- TypeScript
- Java
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 CartObject/expireTicket
is executed 5 seconds later.
Service logs
- TypeScript
- Java
... logs from reserve call ...[restate] [CartObject/addTicket][inv_1gdJBtdVEcM90xbqbDEnOzNgilf2WmjZTP][2024-03-19T08:49:43.081Z] DEBUG: Received completion message from Restate, adding to journal. ; CompletionMessage[restate] [CartObject/addTicket][inv_1gdJBtdVEcM90xbqbDEnOzNgilf2WmjZTP][2024-03-19T08:49:43.081Z] DEBUG: Adding message to journal and sending to Restate ; BackgroundInvokeEntryMessage[restate] [CartObject/addTicket][inv_1gdJBtdVEcM90xbqbDEnOzNgilf2WmjZTP][2024-03-19T08:49:43.081Z] DEBUG: Journaled and sent output message ; OutputStreamEntryMessage[restate] [CartObject/addTicket][inv_1gdJBtdVEcM90xbqbDEnOzNgilf2WmjZTP][2024-03-19T08:49:43.081Z] DEBUG: Function completed successfully.[restate] [CartObject/expireTicket][inv_1gdJBtdVEcM93r8tce9IfwnbiAsk8lCevD][2024-03-19T08:49:48.092Z] DEBUG: Invoking function.[restate] [CartObject/expireTicket][inv_1gdJBtdVEcM93r8tce9IfwnbiAsk8lCevD][2024-03-19T08:49:48.093Z] DEBUG: Adding message to journal and sending to Restate ; BackgroundInvokeEntryMessage[restate] [CartObject/expireTicket][inv_1gdJBtdVEcM93r8tce9IfwnbiAsk8lCevD][2024-03-19T08:49:48.093Z] DEBUG: Journaled and sent output message ; OutputStreamEntryMessage[restate] [CartObject/expireTicket][inv_1gdJBtdVEcM93r8tce9IfwnbiAsk8lCevD][2024-03-19T08:49:48.093Z] DEBUG: Function completed successfully.[restate] [TicketObject/unreserve][inv_1k78Krj3GqrK529L4BRmz8ntFtiw2DkahH][2024-03-19T08:49:48.141Z] DEBUG: Invoking function.[restate] [TicketObject/unreserve][inv_1k78Krj3GqrK529L4BRmz8ntFtiw2DkahH][2024-03-19T08:49:48.141Z] DEBUG: Journaled and sent output message ; OutputStreamEntryMessage[restate] [TicketObject/unreserve][inv_1k78Krj3GqrK529L4BRmz8ntFtiw2DkahH][2024-03-19T08:49:48.141Z] DEBUG: Function completed successfully.
... logs from reserve call ...2024-04-17 08:08:10 DEBUG [CartObject/addTicket][inv_1aiqX0vFEFNH3fRqvARAGmeIcbyLXImG3L] dev.restate.sdk.core.InvocationStateMachine - Transitioning state machine to CLOSED2024-04-17 08:08:10 INFO [CartObject/addTicket][inv_1aiqX0vFEFNH3fRqvARAGmeIcbyLXImG3L] dev.restate.sdk.core.InvocationStateMachine - End invocation2024-04-17 08:08:15 DEBUG [CartObject/expireTicket] dev.restate.sdk.http.vertx.RequestHttpServerHandler - Handling request to CartObject/expireTicket2024-04-17 08:08:15 INFO [CartObject/expireTicket] dev.restate.sdk.core.ResolvedEndpointHandlerImpl - Start processing invocation2024-04-17 08:08:15 DEBUG [CartObject/expireTicket][inv_1aiqX0vFEFNH5R28lg9wg1c3CtOJOhHEM9] dev.restate.sdk.core.InvocationStateMachine - Transitioning state machine to REPLAYING2024-04-17 08:08:15 DEBUG [CartObject/expireTicket][inv_1aiqX0vFEFNH5R28lg9wg1c3CtOJOhHEM9] dev.restate.sdk.core.InvocationStateMachine - Current journal entry [1](): BackgroundInvokeEntryMessage2024-04-17 08:08:15 DEBUG [CartObject/expireTicket][inv_1aiqX0vFEFNH5R28lg9wg1c3CtOJOhHEM9] dev.restate.sdk.core.InvocationStateMachine - Current journal entry [2](): OutputEntryMessage2024-04-17 08:08:15 INFO [CartObject/expireTicket][inv_1aiqX0vFEFNH5R28lg9wg1c3CtOJOhHEM9] dev.restate.sdk.core.InvocationStateMachine - End invocation2024-04-17 08:08:15 DEBUG [CartObject/expireTicket][inv_1aiqX0vFEFNH5R28lg9wg1c3CtOJOhHEM9] dev.restate.sdk.core.InvocationStateMachine - Transitioning state machine to CLOSED2024-04-17 08:08:15 INFO [CartObject/expireTicket][inv_1aiqX0vFEFNH5R28lg9wg1c3CtOJOhHEM9] dev.restate.sdk.core.InvocationStateMachine - End invocation2024-04-17 08:08:15 DEBUG [TicketObject/unreserve] dev.restate.sdk.http.vertx.RequestHttpServerHandler - Handling request to TicketObject/unreserve2024-04-17 08:08:15 INFO [TicketObject/unreserve] dev.restate.sdk.core.ResolvedEndpointHandlerImpl - Start processing invocation2024-04-17 08:08:15 DEBUG [TicketObject/unreserve][inv_1aAMfXkieWDz0btTCuaF2NHgJEdX2tXHCF] dev.restate.sdk.core.InvocationStateMachine - Transitioning state machine to REPLAYING2024-04-17 08:08:15 DEBUG [TicketObject/unreserve][inv_1aAMfXkieWDz0btTCuaF2NHgJEdX2tXHCF] dev.restate.sdk.core.InvocationStateMachine - Current journal entry [1](): OutputEntryMessage2024-04-17 08:08:15 INFO [TicketObject/unreserve][inv_1aAMfXkieWDz0btTCuaF2NHgJEdX2tXHCF] dev.restate.sdk.core.InvocationStateMachine - End invocation2024-04-17 08:08:15 DEBUG [TicketObject/unreserve][inv_1aAMfXkieWDz0btTCuaF2NHgJEdX2tXHCF] dev.restate.sdk.core.InvocationStateMachine - Transitioning state machine to CLOSED2024-04-17 08:08:15 INFO [TicketObject/unreserve][inv_1aAMfXkieWDz0btTCuaF2NHgJEdX2tXHCF] dev.restate.sdk.core.InvocationStateMachine - End invocation
Don't forget to set the delay back to 15 minutes.
Durable timers are a powerful feature that can be used to implement workflows, schedule async tasks, or plan background jobs. Restate makes them resilient to failures and ensures that they get executed. No extra infrastructure needed!
Another timer-like feature of the SDK is suspendable sleep. Restate will make sure that the function gets resumed after the specified duration has passed. When running on function-as-a-service platforms, your function can suspend in the meantime, so you don't pay for the wait time.
- TypeScript
- Java
await ctx.sleep(15 * 60 * 1000);
ctx.sleep(Duration.ofMinutes(15));