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.
To a Service:
response, err := restate.Service[string](ctx, "MyService", "MyHandler").Request("Hi")if err != nil {return err}
To a Virtual Object:
response, err := restate.Object[string](ctx, "MyVirtualObject", "Mary", "MyHandler").Request("Hi")if err != nil {return err}
To a Workflow:
// Call the `run` handler of the workflow (only works once).result, err := restate.Workflow[bool](ctx, "MyWorkflow", "my-workflow-id", "Run").Request("Hi")if err != nil {return err}// Call some other `GetStatus` handler of the workflow.status, err := restate.Workflow[restate.Void](ctx, "MyWorkflow", "my-workflow-id", "GetStatus").Request("Hi again")if err != nil {return err}
- Create a service client and supply the name of the service and method you want to call, as well
as a type parameter for the output type.
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, here"my-workflow-id"
. - Call Request() with the input data to make a synchronous request which will block on the response.
These calls are proxied by Restate, and get logged in the journal. In case of failures, Restate takes care of retries.
The Go SDK also supports defining handlers and input/output types using code generation from Protocol Buffers. This will also generate typed clients which can be used to safely call other methods. See Code Generation for more details.
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 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:
restate.ServiceSend(ctx, "MyService", "MyHandler").Send("Hi")
To a Virtual Object:
restate.ObjectSend(ctx, "MyService", "Mary", "MyHandler").Send("Hi")
To a Workflow:
// Call the `run` handler of the workflow (only works once).restate.WorkflowSend(ctx, "MyWorkflow", "my-workflow-id", "Run").Send("Hi")// Call some other `interactWithWorkflow` handler of the workflow.restate.WorkflowSend(ctx, "MyWorkflow", "my-workflow-id", "InteractWithWorkflow").Send("Hi again")
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:
restate.ServiceSend(ctx, "MyService", "MyHandler").Send("Hi", restate.WithDelay(5*time.Second))
To a Virtual Object:
restate.ObjectSend(ctx, "MyService", "Mary", "MyHandler").Send("Hi", restate.WithDelay(5*time.Second))
To a Workflow:
restate.WorkflowSend(ctx, "MyWorkflow", "my-workflow-id", "Run").Send("Hi", restate.WithDelay(5*time.Second))
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:
restate.ObjectSend(ctx, "MyService", "Mary", "MyHandler").Send("I'm call A")restate.ObjectSend(ctx, "MyService", "Mary", "MyHandler").Send("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.
In order to get this additional guarantee, you can provide an idempotency key on any request using WithIdempotencyKey
:
restate.ServiceSend(ctx, "MyService", "MyHandler").// Send attaching idempotency keySend("Hi", restate.WithIdempotencyKey("my-idempotency-key"))
Re-attach to an invocation
You can re-attach to any request using AttachInvocation
:
// Execute the request and retrieve the invocation idinvocationId := restate.ServiceSend(ctx, "MyService", "MyHandler").// Optional: send attaching idempotency keySend("Hi", restate.WithIdempotencyKey("my-idempotency-key")).GetInvocationId()// Later re-attach to the requestresponse, err := restate.AttachInvocation[string](ctx, invocationId).Response()
Attach will either wait for the in-flight invocation to complete, or in case of requests created with idempotency key, it will return its completion back when available. Thus we generally recommended to attach to invocations created with an idempotency key, because those invocations will persist their result after completion, up to the configured retention time.
Cancel an invocation
You can cancel an invocation using CancelInvocation
:
// Execute the request and retrieve the invocation idinvocationId := restate.ServiceSend(ctx, "MyService", "MyHandler").Send("Hi").GetInvocationId()// I don't need this invocation anymore, let me just cancel itrestate.CancelInvocation(ctx, invocationId)
Have a look at our sagas guide to learn more about cancellations and how to handle them.