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.
Request-response calls
Request-response calls are requests where the client waits for the response. The Java SDK generates a client for each service, which you can use to make these calls.
The clients are generated when you build the project. If you don't see the generated clients, make sure you have built the project with ./gradlew build
.
Use the generated client's of the SDK to do the call:
- Java
- Kotlin
// To a serviceString response = MyServiceClient.fromContext(ctx).myHandler(request).await();
// To a Virtual ObjectString response = MyVirtualObjectClient.fromContext(ctx, objectKey).myHandler(request).await();
// To a Workflow// Call the `run` handler of the workflowString response = MyWorkflowClient.fromContext(ctx, workflowId).run(request).await();// Calling some other `interactWithWorkflow` handler of the workflowMyWorkflowClient.fromContext(ctx, workflowId).interactWithWorkflow(request).await();
// To a serviceval response: String = MyServiceClient.fromContext(ctx).myHandler(request).await()
// To a Virtual Objectval response: String =MyVirtualObjectClient.fromContext(ctx, objectKey).myHandler(request).await()
// To a Workflow// Call the `run` handler of the workflowval response: String = MyWorkflowClient.fromContext(ctx, workflowId).run(request).await()// Call some other `interactWithWorkflow` handler of the workflow.MyWorkflowClient.fromContext(ctx, workflowId).interactWithWorkflow(request).await()
Or with the generic clients, if you don't have access to typed clients or want to specify the service and handler with strings (e.g. to implement workflow interpreters):
- Java
- Kotlin
Target target = Target.service("MyService", "myHandler"); // or virtualObject or workflowString response =ctx.call(Request.of(target, TypeTag.of(String.class), TypeTag.of(String.class), request)).await();
val target = Target.service("MyService", "myHandler")val response =ctx.call(Request.of(target, typeTag<String>(), typeTag<String>(), request)).await()
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 Admin API per Workflow definition by setting workflow_completion_retention
.
Request-response calls to Virtual Object can lead to deadlocks. When this happens, the Virtual Object remains locked and the system can't process any more requests. In this situation you'll have to unblock the Virtual Object manually by cancelling invocations. 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.
Sending messages
Handlers can send messages (a.k.a. one-way calls, or fire-and-forget calls).
Use the client's .send()
method to do this as follows:
- Java
- Kotlin
MyServiceClient.fromContext(ctx).send().myHandler(request);
MyServiceClient.fromContext(ctx).send().myHandler(request)
Or with the generic clients:
- Java
- Kotlin
Target target = Target.service("MyService", "myHandler"); // or virtualObject or workflowctx.send(Request.of(target, TypeTag.of(String.class), TypeTag.of(String.class), request));
val target = Target.service("MyService", "myHandler")ctx.send(Request.of(target, typeTag<String>(), typeTag<String>(), request))
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.
Use Restate's generated clients .send()
and the overload with Duration
as second parameter to send a delayed requests:
- Java
- Kotlin
MyServiceClient.fromContext(ctx).send().myHandler(request, Duration.ofDays(5));
MyServiceClient.fromContext(ctx).send().myHandler(request, 5.days)
Or with the generic clients:
- Java
- Kotlin
Target target = Target.service("MyService", "myHandler"); // or virtualObject or workflowctx.send(Request.of(target, TypeTag.of(String.class), TypeTag.of(String.class), request).asSendDelayed(Duration.ofDays(5)));
val target = Target.service("MyService", "myHandler")ctx.send(Request.of(target, typeTag<String>(), typeTag<String>(), request).asSendDelayed(5.days))
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 the following code in ServiceA
:
MyVirtualObjectClient.fromContext(ctx, objectKey).send().myHandler("I'm call A");MyVirtualObjectClient.fromContext(ctx, objectKey).send().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 decorating the Request
object:
- Java
- Kotlin
// For a regular callMyServiceClient.fromContext(ctx).myHandler(request, req -> req.idempotencyKey("my-idempotency-key"));// For a one way callMyServiceClient.fromContext(ctx).send().myHandler(request, req -> req.idempotencyKey("my-idempotency-key"));
// For a regular callMyServiceClient.fromContext(ctx).myHandler(request) { idempotencyKey = "my-idempotency-key" }// For a one way callMyServiceClient.fromContext(ctx).send().myHandler(request) {idempotencyKey = "my-idempotency-key"}
Re-attach to an invocation
You can re-attach to any request by using the InvocationHandle
API:
- Java
- Kotlin
var handle =MyServiceClient.fromContext(ctx).send().myHandler(request,// Optional: send attaching idempotency keyreq -> req.idempotencyKey("my-idempotency-key"));var response = handle.attach().await();
val handle =MyServiceClient.fromContext(ctx).send().myHandler(request) {idempotencyKey = "my-idempotency-key"}val response = handle.attach().await()
You can acquire an InvocationHandle
both from a just sent one way call, or from an invocation id.
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 by using the InvocationHandle
API:
- Java
- Kotlin
var handle = MyServiceClient.fromContext(ctx).send().myHandler(request);handle.cancel();
val handle = MyServiceClient.fromContext(ctx).send().myHandler(request)handle.cancel()
Have a look at our sagas guide to learn more about cancellations and how to handle them.