Skip to content

Commit 492fd9d

Browse files
API followups to add generic calls/sends. (#634)
* Java: Restate.call/Restate.send * Kotlin: prepareRequest
1 parent 6a554f7 commit 492fd9d

5 files changed

Lines changed: 157 additions & 21 deletions

File tree

MIGRATION.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ one service at a time.
3232
| Code-generated clients (Service) | `Restate.service(Class)` / `Restate.serviceHandle(Class)` |
3333
| Code-generated clients (Virtual Object) | `Restate.virtualObject(Class, key)` / `Restate.virtualObjectHandle(Class, key)` |
3434
| Code-generated clients (Workflow) | `Restate.workflow(Class, key)` / `Restate.workflowHandle(Class, key)` |
35+
| `ctx.call(request)` / `ctx.send(request, delay)` (raw `Request`) | `Restate.call(request)` / `Restate.send(request[, delay])` |
3536

3637
From **outside** a handler (the ingress client), the equivalents live on `dev.restate.client.Client`:
3738
`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
5253
| Code-generated clients (Service) | `service<T>()` / `toService<T>()` |
5354
| Code-generated clients (Virtual Object) | `virtualObject<T>(key)` / `toVirtualObject<T>(key)` |
5455
| Code-generated clients (Workflow) | `workflow<T>(key)` / `toWorkflow<T>(key)` |
56+
| `ctx.call(request)` / `ctx.send(request, delay)` (raw `Request`) | `prepareRequest(request).call()` / `prepareRequest(request).send(delay)` |
5557

5658
All the top-level functions are in the `dev.restate.sdk.kotlin` package — add `import dev.restate.sdk.kotlin.*`.
5759

@@ -143,6 +145,34 @@ int idempotentCount = Restate.virtualObjectHandle(Counter.class, "my-counter")
143145
.await();
144146
```
145147

148+
### 5. Generic / dynamic-target invocation (advanced)
149+
150+
The old codegen `<Service>Handlers` request builders produced a `Request` you passed to
151+
`ctx.call(...)` / `ctx.send(...)`. Those builders are going away. When the target is only known at
152+
runtime — or you otherwise need to build a `Request` by hand rather than through the typed proxies
153+
above — use the generic `Restate.call` / `Restate.send` overloads:
154+
155+
```java
156+
// Before
157+
GreetingResponse response = ctx.call(GreeterHandlers.greet(new Greeting("Alice"))).await();
158+
159+
// After — build the Request manually and pass it to Restate.call / Restate.send
160+
Request<Greeting, GreetingResponse> request =
161+
Request.of(
162+
Target.service("Greeter", "greet"),
163+
TypeTag.of(Greeting.class),
164+
TypeTag.of(GreetingResponse.class),
165+
new Greeting("Alice"))
166+
.idempotencyKey("my-idempotency-key")
167+
.build();
168+
169+
GreetingResponse response = Restate.call(request).await();
170+
171+
// Fire-and-forget, optionally with a delay
172+
Restate.send(request);
173+
Restate.send(request, Duration.ofMinutes(5));
174+
```
175+
146176
---
147177

148178
## Kotlin migration
@@ -233,6 +263,37 @@ val idempotentCount = toVirtualObject<Counter>("my-counter")
233263
.await()
234264
```
235265

266+
### 5. Generic / dynamic-target invocation (advanced)
267+
268+
When the target is only known at runtime — or you otherwise need to build a `Request` by hand rather
269+
than through the typed `toService` / `toVirtualObject` / `toWorkflow` builders — wrap the `Request`
270+
with `prepareRequest(...)`, which exposes the same `options {}` / `call()` / `send()` DSL. This
271+
replaces a raw `ctx.call(request)` / `ctx.send(request, delay)`:
272+
273+
```kotlin
274+
// Before
275+
val response = ctx.call(request).await()
276+
277+
// After — build the Request manually (e.g. via Request.of) and wrap it with prepareRequest
278+
val request =
279+
Request.of(
280+
Target.service("Greeter", "greet"),
281+
typeTag<Greeting>(),
282+
typeTag<GreetingResponse>(),
283+
Greeting("Alice"),
284+
)
285+
286+
val response =
287+
prepareRequest(request)
288+
.options { idempotencyKey = "my-idempotency-key" }
289+
.call()
290+
.await()
291+
292+
// Fire-and-forget, optionally with a delay
293+
prepareRequest(request).send()
294+
prepareRequest(request).send(delay = 5.minutes)
295+
```
296+
236297
### Kotlin gotcha: proxy clients need non-final classes
237298

238299
The proxy clients (`service<T>()`, `virtualObject<T>(key)`, `toService<T>()`, …) create a runtime proxy of

sdk-api-kotlin/src/main/kotlin/dev/restate/sdk/kotlin/api.kt

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1468,7 +1468,11 @@ private class KRequestImpl<Req, Res>(private val request: Request<Req, Res>) :
14681468
val builder = InvocationOptions.builder()
14691469
builder.block()
14701470
return KRequestImpl(
1471-
this.toBuilder().headers(builder.headers).idempotencyKey(builder.idempotencyKey).build()
1471+
this.toBuilder()
1472+
.headers(builder.headers)
1473+
.idempotencyKey(builder.idempotencyKey)
1474+
.limitKey(builder.limitKey)
1475+
.build()
14721476
)
14731477
}
14741478

@@ -1481,6 +1485,26 @@ private class KRequestImpl<Req, Res>(private val request: Request<Req, Res>) :
14811485
}
14821486
}
14831487

1488+
/**
1489+
* Wrap a pre-built [Request] into an invocable [KRequest].
1490+
*
1491+
* This is the low-level, generic invocation entrypoint. For the common case, prefer the type-safe
1492+
* [toService]/[toVirtualObject]/[toWorkflow] (and [service]/[virtualObject]/[workflow]) builders.
1493+
*
1494+
* ```
1495+
* prepareRequest(Request.of(target, Serde.RAW, Serde.RAW, payload).build())
1496+
* .options { idempotencyKey = "my-key" }
1497+
* .call()
1498+
* .await()
1499+
* ```
1500+
*
1501+
* @param request the request describing the target, payload and response type.
1502+
* @return an invocable [KRequest].
1503+
*/
1504+
fun <Req, Res> prepareRequest(request: Request<Req, Res>): KRequest<Req, Res> {
1505+
return KRequestImpl(request)
1506+
}
1507+
14841508
/**
14851509
* **PREVIEW:** Returns a [KScope] that routes all outgoing calls within the given scope.
14861510
*

sdk-api/src/main/java/dev/restate/sdk/Restate.java

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,55 @@ public static DurableFuture<Void> timer(Duration duration) {
443443
return Context.current().timer(duration);
444444
}
445445

446+
/**
447+
* Invoke another Restate service method, and await the response.
448+
*
449+
* <p>This is the low-level, generic invocation entrypoint accepting a pre-built {@link Request}.
450+
* For the common case, prefer the type-safe methods {@link #service(Class)}, {@link
451+
* #serviceHandle(Class)}, {@link #virtualObject(Class, String)}, {@link
452+
* #virtualObjectHandle(Class, String)}, {@link #workflow(Class, String)} and {@link
453+
* #workflowHandle(Class, String)}.
454+
*
455+
* @param request Request object describing the target, payload and response type.
456+
* @return a {@link CallDurableFuture} that wraps the Restate service method result.
457+
* @see #service(Class)
458+
* @see #serviceHandle(Class)
459+
*/
460+
public static <T, R> CallDurableFuture<R> call(Request<T, R> request) {
461+
return Context.current().call(request);
462+
}
463+
464+
/**
465+
* Invoke another Restate service without waiting for the response (fire-and-forget).
466+
*
467+
* <p>This is the low-level, generic invocation entrypoint accepting a pre-built {@link Request}.
468+
* For the common case, prefer the type-safe {@code send(...)} methods on {@link
469+
* #serviceHandle(Class)}, {@link #virtualObjectHandle(Class, String)} and {@link
470+
* #workflowHandle(Class, String)}.
471+
*
472+
* @param request Request object describing the target, payload and response type.
473+
* @return an {@link InvocationHandle} that can be used to retrieve the invocation id, cancel the
474+
* invocation, or attach to its result.
475+
* @see #send(Request, Duration)
476+
* @see #serviceHandle(Class)
477+
*/
478+
public static <T, R> InvocationHandle<R> send(Request<T, R> request) {
479+
return Context.current().send(request);
480+
}
481+
482+
/**
483+
* Like {@link #send(Request)}, but scheduling the invocation after the given {@code delay}.
484+
*
485+
* @param request Request object describing the target, payload and response type.
486+
* @param delay the delay after which the request should be executed.
487+
* @return an {@link InvocationHandle} that can be used to retrieve the invocation id, cancel the
488+
* invocation, or attach to its result.
489+
* @see #send(Request)
490+
*/
491+
public static <T, R> InvocationHandle<R> send(Request<T, R> request, Duration delay) {
492+
return Context.current().send(request, delay);
493+
}
494+
446495
/**
447496
* Simple API to invoke a Restate service.
448497
*

sdk-core/src/test/kotlin/dev/restate/sdk/core/kotlinapi/CallTest.kt

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import dev.restate.common.Slice
1313
import dev.restate.common.Target
1414
import dev.restate.sdk.core.CallTestSuite
1515
import dev.restate.sdk.core.kotlinapi.KotlinAPITests.Companion.testDefinitionForService
16+
import dev.restate.sdk.kotlin.prepareRequest
1617
import dev.restate.serde.Serde
1718

1819
class CallTest : CallTestSuite() {
@@ -23,18 +24,21 @@ class CallTest : CallTestSuite() {
2324
headers: Map<String, String>,
2425
body: Slice,
2526
) =
26-
testDefinitionForService("OneWayCall") { ctx, _: Unit ->
27+
testDefinitionForService("OneWayCall") { _, _: Unit ->
2728
val ignored =
28-
ctx.send(
29-
Request.of<Slice, ByteArray>(target, Serde.SLICE, Serde.RAW, body)
30-
.headers(headers)
31-
.idempotencyKey(idempotencyKey)
32-
)
29+
prepareRequest(
30+
Request.of<Slice, ByteArray>(target, Serde.SLICE, Serde.RAW, body)
31+
.headers(headers)
32+
.idempotencyKey(idempotencyKey)
33+
)
34+
.send()
3335
}
3436

3537
override fun implicitCancellation(target: Target, body: Slice) =
36-
testDefinitionForService("ImplicitCancellation") { ctx, _: Unit ->
38+
testDefinitionForService("ImplicitCancellation") { _, _: Unit ->
3739
val ignored =
38-
ctx.call(Request.of<Slice, ByteArray>(target, Serde.SLICE, Serde.RAW, body)).await()
40+
prepareRequest(Request.of<Slice, ByteArray>(target, Serde.SLICE, Serde.RAW, body))
41+
.call()
42+
.await()
3943
}
4044
}

test-services/src/main/kotlin/dev/restate/sdk/testservices/ProxyImpl.kt

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,7 @@ class ProxyImpl : Proxy {
2828
}
2929

3030
override suspend fun call(request: Proxy.ProxyRequest): ByteArray {
31-
return context()
32-
.call(
31+
return prepareRequest(
3332
Request.of(request.toTarget(), Serde.RAW, Serde.RAW, request.message).also {
3433
if (request.idempotencyKey != null) {
3534
it.idempotencyKey = request.idempotencyKey
@@ -39,31 +38,30 @@ class ProxyImpl : Proxy {
3938
}
4039
}
4140
)
41+
.call()
4242
.await()
4343
}
4444

4545
override suspend fun oneWayCall(request: Proxy.ProxyRequest): String =
46-
context()
47-
.send(
46+
prepareRequest(
4847
Request.of(request.toTarget(), Serde.RAW, Serde.SLICE, request.message).also {
4948
if (request.idempotencyKey != null) {
5049
it.idempotencyKey = request.idempotencyKey
5150
}
5251
if (request.limitKey != null) {
5352
it.limitKey = request.limitKey
5453
}
55-
},
56-
request.delayMillis?.milliseconds ?: Duration.ZERO,
54+
}
5755
)
56+
.send(request.delayMillis?.milliseconds ?: Duration.ZERO)
5857
.invocationId()
5958

6059
override suspend fun manyCalls(requests: List<Proxy.ManyCallRequest>) {
6160
val toAwait = mutableListOf<DurableFuture<ByteArray>>()
6261

6362
for (request in requests) {
6463
if (request.oneWayCall) {
65-
context()
66-
.send(
64+
prepareRequest(
6765
Request.of(
6866
request.proxyRequest.toTarget(),
6967
Serde.RAW,
@@ -77,13 +75,12 @@ class ProxyImpl : Proxy {
7775
if (request.proxyRequest.limitKey != null) {
7876
it.limitKey = request.proxyRequest.limitKey
7977
}
80-
},
81-
request.proxyRequest.delayMillis?.milliseconds ?: Duration.ZERO,
78+
}
8279
)
80+
.send(request.proxyRequest.delayMillis?.milliseconds ?: Duration.ZERO)
8381
} else {
8482
val fut =
85-
context()
86-
.call(
83+
prepareRequest(
8784
Request.of(
8885
request.proxyRequest.toTarget(),
8986
Serde.RAW,
@@ -99,6 +96,7 @@ class ProxyImpl : Proxy {
9996
}
10097
}
10198
)
99+
.call()
102100
if (request.awaitAtTheEnd) {
103101
toAwait.add(fut)
104102
}

0 commit comments

Comments
 (0)