Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -52,6 +53,7 @@ From **outside** a handler (the ingress client), the equivalents live on `dev.re
| Code-generated clients (Service) | `service<T>()` / `toService<T>()` |
| Code-generated clients (Virtual Object) | `virtualObject<T>(key)` / `toVirtualObject<T>(key)` |
| Code-generated clients (Workflow) | `workflow<T>(key)` / `toWorkflow<T>(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.*`.

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

### 5. Generic / dynamic-target invocation (advanced)

The old codegen `<Service>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<Greeting, GreetingResponse> 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
Expand Down Expand Up @@ -233,6 +263,37 @@ val idempotentCount = toVirtualObject<Counter>("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<Greeting>(),
typeTag<GreetingResponse>(),
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<T>()`, `virtualObject<T>(key)`, `toService<T>()`, …) create a runtime proxy of
Expand Down
26 changes: 25 additions & 1 deletion sdk-api-kotlin/src/main/kotlin/dev/restate/sdk/kotlin/api.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1468,7 +1468,11 @@ private class KRequestImpl<Req, Res>(private val request: Request<Req, Res>) :
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()
)
}

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

/**
* 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 <Req, Res> prepareRequest(request: Request<Req, Res>): KRequest<Req, Res> {
return KRequestImpl(request)
}

/**
* **PREVIEW:** Returns a [KScope] that routes all outgoing calls within the given scope.
*
Expand Down
49 changes: 49 additions & 0 deletions sdk-api/src/main/java/dev/restate/sdk/Restate.java
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,55 @@ public static DurableFuture<Void> timer(Duration duration) {
return Context.current().timer(duration);
}

/**
* Invoke another Restate service method, and await the response.
*
* <p>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 <T, R> CallDurableFuture<R> call(Request<T, R> request) {
return Context.current().call(request);
}

/**
* Invoke another Restate service without waiting for the response (fire-and-forget).
*
* <p>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 <T, R> InvocationHandle<R> send(Request<T, R> 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 <T, R> InvocationHandle<R> send(Request<T, R> request, Duration delay) {
return Context.current().send(request, delay);
}

/**
* Simple API to invoke a Restate service.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -23,18 +24,21 @@ class CallTest : CallTestSuite() {
headers: Map<String, String>,
body: Slice,
) =
testDefinitionForService("OneWayCall") { ctx, _: Unit ->
testDefinitionForService("OneWayCall") { _, _: Unit ->
val ignored =
ctx.send(
Request.of<Slice, ByteArray>(target, Serde.SLICE, Serde.RAW, body)
.headers(headers)
.idempotencyKey(idempotencyKey)
)
prepareRequest(
Request.of<Slice, ByteArray>(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<Slice, ByteArray>(target, Serde.SLICE, Serde.RAW, body)).await()
prepareRequest(Request.of<Slice, ByteArray>(target, Serde.SLICE, Serde.RAW, body))
.call()
.await()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -39,31 +38,30 @@ 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
}
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<Proxy.ManyCallRequest>) {
val toAwait = mutableListOf<DurableFuture<ByteArray>>()

for (request in requests) {
if (request.oneWayCall) {
context()
.send(
prepareRequest(
Request.of(
request.proxyRequest.toTarget(),
Serde.RAW,
Expand All @@ -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,
Expand All @@ -99,6 +96,7 @@ class ProxyImpl : Proxy {
}
}
)
.call()
if (request.awaitAtTheEnd) {
toAwait.add(fut)
}
Expand Down
Loading