Skip to content

Commit a8f555c

Browse files
Fix scope/limit key in ingress client, plus scope() reachability in kotlin ingress client (#637)
* Fix kotlin scope() ingress client reachability issue * Fix scope and limitkey path components in client
1 parent 4cbbadc commit a8f555c

5 files changed

Lines changed: 460 additions & 37 deletions

File tree

client-kotlin/build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,9 @@ dependencies {
1616

1717
implementation(project(":common-kotlin"))
1818
implementation(libs.kotlinx.coroutines.core)
19+
20+
testImplementation(libs.junit.jupiter)
21+
testImplementation(libs.assertj)
22+
testImplementation(libs.kotlinx.coroutines.test)
23+
testRuntimeOnly(libs.junit.platform.launcher)
1924
}

client-kotlin/src/main/kotlin/dev/restate/client/kotlin/ingress.kt

Lines changed: 129 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import dev.restate.client.Client
1212
import dev.restate.client.RequestOptions
1313
import dev.restate.client.Response
1414
import dev.restate.client.ResponseHead
15+
import dev.restate.client.ScopedClient
1516
import dev.restate.client.SendResponse
1617
import dev.restate.common.InvocationOptions
1718
import dev.restate.common.Output
@@ -272,51 +273,148 @@ val <Res> SendResponse<Res>.sendStatus: SendResponse.SendStatus
272273
get() = this.sendStatus()
273274

274275
/**
275-
* **PREVIEW:** Returns a [ScopedKotlinClient] that routes all outgoing calls within the given
276-
* scope.
276+
* **PREVIEW:** Create a proxy client for a Restate service within the given [scope][Client.scope].
277277
*
278-
* **NOTE:** This API is in preview and is not enabled by default. To use it in restate-server 1.7,
279-
* enable the flow control and protocol v7 experimental features, via
280-
* `RESTATE_EXPERIMENTAL_ENABLE_PROTOCOL_V7=true` and `RESTATE_EXPERIMENTAL_ENABLE_VQUEUES=true`.
281-
* These can be enabled only on **new clusters**, for more info check out
282-
* https://docs.restate.dev/services/flow-control#enabling-flow-control. If these experimental
283-
* features aren't enabled, the call fails with a retryable error and keeps retrying until they are.
278+
* Example usage:
279+
* ```kotlin
280+
* // Route a call into a named scope
281+
* client.scope("tenant-123").service<MyService>().process(payload)
282+
* ```
284283
*
285-
* A scope is a sub-grouping of resources (invocations, virtual object instances, workflow
286-
* instances, concurrency limits) within the Restate cluster. It becomes part of the target identity
287-
* tuple:
288-
* - `scope, service, handler, idempotencyKey?`
289-
* - `scope, virtualObject, objectKey, handler, idempotencyKey?`
290-
* - `scope, workflow, workflowKey, handler`
284+
* @param SVC the service class annotated with @Service
285+
* @return a proxy client to invoke the service
286+
* @see Client.scope
287+
*/
288+
@org.jetbrains.annotations.ApiStatus.Experimental
289+
inline fun <reified SVC : Any> ScopedClient.service(): SVC {
290+
return service(this.innerClient, SVC::class.java, this.scope)
291+
}
292+
293+
/**
294+
* **PREVIEW:** Create a proxy client for a Restate virtual object within the given
295+
* [scope][Client.scope].
291296
*
292-
* Under the hood, the scope contributes to the partition key, so all resources in a scope get
293-
* co-located by the restate-server.
297+
* *NOTE:* To use scopes with virtual objects you must enable the additional experimental feature in
298+
* restate-server, via `RESTATE_EXPERIMENTAL_ENABLE_SCOPED_VIRTUAL_OBJECTS=true`.
294299
*
295-
* Omitting the scope (i.e. using the regular `service` / `workflow` methods) is equivalent to
296-
* calling with no scope, which is the existing behavior.
300+
* @param SVC the virtual object class annotated with @VirtualObject
301+
* @param key the key identifying the specific virtual object instance
302+
* @return a proxy client to invoke the virtual object
303+
* @see Client.scope
304+
*/
305+
@org.jetbrains.annotations.ApiStatus.Experimental
306+
inline fun <reified SVC : Any> ScopedClient.virtualObject(key: String): SVC {
307+
return virtualObject(this.innerClient, SVC::class.java, key, this.scope)
308+
}
309+
310+
/**
311+
* **PREVIEW:** Create a proxy client for a Restate workflow within the given [scope][Client.scope].
297312
*
298-
* The scope key must consist only of `[a-zA-Z0-9_.-]` characters, with `1 <= length <= 36` chars.
313+
* @param SVC the workflow class annotated with @Workflow
314+
* @param key the key identifying the specific workflow instance
315+
* @return a proxy client to invoke the workflow
316+
* @see Client.scope
317+
*/
318+
@org.jetbrains.annotations.ApiStatus.Experimental
319+
inline fun <reified SVC : Any> ScopedClient.workflow(key: String): SVC {
320+
return workflow(this.innerClient, SVC::class.java, key, this.scope)
321+
}
322+
323+
/**
324+
* **PREVIEW:** Create a builder for invoking a Restate service within the given
325+
* [scope][Client.scope].
299326
*
300327
* Example usage:
301328
* ```kotlin
302-
* // Route a call into a named scope
303-
* client.scope("tenant-123").service<MyService>().process(payload)
329+
* client.scope("tenant-123").toService<MyService>()
330+
* .request { process(payload) }
331+
* .options { idempotencyKey = "req-1" }
332+
* .call()
304333
* ```
305334
*
335+
* @param SVC the service class annotated with @Service
336+
* @return a builder for creating typed requests
337+
* @see Client.scope
338+
*/
339+
@org.jetbrains.annotations.ApiStatus.Experimental
340+
inline fun <reified SVC : Any> ScopedClient.toService(): KClientRequestBuilder<SVC> {
341+
ReflectionUtils.mustHaveServiceAnnotation(SVC::class.java)
342+
require(ReflectionUtils.isKotlinClass(SVC::class.java)) {
343+
"Using Java classes with Kotlin's API is not supported"
344+
}
345+
return KClientRequestBuilder(this.innerClient, SVC::class.java, null, this.scope)
346+
}
347+
348+
/**
349+
* **PREVIEW:** Create a builder for invoking a Restate virtual object within the given
350+
* [scope][Client.scope].
351+
*
352+
* *NOTE:* To use scopes with virtual objects you must enable the additional experimental feature in
353+
* restate-server, via `RESTATE_EXPERIMENTAL_ENABLE_SCOPED_VIRTUAL_OBJECTS=true`.
354+
*
355+
* @param SVC the virtual object class annotated with @VirtualObject
356+
* @param key the key identifying the specific virtual object instance
357+
* @return a builder for creating typed requests
358+
* @see Client.scope
359+
*/
360+
@org.jetbrains.annotations.ApiStatus.Experimental
361+
inline fun <reified SVC : Any> ScopedClient.toVirtualObject(
362+
key: String
363+
): KClientRequestBuilder<SVC> {
364+
ReflectionUtils.mustHaveVirtualObjectAnnotation(SVC::class.java)
365+
require(ReflectionUtils.isKotlinClass(SVC::class.java)) {
366+
"Using Java classes with Kotlin's API is not supported"
367+
}
368+
return KClientRequestBuilder(this.innerClient, SVC::class.java, key, this.scope)
369+
}
370+
371+
/**
372+
* **PREVIEW:** Create a builder for invoking a Restate workflow within the given
373+
* [scope][Client.scope].
374+
*
375+
* @param SVC the workflow class annotated with @Workflow
376+
* @param key the key identifying the specific workflow instance
377+
* @return a builder for creating typed requests
378+
* @see Client.scope
379+
*/
380+
@org.jetbrains.annotations.ApiStatus.Experimental
381+
inline fun <reified SVC : Any> ScopedClient.toWorkflow(key: String): KClientRequestBuilder<SVC> {
382+
ReflectionUtils.mustHaveWorkflowAnnotation(SVC::class.java)
383+
require(ReflectionUtils.isKotlinClass(SVC::class.java)) {
384+
"Using Java classes with Kotlin's API is not supported"
385+
}
386+
return KClientRequestBuilder(this.innerClient, SVC::class.java, key, this.scope)
387+
}
388+
389+
/**
390+
* **PREVIEW:** Returns a [ScopedKotlinClient] that routes all outgoing calls within the given
391+
* scope.
392+
*
393+
* This extension has the same signature as the [Client.scope] member on the Java `Client` interface
394+
* (which returns the platform [ScopedClient]), so it is shadowed by the member and is only
395+
* reachable via an aliased import. Prefer `client.scope(scopeKey)`, which returns [ScopedClient]
396+
* and now exposes the Kotlin `service`/`virtualObject`/`workflow`/`toService`/... extensions
397+
* directly.
398+
*
306399
* @param scopeKey the scope identifier
307-
* @see <a
308-
* href="https://docs.restate.dev/services/flow-control">https://docs.restate.dev/services/flow-control</a>
309400
*/
401+
@Deprecated(
402+
"Use client.scope(scopeKey), which returns a ScopedClient exposing the Kotlin extension methods. " +
403+
"This extension is shadowed by the Client.scope member and only reachable via an aliased import."
404+
)
405+
@Suppress("DEPRECATION")
310406
@org.jetbrains.annotations.ApiStatus.Experimental
311407
fun Client.scope(scopeKey: String): ScopedKotlinClient = ScopedKotlinClient(this, scopeKey)
312408

313409
/**
314410
* **PREVIEW:** A client for making RPC calls within a specific scope.
315411
*
316-
* Obtain an instance via [Client.scope].
317-
*
318-
* @see Client.scope
412+
* Use the extension methods on the platform [ScopedClient] instead, obtained via
413+
* `client.scope(scopeKey)`.
319414
*/
415+
@Deprecated(
416+
"Use the extension methods on ScopedClient, obtained via client.scope(scopeKey).",
417+
)
320418
@org.jetbrains.annotations.ApiStatus.Experimental
321419
class ScopedKotlinClient
322420
@PublishedApi
@@ -673,7 +771,11 @@ private class KClientRequestImpl<Req, Res>(
673771
builder.block()
674772
return KClientRequestImpl(
675773
client,
676-
this.toBuilder().headers(builder.headers).idempotencyKey(builder.idempotencyKey).build(),
774+
this.toBuilder()
775+
.headers(builder.headers)
776+
.idempotencyKey(builder.idempotencyKey)
777+
.limitKey(builder.limitKey)
778+
.build(),
677779
)
678780
}
679781

0 commit comments

Comments
 (0)