From 2862830a29516f7b186687068a4e212e1a9180ed Mon Sep 17 00:00:00 2001 From: slinkydeveloper Date: Tue, 7 Jul 2026 18:26:58 +0200 Subject: [PATCH] API followups to add generic calls/sends. * Java: Restate.call/Restate.send * Kotlin: prepareRequest --- MIGRATION.md | 61 +++++++++++++++++++ .../main/kotlin/dev/restate/sdk/kotlin/api.kt | 26 +++++++- .../main/java/dev/restate/sdk/Restate.java | 49 +++++++++++++++ .../restate/sdk/core/kotlinapi/CallTest.kt | 20 +++--- .../dev/restate/sdk/testservices/ProxyImpl.kt | 22 +++---- 5 files changed, 157 insertions(+), 21 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index 9acb68a2..8c527b15 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -32,6 +32,7 @@ one service at a time. | Code-generated clients (Service) | `Restate.service(Class)` / `Restate.serviceHandle(Class)` | | Code-generated clients (Virtual Object) | `Restate.virtualObject(Class, key)` / `Restate.virtualObjectHandle(Class, key)` | | Code-generated clients (Workflow) | `Restate.workflow(Class, key)` / `Restate.workflowHandle(Class, key)` | +| `ctx.call(request)` / `ctx.send(request, delay)` (raw `Request`) | `Restate.call(request)` / `Restate.send(request[, delay])` | From **outside** a handler (the ingress client), the equivalents live on `dev.restate.client.Client`: `client.service(Class)` / `client.serviceHandle(Class)` / `client.virtualObject(Class, key)` / etc. @@ -52,6 +53,7 @@ From **outside** a handler (the ingress client), the equivalents live on `dev.re | Code-generated clients (Service) | `service()` / `toService()` | | Code-generated clients (Virtual Object) | `virtualObject(key)` / `toVirtualObject(key)` | | Code-generated clients (Workflow) | `workflow(key)` / `toWorkflow(key)` | +| `ctx.call(request)` / `ctx.send(request, delay)` (raw `Request`) | `prepareRequest(request).call()` / `prepareRequest(request).send(delay)` | All the top-level functions are in the `dev.restate.sdk.kotlin` package — add `import dev.restate.sdk.kotlin.*`. @@ -143,6 +145,34 @@ int idempotentCount = Restate.virtualObjectHandle(Counter.class, "my-counter") .await(); ``` +### 5. Generic / dynamic-target invocation (advanced) + +The old codegen `Handlers` request builders produced a `Request` you passed to +`ctx.call(...)` / `ctx.send(...)`. Those builders are going away. When the target is only known at +runtime — or you otherwise need to build a `Request` by hand rather than through the typed proxies +above — use the generic `Restate.call` / `Restate.send` overloads: + +```java +// Before +GreetingResponse response = ctx.call(GreeterHandlers.greet(new Greeting("Alice"))).await(); + +// After — build the Request manually and pass it to Restate.call / Restate.send +Request request = + Request.of( + Target.service("Greeter", "greet"), + TypeTag.of(Greeting.class), + TypeTag.of(GreetingResponse.class), + new Greeting("Alice")) + .idempotencyKey("my-idempotency-key") + .build(); + +GreetingResponse response = Restate.call(request).await(); + +// Fire-and-forget, optionally with a delay +Restate.send(request); +Restate.send(request, Duration.ofMinutes(5)); +``` + --- ## Kotlin migration @@ -233,6 +263,37 @@ val idempotentCount = toVirtualObject("my-counter") .await() ``` +### 5. Generic / dynamic-target invocation (advanced) + +When the target is only known at runtime — or you otherwise need to build a `Request` by hand rather +than through the typed `toService` / `toVirtualObject` / `toWorkflow` builders — wrap the `Request` +with `prepareRequest(...)`, which exposes the same `options {}` / `call()` / `send()` DSL. This +replaces a raw `ctx.call(request)` / `ctx.send(request, delay)`: + +```kotlin +// Before +val response = ctx.call(request).await() + +// After — build the Request manually (e.g. via Request.of) and wrap it with prepareRequest +val request = + Request.of( + Target.service("Greeter", "greet"), + typeTag(), + typeTag(), + Greeting("Alice"), + ) + +val response = + prepareRequest(request) + .options { idempotencyKey = "my-idempotency-key" } + .call() + .await() + +// Fire-and-forget, optionally with a delay +prepareRequest(request).send() +prepareRequest(request).send(delay = 5.minutes) +``` + ### Kotlin gotcha: proxy clients need non-final classes The proxy clients (`service()`, `virtualObject(key)`, `toService()`, …) create a runtime proxy of diff --git a/sdk-api-kotlin/src/main/kotlin/dev/restate/sdk/kotlin/api.kt b/sdk-api-kotlin/src/main/kotlin/dev/restate/sdk/kotlin/api.kt index b46a1ce9..c9ce3773 100644 --- a/sdk-api-kotlin/src/main/kotlin/dev/restate/sdk/kotlin/api.kt +++ b/sdk-api-kotlin/src/main/kotlin/dev/restate/sdk/kotlin/api.kt @@ -1468,7 +1468,11 @@ private class KRequestImpl(private val request: Request) : val builder = InvocationOptions.builder() builder.block() return KRequestImpl( - this.toBuilder().headers(builder.headers).idempotencyKey(builder.idempotencyKey).build() + this.toBuilder() + .headers(builder.headers) + .idempotencyKey(builder.idempotencyKey) + .limitKey(builder.limitKey) + .build() ) } @@ -1481,6 +1485,26 @@ private class KRequestImpl(private val request: Request) : } } +/** + * Wrap a pre-built [Request] into an invocable [KRequest]. + * + * This is the low-level, generic invocation entrypoint. For the common case, prefer the type-safe + * [toService]/[toVirtualObject]/[toWorkflow] (and [service]/[virtualObject]/[workflow]) builders. + * + * ``` + * prepareRequest(Request.of(target, Serde.RAW, Serde.RAW, payload).build()) + * .options { idempotencyKey = "my-key" } + * .call() + * .await() + * ``` + * + * @param request the request describing the target, payload and response type. + * @return an invocable [KRequest]. + */ +fun prepareRequest(request: Request): KRequest { + return KRequestImpl(request) +} + /** * **PREVIEW:** Returns a [KScope] that routes all outgoing calls within the given scope. * diff --git a/sdk-api/src/main/java/dev/restate/sdk/Restate.java b/sdk-api/src/main/java/dev/restate/sdk/Restate.java index 06d45217..8135f189 100644 --- a/sdk-api/src/main/java/dev/restate/sdk/Restate.java +++ b/sdk-api/src/main/java/dev/restate/sdk/Restate.java @@ -443,6 +443,55 @@ public static DurableFuture timer(Duration duration) { return Context.current().timer(duration); } + /** + * Invoke another Restate service method, and await the response. + * + *

This is the low-level, generic invocation entrypoint accepting a pre-built {@link Request}. + * For the common case, prefer the type-safe methods {@link #service(Class)}, {@link + * #serviceHandle(Class)}, {@link #virtualObject(Class, String)}, {@link + * #virtualObjectHandle(Class, String)}, {@link #workflow(Class, String)} and {@link + * #workflowHandle(Class, String)}. + * + * @param request Request object describing the target, payload and response type. + * @return a {@link CallDurableFuture} that wraps the Restate service method result. + * @see #service(Class) + * @see #serviceHandle(Class) + */ + public static CallDurableFuture call(Request request) { + return Context.current().call(request); + } + + /** + * Invoke another Restate service without waiting for the response (fire-and-forget). + * + *

This is the low-level, generic invocation entrypoint accepting a pre-built {@link Request}. + * For the common case, prefer the type-safe {@code send(...)} methods on {@link + * #serviceHandle(Class)}, {@link #virtualObjectHandle(Class, String)} and {@link + * #workflowHandle(Class, String)}. + * + * @param request Request object describing the target, payload and response type. + * @return an {@link InvocationHandle} that can be used to retrieve the invocation id, cancel the + * invocation, or attach to its result. + * @see #send(Request, Duration) + * @see #serviceHandle(Class) + */ + public static InvocationHandle send(Request request) { + return Context.current().send(request); + } + + /** + * Like {@link #send(Request)}, but scheduling the invocation after the given {@code delay}. + * + * @param request Request object describing the target, payload and response type. + * @param delay the delay after which the request should be executed. + * @return an {@link InvocationHandle} that can be used to retrieve the invocation id, cancel the + * invocation, or attach to its result. + * @see #send(Request) + */ + public static InvocationHandle send(Request request, Duration delay) { + return Context.current().send(request, delay); + } + /** * Simple API to invoke a Restate service. * diff --git a/sdk-core/src/test/kotlin/dev/restate/sdk/core/kotlinapi/CallTest.kt b/sdk-core/src/test/kotlin/dev/restate/sdk/core/kotlinapi/CallTest.kt index 9b21532c..35c0b006 100644 --- a/sdk-core/src/test/kotlin/dev/restate/sdk/core/kotlinapi/CallTest.kt +++ b/sdk-core/src/test/kotlin/dev/restate/sdk/core/kotlinapi/CallTest.kt @@ -13,6 +13,7 @@ import dev.restate.common.Slice import dev.restate.common.Target import dev.restate.sdk.core.CallTestSuite import dev.restate.sdk.core.kotlinapi.KotlinAPITests.Companion.testDefinitionForService +import dev.restate.sdk.kotlin.prepareRequest import dev.restate.serde.Serde class CallTest : CallTestSuite() { @@ -23,18 +24,21 @@ class CallTest : CallTestSuite() { headers: Map, body: Slice, ) = - testDefinitionForService("OneWayCall") { ctx, _: Unit -> + testDefinitionForService("OneWayCall") { _, _: Unit -> val ignored = - ctx.send( - Request.of(target, Serde.SLICE, Serde.RAW, body) - .headers(headers) - .idempotencyKey(idempotencyKey) - ) + prepareRequest( + Request.of(target, Serde.SLICE, Serde.RAW, body) + .headers(headers) + .idempotencyKey(idempotencyKey) + ) + .send() } override fun implicitCancellation(target: Target, body: Slice) = - testDefinitionForService("ImplicitCancellation") { ctx, _: Unit -> + testDefinitionForService("ImplicitCancellation") { _, _: Unit -> val ignored = - ctx.call(Request.of(target, Serde.SLICE, Serde.RAW, body)).await() + prepareRequest(Request.of(target, Serde.SLICE, Serde.RAW, body)) + .call() + .await() } } diff --git a/test-services/src/main/kotlin/dev/restate/sdk/testservices/ProxyImpl.kt b/test-services/src/main/kotlin/dev/restate/sdk/testservices/ProxyImpl.kt index f9b01cd8..849325ad 100644 --- a/test-services/src/main/kotlin/dev/restate/sdk/testservices/ProxyImpl.kt +++ b/test-services/src/main/kotlin/dev/restate/sdk/testservices/ProxyImpl.kt @@ -28,8 +28,7 @@ class ProxyImpl : Proxy { } override suspend fun call(request: Proxy.ProxyRequest): ByteArray { - return context() - .call( + return prepareRequest( Request.of(request.toTarget(), Serde.RAW, Serde.RAW, request.message).also { if (request.idempotencyKey != null) { it.idempotencyKey = request.idempotencyKey @@ -39,12 +38,12 @@ class ProxyImpl : Proxy { } } ) + .call() .await() } override suspend fun oneWayCall(request: Proxy.ProxyRequest): String = - context() - .send( + prepareRequest( Request.of(request.toTarget(), Serde.RAW, Serde.SLICE, request.message).also { if (request.idempotencyKey != null) { it.idempotencyKey = request.idempotencyKey @@ -52,9 +51,9 @@ class ProxyImpl : Proxy { if (request.limitKey != null) { it.limitKey = request.limitKey } - }, - request.delayMillis?.milliseconds ?: Duration.ZERO, + } ) + .send(request.delayMillis?.milliseconds ?: Duration.ZERO) .invocationId() override suspend fun manyCalls(requests: List) { @@ -62,8 +61,7 @@ class ProxyImpl : Proxy { for (request in requests) { if (request.oneWayCall) { - context() - .send( + prepareRequest( Request.of( request.proxyRequest.toTarget(), Serde.RAW, @@ -77,13 +75,12 @@ class ProxyImpl : Proxy { if (request.proxyRequest.limitKey != null) { it.limitKey = request.proxyRequest.limitKey } - }, - request.proxyRequest.delayMillis?.milliseconds ?: Duration.ZERO, + } ) + .send(request.proxyRequest.delayMillis?.milliseconds ?: Duration.ZERO) } else { val fut = - context() - .call( + prepareRequest( Request.of( request.proxyRequest.toTarget(), Serde.RAW, @@ -99,6 +96,7 @@ class ProxyImpl : Proxy { } } ) + .call() if (request.awaitAtTheEnd) { toAwait.add(fut) }