@@ -271,6 +271,108 @@ val <Res> Response<Res>.response: Res
271271val <Res > SendResponse <Res >.sendStatus: SendResponse .SendStatus
272272 get() = this .sendStatus()
273273
274+ /* *
275+ * **PREVIEW:** Returns a [ScopedKotlinClient] that routes all outgoing calls within the given
276+ * scope.
277+ *
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.
284+ *
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`
291+ *
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.
294+ *
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.
297+ *
298+ * The scope key must consist only of `[a-zA-Z0-9_.-]` characters, with `1 <= length <= 36` chars.
299+ *
300+ * Example usage:
301+ * ```kotlin
302+ * // Route a call into a named scope
303+ * client.scope("tenant-123").service<MyService>().process(payload)
304+ * ```
305+ *
306+ * @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>
309+ */
310+ @org.jetbrains.annotations.ApiStatus .Experimental
311+ fun Client.scope (scopeKey : String ): ScopedKotlinClient = ScopedKotlinClient (this , scopeKey)
312+
313+ /* *
314+ * **PREVIEW:** A client for making RPC calls within a specific scope.
315+ *
316+ * Obtain an instance via [Client.scope].
317+ *
318+ * @see Client.scope
319+ */
320+ @org.jetbrains.annotations.ApiStatus .Experimental
321+ class ScopedKotlinClient
322+ @PublishedApi
323+ internal constructor (
324+ @PublishedApi internal val client: Client ,
325+ @PublishedApi internal val scopeKey: String ,
326+ ) {
327+ /* * @see Client.service */
328+ @org.jetbrains.annotations.ApiStatus .Experimental
329+ inline fun <reified SVC : Any > service (): SVC {
330+ return service(client, SVC ::class .java, scopeKey)
331+ }
332+
333+ /* * @see Client.virtualObject */
334+ @org.jetbrains.annotations.ApiStatus .Experimental
335+ inline fun <reified SVC : Any > virtualObject (key : String ): SVC {
336+ return virtualObject(client, SVC ::class .java, key, scopeKey)
337+ }
338+
339+ /* * @see Client.workflow */
340+ @org.jetbrains.annotations.ApiStatus .Experimental
341+ inline fun <reified SVC : Any > workflow (key : String ): SVC {
342+ return workflow(client, SVC ::class .java, key, scopeKey)
343+ }
344+
345+ /* * @see Client.toService */
346+ @org.jetbrains.annotations.ApiStatus .Experimental
347+ inline fun <reified SVC : Any > toService (): KClientRequestBuilder <SVC > {
348+ ReflectionUtils .mustHaveServiceAnnotation(SVC ::class .java)
349+ require(ReflectionUtils .isKotlinClass(SVC ::class .java)) {
350+ " Using Java classes with Kotlin's API is not supported"
351+ }
352+ return KClientRequestBuilder (client, SVC ::class .java, null , scopeKey)
353+ }
354+
355+ /* * @see Client.toVirtualObject */
356+ @org.jetbrains.annotations.ApiStatus .Experimental
357+ inline fun <reified SVC : Any > toVirtualObject (key : String ): KClientRequestBuilder <SVC > {
358+ ReflectionUtils .mustHaveVirtualObjectAnnotation(SVC ::class .java)
359+ require(ReflectionUtils .isKotlinClass(SVC ::class .java)) {
360+ " Using Java classes with Kotlin's API is not supported"
361+ }
362+ return KClientRequestBuilder (client, SVC ::class .java, key, scopeKey)
363+ }
364+
365+ /* * @see Client.toWorkflow */
366+ @org.jetbrains.annotations.ApiStatus .Experimental
367+ inline fun <reified SVC : Any > toWorkflow (key : String ): KClientRequestBuilder <SVC > {
368+ ReflectionUtils .mustHaveWorkflowAnnotation(SVC ::class .java)
369+ require(ReflectionUtils .isKotlinClass(SVC ::class .java)) {
370+ " Using Java classes with Kotlin's API is not supported"
371+ }
372+ return KClientRequestBuilder (client, SVC ::class .java, key, scopeKey)
373+ }
374+ }
375+
274376/* *
275377 * Create a proxy client for a Restate service.
276378 *
@@ -329,15 +431,15 @@ inline fun <reified SVC : Any> Client.workflow(key: String): SVC {
329431 * @return a proxy that intercepts method calls and executes them via the client
330432 */
331433@PublishedApi
332- internal fun <SVC : Any > service (client : Client , clazz : Class <SVC >): SVC {
434+ internal fun <SVC : Any > service (client : Client , clazz : Class <SVC >, scope : String? = null ): SVC {
333435 ReflectionUtils .mustHaveServiceAnnotation(clazz)
334436 require(ReflectionUtils .isKotlinClass(clazz)) {
335437 " Using Java classes with Kotlin's API is not supported"
336438 }
337439
338440 val serviceName = ReflectionUtils .extractServiceName(clazz)
339441 return ProxySupport .createProxy(clazz) { invocation ->
340- val request = invocation.captureInvocation(serviceName, null ).toRequest()
442+ val request = invocation.captureInvocation(serviceName, null , scope ).toRequest()
341443 @Suppress(" UNCHECKED_CAST" ) val continuation = invocation.arguments.last() as Continuation <Any ?>
342444
343445 // Start a coroutine that calls the client and resumes the continuation
@@ -356,15 +458,20 @@ internal fun <SVC : Any> service(client: Client, clazz: Class<SVC>): SVC {
356458 * @return a proxy that intercepts method calls and executes them via the client
357459 */
358460@PublishedApi
359- internal fun <SVC : Any > virtualObject (client : Client , clazz : Class <SVC >, key : String ): SVC {
461+ internal fun <SVC : Any > virtualObject (
462+ client : Client ,
463+ clazz : Class <SVC >,
464+ key : String ,
465+ scope : String? = null,
466+ ): SVC {
360467 ReflectionUtils .mustHaveVirtualObjectAnnotation(clazz)
361468 require(ReflectionUtils .isKotlinClass(clazz)) {
362469 " Using Java classes with Kotlin's API is not supported"
363470 }
364471
365472 val serviceName = ReflectionUtils .extractServiceName(clazz)
366473 return ProxySupport .createProxy(clazz) { invocation ->
367- val request = invocation.captureInvocation(serviceName, key).toRequest()
474+ val request = invocation.captureInvocation(serviceName, key, scope ).toRequest()
368475 @Suppress(" UNCHECKED_CAST" ) val continuation = invocation.arguments.last() as Continuation <Any ?>
369476
370477 // Start a coroutine that calls the client and resumes the continuation
@@ -383,15 +490,20 @@ internal fun <SVC : Any> virtualObject(client: Client, clazz: Class<SVC>, key: S
383490 * @return a proxy that intercepts method calls and executes them via the client
384491 */
385492@PublishedApi
386- internal fun <SVC : Any > workflow (client : Client , clazz : Class <SVC >, key : String ): SVC {
493+ internal fun <SVC : Any > workflow (
494+ client : Client ,
495+ clazz : Class <SVC >,
496+ key : String ,
497+ scope : String? = null,
498+ ): SVC {
387499 ReflectionUtils .mustHaveWorkflowAnnotation(clazz)
388500 require(ReflectionUtils .isKotlinClass(clazz)) {
389501 " Using Java classes with Kotlin's API is not supported"
390502 }
391503
392504 val serviceName = ReflectionUtils .extractServiceName(clazz)
393505 return ProxySupport .createProxy(clazz) { invocation ->
394- val request = invocation.captureInvocation(serviceName, key).toRequest()
506+ val request = invocation.captureInvocation(serviceName, key, scope ).toRequest()
395507 @Suppress(" UNCHECKED_CAST" ) val continuation = invocation.arguments.last() as Continuation <Any ?>
396508
397509 // Start a coroutine that calls the client and resumes the continuation
@@ -414,6 +526,7 @@ internal constructor(
414526 private val client: Client ,
415527 private val clazz: Class <SVC >,
416528 private val key: String? ,
529+ private val scope: String? = null ,
417530) {
418531 /* *
419532 * Create a request by invoking a method on the target.
@@ -428,7 +541,7 @@ internal constructor(
428541 suspend fun <Res > request (block : suspend SVC .() -> Res ): KClientRequest <Any ?, Res > {
429542 return KClientRequestImpl (
430543 client,
431- RequestCaptureProxy (clazz, key).capture(block as suspend SVC .() -> Any? ).toRequest(),
544+ RequestCaptureProxy (clazz, key, scope ).capture(block as suspend SVC .() -> Any? ).toRequest(),
432545 )
433546 as KClientRequest <Any ?, Res >
434547 }
0 commit comments