Service Communication
A handler can call another handler and wait for the response (request-response), or it can send a message without waiting for the response.
Make sure you export the service definition of the service you want to call, as we did in the service example on the overview page:
export const MyService: typeof myService = { name: "MyService" };
Import this service definition in the service handler that wants to call it.
Request-response calls
Request-response calls are requests where the client waits for the response.
To call a Service:
const response = await ctx.serviceClient(MyService).myHandler("Hi");
To call a Virtual Object:
const response = await ctx.objectClient(MyVirtualObject, "Mary").myHandler("Hi");
To call a Workflow:
// Call the `run` handler of the workflow (only allowed once).await ctx.workflowClient(MyWorkflow, "my-workflow-id").run("Hi");// Call some other `interactWithWorkflow` handler of the workflow.await ctx.workflowClient(MyWorkflow, "my-workflow-id").interactWithWorkflow();
- Create a service client and supply the service definition of the service you want to call.
For Virtual Objects, you also need to supply the key of the Virtual Object you want to call, here
"Mary"
. For Workflows, you need to supply a workflow ID that is unique per workflow execution. - Specify the handler you want to call and supply the request.
- Await the call to retrieve the response.
Or with the generic clients, if you don't have access to the service definition or want to specify the service and handler with strings (e.g. to implement workflow interpreters):
const response = await ctx.genericCall({service: "MyVirtualObject",method: "myHandler",parameter: "Hi",key: "Mary", // drop this for Service callsinputSerde: restate.serde.json,outputSerde: restate.serde.json,});
These calls are proxied by Restate, and get logged in the journal. In case of failures, Restate takes care of retries.
Once the run
handler of the workflow has finished, the other handlers can still be called up to the retention time of the workflow, by default 24 hours.
This can be configured via the UI or Admin API per Workflow definition by setting workflow_completion_retention
.
Request-response calls to Virtual Objects can lead to deadlocks, in which the Virtual Object remains locked and can't process any more requests. Some example cases:
- Cross deadlock between Virtual Object A and B: A calls B, and B calls A, both using same keys.
- Cyclical deadlock: A calls B, and B calls C, and C calls A again.
In this situation, you can use the UI or CLI to unblock the Virtual Object manually by cancelling invocations.
Sending messages
Handlers can send messages (a.k.a. one-way calls, or fire-and-forget calls), as follows:
To a Service:
ctx.serviceSendClient(MyService).myHandler("Hi");
To a Virtual Object:
ctx.objectSendClient(MyVirtualObject, "Mary").myHandler("Hi");
To a Workflow:
// Call the `run` handler of the workflow (only works once).ctx.workflowSendClient(MyWorkflow, "wf-id").run("Hi");// Call some other `interactWithWorkflow` handler of the workflow.ctx.workflowSendClient(MyWorkflow, "wf-id").interactWithWorkflow();
Or with the generic clients, if you don't have access to the service definition or want to specify the service and handler with strings (e.g. to implement workflow interpreters):
ctx.genericSend({service: "MyService",method: "myHandler",parameter: "Hi",inputSerde: restate.serde.json,});
Without Restate, you would usually put a message queue in between the two services, to guarantee the message delivery. Restate eliminates the need for a message queue because Restate durably logs the request and makes sure it gets executed.
Delayed calls
A delayed call is a one-way call that gets executed after a specified delay.
To schedule a delayed call, send a message with a delay parameter, as follows:
To a Service:
ctx.serviceSendClient(MyService).myHandler("Hi", restate.rpc.sendOpts({ delay: 5000 }));
To a Virtual Object:
ctx.objectSendClient(MyVirtualObject, "Mary").myHandler("Hi", restate.rpc.sendOpts({ delay: 5000 }));
To a Workflow:
ctx.workflowSendClient(MyWorkflow, "Mary").run("Hi", restate.rpc.sendOpts({ delay: 5000 }));
Or with the generic clients, if you don't have access to the service definition or want to specify the service and handler with strings (e.g. to implement workflow interpreters):
ctx.genericSend({service: "MyService",method: "myHandler",parameter: "Hi",inputSerde: restate.serde.json,delay: 5000,});
You can also use this functionality to schedule async tasks. Restate will make sure the task gets executed at the desired time.
Invocations to a Virtual Object are executed serially. Invocations will execute in the same order in which they arrive at Restate. For example, assume a handler calls the same Virtual Object twice:
ctx.objectSendClient(MyVirtualObject, "Mary").myHandler("I'm call A");ctx.objectSendClient(MyVirtualObject, "Mary").myHandler("I'm call B");
It is guaranteed that call A will execute before call B. It is not guaranteed though that call B will be executed immediately after call A, as invocations coming from other handlers/sources, could interleave these two calls.
Using an idempotency key
While service-to-service communication provides exactly once semantics, it won't guarantee that two separate services, or a service and an external client, won't submit the same logical request twice.
To get this additional guarantee, you can provide an idempotency key on any request using restate.rpc.opts
/restate.rpc.sendOpts
:
// For a regular callconst response = await ctx.serviceClient(MyService).myHandler("Hi",restate.rpc.opts({idempotencyKey: "my-idempotency-key",}));// For a one way callctx.serviceSendClient(MyService).myHandler("Hi",restate.rpc.sendOpts({idempotencyKey: "my-idempotency-key",}));
Re-attach to an invocation
You can re-attach to any request using ctx.attach
:
const handle = ctx.serviceSendClient(MyService).myHandler("Hi",restate.rpc.sendOpts({// Optional: send attaching idempotency keyidempotencyKey: "my-idempotency-key",}));const invocationId = await handle.invocationId;// Later re-attach to the requestconst response = ctx.attach(invocationId);
Attaching to requests with an idempotency key, lets you wait for the request to finish and retrieve its response. Restate persists the response to these requests for the configured retention time.
For requests without an idempotency key, you will not be able to retrieve the result, only wait for them to complete.
Cancel an invocation
You can cancel an invocation using ctx.cancel
:
const handle = ctx.serviceSendClient(MyService).myHandler("Hi");const invocationId = await handle.invocationId;// I don't need this invocation anymore, let me just cancel itctx.cancel(invocationId);
Have a look at our sagas guide to learn more about cancellations and how to handle them.