restate-sdk 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6c2177e5412d0b530fcddf1ac29f0ac8caf020169e00499fcf32955c0e818cec
4
- data.tar.gz: 216ce2e9488ef6e101ab55784bc4e44caa42bcec7631b4f5ea3ac2591e917691
3
+ metadata.gz: af00581289bac17d418aea72014305bdd1fd64e3a8d02a0ca6a5194ac1497443
4
+ data.tar.gz: 3f9dc77d320b092cd074b7e97317893cee9332b0e2a9014c4020b50500ad8701
5
5
  SHA512:
6
- metadata.gz: f04052cbb2e8e8b98ee26ac6e8f085c11759123e7744e959d6cd4cdcd244da9044f6571dfc66fe8511ec188a50a9bc070f6b02a30dd9a5c60a22414ca524791e
7
- data.tar.gz: 9038a6a76a4cde0fe6612921552b9564e4dbe12fcd7db2a9b5d0c3bc04d1f0176c975fbc0b1d43dbcc317ea7d8bd032be090a205e9d3b91cf35738d9fabcfd57
6
+ metadata.gz: 471fd2f7996b2a03ad7f77d9ed27a0ab5fa03f4f65350292845893b5727165277fc2c7d6b767a5e775dfa26a6b0cd7911bda317b25d7ed19cd35cfd524b3259a
7
+ data.tar.gz: a0f4f51c113aa455e649a2194bc0b4095110e746122750687017397911dcd3e107eb81f2f8fe1a7feb893fd83c894666157cca2b8c1f487c760ed22d737a05b0
data/Cargo.lock CHANGED
@@ -830,9 +830,9 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
830
830
 
831
831
  [[package]]
832
832
  name = "restate-sdk-shared-core"
833
- version = "7.0.0"
833
+ version = "7.0.1"
834
834
  source = "registry+https://github.com/rust-lang/crates.io-index"
835
- checksum = "9bf02583379fc28d6386bd864629ba6553ce942124956b862fa9e95a4d836558"
835
+ checksum = "8bd1f1d1961ac32aea52923f089e668a07630ca9c4edcadf18d38bc24e5f14c0"
836
836
  dependencies = [
837
837
  "base64",
838
838
  "bs58",
@@ -850,7 +850,7 @@ dependencies = [
850
850
 
851
851
  [[package]]
852
852
  name = "restate_internal"
853
- version = "1.1.0"
853
+ version = "1.2.0"
854
854
  dependencies = [
855
855
  "magnus",
856
856
  "rb-sys",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "restate_internal"
3
- version = "1.1.0"
3
+ version = "1.2.0"
4
4
  edition = "2021"
5
5
  publish = false
6
6
 
@@ -12,5 +12,5 @@ doc = false
12
12
  [dependencies]
13
13
  magnus = { version = "0.8", features = ["rb-sys"] }
14
14
  rb-sys = { version = "0.9", features = ["stable-api-compiled-fallback"] }
15
- restate-sdk-shared-core = { version = "7.0.0" , features = ["request_identity", "sha2_random_seed", "rust_crypto"] }
15
+ restate-sdk-shared-core = { version = "7.0.1" , features = ["request_identity", "sha2_random_seed", "rust_crypto"] }
16
16
  tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] }
@@ -215,6 +215,9 @@ struct RbInput {
215
215
  key: String,
216
216
  headers: Vec<RbHeader>,
217
217
  input: Vec<u8>,
218
+ scope: Option<String>,
219
+ limit_key: Option<String>,
220
+ idempotency_key: Option<String>,
218
221
  }
219
222
 
220
223
  impl RbInput {
@@ -227,6 +230,15 @@ impl RbInput {
227
230
  fn key(&self) -> &str {
228
231
  &self.key
229
232
  }
233
+ fn scope(&self) -> Option<&str> {
234
+ self.scope.as_deref()
235
+ }
236
+ fn limit_key(&self) -> Option<&str> {
237
+ self.limit_key.as_deref()
238
+ }
239
+ fn idempotency_key(&self) -> Option<&str> {
240
+ self.idempotency_key.as_deref()
241
+ }
230
242
  fn headers_array(&self) -> Result<RArray, Error> {
231
243
  let ruby = Ruby::get().map_err(|_| Error::new(vm_error_class(), "Ruby not available"))?;
232
244
  let ary = ruby.ary_new_capa(self.headers.len());
@@ -249,6 +261,9 @@ impl From<Input> for RbInput {
249
261
  key: i.key,
250
262
  headers: i.headers.into_iter().map(Into::into).collect(),
251
263
  input: i.input.into(),
264
+ scope: i.scope,
265
+ limit_key: i.limit_key,
266
+ idempotency_key: i.idempotency_key,
252
267
  }
253
268
  }
254
269
  }
@@ -584,7 +599,9 @@ impl RbVM {
584
599
  .map_err(core_error_to_magnus)
585
600
  }
586
601
 
587
- // sys_call(service, handler, buffer, key_or_nil, idempotency_key_or_nil, headers_or_nil)
602
+ // sys_call(service, handler, buffer, key_or_nil, idempotency_key_or_nil, headers_or_nil,
603
+ // scope_or_nil, limit_key_or_nil)
604
+ #[allow(clippy::too_many_arguments)]
588
605
  fn sys_call(
589
606
  &self,
590
607
  service: String,
@@ -593,6 +610,8 @@ impl RbVM {
593
610
  key: Value,
594
611
  idempotency_key: Value,
595
612
  headers: Value,
613
+ scope: Value,
614
+ limit_key: Value,
596
615
  ) -> Result<RbCallHandle, Error> {
597
616
  let bytes: Vec<u8> = unsafe { buffer.as_slice().to_vec() };
598
617
  let key_opt: Option<String> = if key.is_nil() {
@@ -610,6 +629,16 @@ impl RbVM {
610
629
  } else {
611
630
  parse_headers_array(RArray::try_convert(headers)?)?
612
631
  };
632
+ let scope_opt: Option<String> = if scope.is_nil() {
633
+ None
634
+ } else {
635
+ Some(String::try_convert(scope)?)
636
+ };
637
+ let limit_key_opt: Option<String> = if limit_key.is_nil() {
638
+ None
639
+ } else {
640
+ Some(String::try_convert(limit_key)?)
641
+ };
613
642
  self.vm
614
643
  .borrow_mut()
615
644
  .sys_call(
@@ -618,8 +647,8 @@ impl RbVM {
618
647
  handler,
619
648
  key: key_opt,
620
649
  idempotency_key: idem_opt,
621
- scope: None,
622
- limit_key: None,
650
+ scope: scope_opt,
651
+ limit_key: limit_key_opt,
623
652
  headers: hdr_vec,
624
653
  },
625
654
  bytes.into(),
@@ -630,7 +659,9 @@ impl RbVM {
630
659
  .map_err(core_error_to_magnus)
631
660
  }
632
661
 
633
- // sys_send(service, handler, buffer, key_or_nil, delay_or_nil, idempotency_key_or_nil, headers_or_nil)
662
+ // sys_send(service, handler, buffer, key_or_nil, delay_or_nil, idempotency_key_or_nil,
663
+ // headers_or_nil, scope_or_nil, limit_key_or_nil)
664
+ #[allow(clippy::too_many_arguments)]
634
665
  fn sys_send(
635
666
  &self,
636
667
  service: String,
@@ -640,6 +671,8 @@ impl RbVM {
640
671
  delay: Value,
641
672
  idempotency_key: Value,
642
673
  headers: Value,
674
+ scope: Value,
675
+ limit_key: Value,
643
676
  ) -> Result<u32, Error> {
644
677
  let bytes: Vec<u8> = unsafe { buffer.as_slice().to_vec() };
645
678
  let key_opt: Option<String> = if key.is_nil() {
@@ -662,6 +695,16 @@ impl RbVM {
662
695
  } else {
663
696
  parse_headers_array(RArray::try_convert(headers)?)?
664
697
  };
698
+ let scope_opt: Option<String> = if scope.is_nil() {
699
+ None
700
+ } else {
701
+ Some(String::try_convert(scope)?)
702
+ };
703
+ let limit_key_opt: Option<String> = if limit_key.is_nil() {
704
+ None
705
+ } else {
706
+ Some(String::try_convert(limit_key)?)
707
+ };
665
708
  self.vm
666
709
  .borrow_mut()
667
710
  .sys_send(
@@ -670,8 +713,8 @@ impl RbVM {
670
713
  handler,
671
714
  key: key_opt,
672
715
  idempotency_key: idem_opt,
673
- scope: None,
674
- limit_key: None,
716
+ scope: scope_opt,
717
+ limit_key: limit_key_opt,
675
718
  headers: hdr_vec,
676
719
  },
677
720
  bytes.into(),
@@ -1135,6 +1178,9 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
1135
1178
  input_class.define_method("invocation_id", method!(RbInput::invocation_id, 0))?;
1136
1179
  input_class.define_method("random_seed", method!(RbInput::random_seed, 0))?;
1137
1180
  input_class.define_method("key", method!(RbInput::key, 0))?;
1181
+ input_class.define_method("scope", method!(RbInput::scope, 0))?;
1182
+ input_class.define_method("limit_key", method!(RbInput::limit_key, 0))?;
1183
+ input_class.define_method("idempotency_key", method!(RbInput::idempotency_key, 0))?;
1138
1184
  input_class.define_method("headers", method!(RbInput::headers_array, 0))?;
1139
1185
  input_class.define_method("input", method!(RbInput::input_bytes, 0))?;
1140
1186
 
@@ -1189,8 +1235,8 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
1189
1235
  vm_class.define_method("sys_clear_state", method!(RbVM::sys_clear_state, 1))?;
1190
1236
  vm_class.define_method("sys_clear_all_state", method!(RbVM::sys_clear_all_state, 0))?;
1191
1237
  vm_class.define_method("sys_sleep", method!(RbVM::sys_sleep, 2))?;
1192
- vm_class.define_method("sys_call", method!(RbVM::sys_call, 6))?;
1193
- vm_class.define_method("sys_send", method!(RbVM::sys_send, 7))?;
1238
+ vm_class.define_method("sys_call", method!(RbVM::sys_call, 8))?;
1239
+ vm_class.define_method("sys_send", method!(RbVM::sys_send, 9))?;
1194
1240
  vm_class.define_method("sys_run", method!(RbVM::sys_run, 1))?;
1195
1241
  vm_class.define_method(
1196
1242
  "propose_run_completion_success",
@@ -63,7 +63,14 @@ module Restate
63
63
  # @return [String] raw input bytes
64
64
  # @!attribute [r] attempt_finished_event
65
65
  # @return [AttemptFinishedEvent] signaled when this attempt ends
66
- Request = Struct.new(:id, :headers, :body, :attempt_finished_event, keyword_init: true)
66
+ # @!attribute [r] scope
67
+ # @return [String, nil] the scope this invocation was routed within, if any
68
+ # @!attribute [r] limit_key
69
+ # @return [String, nil] the concurrency limit key this invocation was made with, if any
70
+ # @!attribute [r] idempotency_key
71
+ # @return [String, nil] the idempotency key this invocation was made with, if any
72
+ Request = Struct.new(:id, :headers, :body, :attempt_finished_event,
73
+ :scope, :limit_key, :idempotency_key, keyword_init: true)
67
74
 
68
75
  # Base context interface for all Restate handlers.
69
76
  #
@@ -120,10 +127,37 @@ module Restate
120
127
  end
121
128
 
122
129
  # Durably call a handler using raw bytes (no serialization).
123
- def generic_call(service, handler, arg, key: nil, idempotency_key: nil, headers: nil); end
130
+ def generic_call(service, handler, arg, key: nil, idempotency_key: nil, headers: nil,
131
+ scope: nil, limit_key: nil)
132
+ end
124
133
 
125
134
  # Fire-and-forget send using raw bytes (no serialization).
126
- def generic_send(service, handler, arg, key: nil, delay: nil, idempotency_key: nil, headers: nil); end
135
+ def generic_send(service, handler, arg, key: nil, delay: nil, idempotency_key: nil, headers: nil,
136
+ scope: nil, limit_key: nil)
137
+ end
138
+
139
+ # Returns a +ScopedContext+ that routes all outgoing calls within the given scope.
140
+ #
141
+ # NOTE: This API is in preview and is not enabled by default. To use it in
142
+ # restate-server 1.7, enable the flow control and protocol v7 experimental
143
+ # features via +RESTATE_EXPERIMENTAL_ENABLE_PROTOCOL_V7=true+ and
144
+ # +RESTATE_EXPERIMENTAL_ENABLE_VQUEUES=true+. These can be enabled only on new
145
+ # clusters. If they aren't enabled, the call fails with a retryable error and
146
+ # keeps retrying until they are.
147
+ #
148
+ # A scope is a sub-grouping of resources (invocations, workflow instances,
149
+ # concurrency limits) within the Restate cluster. It becomes part of the target
150
+ # identity tuple and contributes to the partition key, so all resources in a
151
+ # scope get co-located by the restate-server. Omitting the scope (i.e. using the
152
+ # regular +service_call+ / +workflow_call+ methods) is equivalent to calling
153
+ # with no scope, which is the existing behavior.
154
+ #
155
+ # The scope must consist only of +[a-zA-Z0-9_.-]+ characters, 1 <= length <= 36.
156
+ #
157
+ # @param scope [String] the scope identifier
158
+ # @return [ScopedContext]
159
+ # @see https://docs.restate.dev/services/flow-control
160
+ def scope(scope); end
127
161
 
128
162
  # Create an awakeable for external callbacks.
129
163
  # Returns [awakeable_id, DurableFuture].
@@ -243,8 +243,11 @@ module Restate
243
243
  # ── Service calls ──
244
244
 
245
245
  # Durably calls a handler on a Restate service and returns a future for its result.
246
+ #
247
+ # +scope+ and +limit_key+ are internal parameters set by +ScopedContext+
248
+ # (via +ctx.scope(...)+); prefer that API over passing them directly.
246
249
  def service_call(service, handler, arg, key: nil, idempotency_key: nil, headers: nil,
247
- input_serde: NOT_SET, output_serde: NOT_SET)
250
+ input_serde: NOT_SET, output_serde: NOT_SET, scope: nil, limit_key: nil)
248
251
  svc_name, handler_name, handler_meta = resolve_call_target(service, handler)
249
252
  in_serde = resolve_serde(input_serde, handler_meta, :input_serde)
250
253
  out_serde = resolve_serde(output_serde, handler_meta, :output_serde)
@@ -252,7 +255,7 @@ module Restate
252
255
  with_outbound_middleware(svc_name, handler_name, headers, handler_meta: handler_meta) do |hdrs|
253
256
  call_handle = @vm.sys_call(
254
257
  service: svc_name, handler: handler_name, parameter: parameter,
255
- key: key, idempotency_key: idempotency_key, headers: hdrs
258
+ key: key, idempotency_key: idempotency_key, headers: hdrs, scope: scope, limit_key: limit_key
256
259
  )
257
260
  DurableCallFuture.new(self, call_handle.result_handle, call_handle.invocation_id_handle,
258
261
  output_serde: out_serde)
@@ -260,8 +263,11 @@ module Restate
260
263
  end
261
264
 
262
265
  # Sends a one-way invocation to a Restate service handler (fire-and-forget).
266
+ #
267
+ # +scope+ and +limit_key+ are internal parameters set by +ScopedContext+
268
+ # (via +ctx.scope(...)+); prefer that API over passing them directly.
263
269
  def service_send(service, handler, arg, key: nil, delay: nil, idempotency_key: nil, headers: nil,
264
- input_serde: NOT_SET)
270
+ input_serde: NOT_SET, scope: nil, limit_key: nil)
265
271
  svc_name, handler_name, handler_meta = resolve_call_target(service, handler)
266
272
  in_serde = resolve_serde(input_serde, handler_meta, :input_serde)
267
273
  parameter = in_serde.serialize(arg)
@@ -269,15 +275,19 @@ module Restate
269
275
  with_outbound_middleware(svc_name, handler_name, headers, handler_meta: handler_meta) do |hdrs|
270
276
  invocation_id_handle = @vm.sys_send(
271
277
  service: svc_name, handler: handler_name, parameter: parameter,
272
- key: key, delay: delay_ms, idempotency_key: idempotency_key, headers: hdrs
278
+ key: key, delay: delay_ms, idempotency_key: idempotency_key, headers: hdrs,
279
+ scope: scope, limit_key: limit_key
273
280
  )
274
281
  SendHandle.new(self, invocation_id_handle)
275
282
  end
276
283
  end
277
284
 
278
285
  # Durably calls a handler on a Restate virtual object, keyed by +key+.
286
+ #
287
+ # +scope+ and +limit_key+ are internal parameters set by +ScopedContext+
288
+ # (via +ctx.scope(...)+); prefer that API over passing them directly.
279
289
  def object_call(service, handler, key, arg, idempotency_key: nil, headers: nil,
280
- input_serde: NOT_SET, output_serde: NOT_SET)
290
+ input_serde: NOT_SET, output_serde: NOT_SET, scope: nil, limit_key: nil)
281
291
  svc_name, handler_name, handler_meta = resolve_call_target(service, handler)
282
292
  in_serde = resolve_serde(input_serde, handler_meta, :input_serde)
283
293
  out_serde = resolve_serde(output_serde, handler_meta, :output_serde)
@@ -285,7 +295,7 @@ module Restate
285
295
  with_outbound_middleware(svc_name, handler_name, headers, handler_meta: handler_meta) do |hdrs|
286
296
  call_handle = @vm.sys_call(
287
297
  service: svc_name, handler: handler_name, parameter: parameter,
288
- key: key, idempotency_key: idempotency_key, headers: hdrs
298
+ key: key, idempotency_key: idempotency_key, headers: hdrs, scope: scope, limit_key: limit_key
289
299
  )
290
300
  DurableCallFuture.new(self, call_handle.result_handle, call_handle.invocation_id_handle,
291
301
  output_serde: out_serde)
@@ -293,8 +303,11 @@ module Restate
293
303
  end
294
304
 
295
305
  # Sends a one-way invocation to a Restate virtual object handler (fire-and-forget).
306
+ #
307
+ # +scope+ and +limit_key+ are internal parameters set by +ScopedContext+
308
+ # (via +ctx.scope(...)+); prefer that API over passing them directly.
296
309
  def object_send(service, handler, key, arg, delay: nil, idempotency_key: nil, headers: nil,
297
- input_serde: NOT_SET)
310
+ input_serde: NOT_SET, scope: nil, limit_key: nil)
298
311
  svc_name, handler_name, handler_meta = resolve_call_target(service, handler)
299
312
  in_serde = resolve_serde(input_serde, handler_meta, :input_serde)
300
313
  parameter = in_serde.serialize(arg)
@@ -302,24 +315,38 @@ module Restate
302
315
  with_outbound_middleware(svc_name, handler_name, headers, handler_meta: handler_meta) do |hdrs|
303
316
  invocation_id_handle = @vm.sys_send(
304
317
  service: svc_name, handler: handler_name, parameter: parameter,
305
- key: key, delay: delay_ms, idempotency_key: idempotency_key, headers: hdrs
318
+ key: key, delay: delay_ms, idempotency_key: idempotency_key, headers: hdrs,
319
+ scope: scope, limit_key: limit_key
306
320
  )
307
321
  SendHandle.new(self, invocation_id_handle)
308
322
  end
309
323
  end
310
324
 
311
325
  # Durably calls a handler on a Restate workflow, keyed by +key+.
326
+ #
327
+ # +scope+ and +limit_key+ are internal parameters set by +ScopedContext+
328
+ # (via +ctx.scope(...)+); prefer that API over passing them directly.
312
329
  def workflow_call(service, handler, key, arg, idempotency_key: nil, headers: nil,
313
- input_serde: NOT_SET, output_serde: NOT_SET)
314
- object_call(service, handler, key, arg, idempotency_key: idempotency_key, headers: headers,
315
- input_serde: input_serde, output_serde: output_serde) # rubocop:disable Layout/HashAlignment
330
+ input_serde: NOT_SET, output_serde: NOT_SET, scope: nil, limit_key: nil)
331
+ object_call(
332
+ service, handler, key, arg,
333
+ idempotency_key: idempotency_key, headers: headers,
334
+ input_serde: input_serde, output_serde: output_serde,
335
+ scope: scope, limit_key: limit_key
336
+ )
316
337
  end
317
338
 
318
339
  # Sends a one-way invocation to a Restate workflow handler (fire-and-forget).
340
+ #
341
+ # +scope+ and +limit_key+ are internal parameters set by +ScopedContext+
342
+ # (via +ctx.scope(...)+); prefer that API over passing them directly.
319
343
  def workflow_send(service, handler, key, arg, delay: nil, idempotency_key: nil, headers: nil,
320
- input_serde: NOT_SET)
321
- object_send(service, handler, key, arg, delay: delay, idempotency_key: idempotency_key, headers: headers,
322
- input_serde: input_serde) # rubocop:disable Layout/HashAlignment
344
+ input_serde: NOT_SET, scope: nil, limit_key: nil)
345
+ object_send(
346
+ service, handler, key, arg,
347
+ delay: delay, idempotency_key: idempotency_key, headers: headers,
348
+ input_serde: input_serde, scope: scope, limit_key: limit_key
349
+ )
323
350
  end
324
351
 
325
352
  # ── Awakeables ──
@@ -403,11 +430,15 @@ module Restate
403
430
  # ── Generic calls (raw bytes, no serde) ──
404
431
 
405
432
  # Durably calls a handler using raw bytes (no serialization). Useful for proxying.
406
- def generic_call(service, handler, arg, key: nil, idempotency_key: nil, headers: nil)
433
+ #
434
+ # Optionally accepts a +scope+ to route the call within (see +#scope+) and a
435
+ # +limit_key+ to enforce hierarchical concurrency limits within that scope.
436
+ def generic_call(service, handler, arg, key: nil, idempotency_key: nil, headers: nil,
437
+ scope: nil, limit_key: nil)
407
438
  with_outbound_middleware(service, handler, headers) do |hdrs|
408
439
  call_handle = @vm.sys_call(
409
440
  service: service, handler: handler, parameter: arg,
410
- key: key, idempotency_key: idempotency_key, headers: hdrs
441
+ key: key, idempotency_key: idempotency_key, headers: hdrs, scope: scope, limit_key: limit_key
411
442
  )
412
443
  DurableCallFuture.new(self, call_handle.result_handle, call_handle.invocation_id_handle,
413
444
  output_serde: nil)
@@ -415,26 +446,42 @@ module Restate
415
446
  end
416
447
 
417
448
  # Sends a one-way invocation using raw bytes (no serialization). Useful for proxying.
418
- def generic_send(service, handler, arg, key: nil, delay: nil, idempotency_key: nil, headers: nil)
449
+ #
450
+ # Optionally accepts a +scope+ to route the send within (see +#scope+) and a
451
+ # +limit_key+ to enforce hierarchical concurrency limits within that scope.
452
+ def generic_send(service, handler, arg, key: nil, delay: nil, idempotency_key: nil, headers: nil,
453
+ scope: nil, limit_key: nil)
419
454
  delay_ms = delay ? (delay * 1000).to_i : nil
420
455
  with_outbound_middleware(service, handler, headers) do |hdrs|
421
456
  invocation_id_handle = @vm.sys_send(
422
457
  service: service, handler: handler, parameter: arg,
423
- key: key, delay: delay_ms, idempotency_key: idempotency_key, headers: hdrs
458
+ key: key, delay: delay_ms, idempotency_key: idempotency_key, headers: hdrs,
459
+ scope: scope, limit_key: limit_key
424
460
  )
425
461
  SendHandle.new(self, invocation_id_handle)
426
462
  end
427
463
  end
428
464
 
465
+ # Returns a +ScopedContext+ that routes all outgoing calls within the given scope.
466
+ # See {Restate::Context#scope} for the full semantics and constraints.
467
+ def scope(scope)
468
+ ScopedContext.new(self, scope)
469
+ end
470
+
429
471
  # ── Request metadata ──
430
472
 
431
- # Returns metadata about the current invocation (id, headers, raw body).
473
+ # Returns metadata about the current invocation (id, headers, raw body,
474
+ # and — when present — the scope, limit_key, and idempotency_key it was
475
+ # invoked with).
432
476
  def request
433
477
  @request ||= Request.new(
434
478
  id: @invocation.invocation_id,
435
479
  headers: @invocation.headers.to_h,
436
480
  body: @invocation.input_buffer,
437
- attempt_finished_event: @attempt_finished_event
481
+ attempt_finished_event: @attempt_finished_event,
482
+ scope: @invocation.scope,
483
+ limit_key: @invocation.limit_key,
484
+ idempotency_key: @invocation.idempotency_key
438
485
  )
439
486
  end
440
487
 
@@ -737,5 +784,92 @@ module Restate
737
784
  end
738
785
  end
739
786
  end
787
+
788
+ # A context for making RPC calls within a specific scope.
789
+ #
790
+ # NOTE: This API is in preview and is not enabled by default. See
791
+ # {Restate::Context#scope} for how to enable it and for the full semantics.
792
+ #
793
+ # Returned by +ctx.scope(scope)+: calls and sends made through this context
794
+ # carry the captured scope, and each method additionally accepts an optional
795
+ # +limit_key+.
796
+ #
797
+ # The limit key enforces hierarchical concurrency limits on invocations sharing
798
+ # the same scope. It can have one or two levels separated by +/+ (e.g. +"tenant1"+
799
+ # or +"tenant1/user42"+). Each level must consist only of +[a-zA-Z0-9_.-]+
800
+ # characters, 1 <= length <= 36.
801
+ #
802
+ # The limit key is *not* part of the request identity: two calls to the same
803
+ # target with the same scope and object key but different limit keys refer to the
804
+ # *same* resource instance. The limit key only affects concurrency limits.
805
+ class ScopedContext
806
+ def initialize(ctx, scope)
807
+ @ctx = ctx
808
+ @scope = scope
809
+ end
810
+
811
+ # Durably calls a handler on a Restate service, within this scope.
812
+ def service_call(service, handler, arg, key: nil, limit_key: nil, idempotency_key: nil,
813
+ headers: nil, input_serde: NOT_SET, output_serde: NOT_SET)
814
+ @ctx.service_call(
815
+ service, handler, arg,
816
+ key: key, idempotency_key: idempotency_key, headers: headers,
817
+ input_serde: input_serde, output_serde: output_serde,
818
+ scope: @scope, limit_key: limit_key
819
+ )
820
+ end
821
+
822
+ # Fire-and-forget send to a Restate service handler, within this scope.
823
+ def service_send(service, handler, arg, key: nil, delay: nil, limit_key: nil,
824
+ idempotency_key: nil, headers: nil, input_serde: NOT_SET)
825
+ @ctx.service_send(
826
+ service, handler, arg,
827
+ key: key, delay: delay, idempotency_key: idempotency_key, headers: headers,
828
+ input_serde: input_serde, scope: @scope, limit_key: limit_key
829
+ )
830
+ end
831
+
832
+ # Durably calls a handler on a Restate virtual object, within this scope.
833
+ def object_call(service, handler, key, arg, limit_key: nil, idempotency_key: nil,
834
+ headers: nil, input_serde: NOT_SET, output_serde: NOT_SET)
835
+ @ctx.object_call(
836
+ service, handler, key, arg,
837
+ idempotency_key: idempotency_key, headers: headers,
838
+ input_serde: input_serde, output_serde: output_serde,
839
+ scope: @scope, limit_key: limit_key
840
+ )
841
+ end
842
+
843
+ # Fire-and-forget send to a Restate virtual object handler, within this scope.
844
+ def object_send(service, handler, key, arg, delay: nil, limit_key: nil,
845
+ idempotency_key: nil, headers: nil, input_serde: NOT_SET)
846
+ @ctx.object_send(
847
+ service, handler, key, arg,
848
+ delay: delay, idempotency_key: idempotency_key, headers: headers,
849
+ input_serde: input_serde, scope: @scope, limit_key: limit_key
850
+ )
851
+ end
852
+
853
+ # Durably calls a handler on a Restate workflow, within this scope.
854
+ def workflow_call(service, handler, key, arg, limit_key: nil, idempotency_key: nil,
855
+ headers: nil, input_serde: NOT_SET, output_serde: NOT_SET)
856
+ @ctx.workflow_call(
857
+ service, handler, key, arg,
858
+ idempotency_key: idempotency_key, headers: headers,
859
+ input_serde: input_serde, output_serde: output_serde,
860
+ scope: @scope, limit_key: limit_key
861
+ )
862
+ end
863
+
864
+ # Fire-and-forget send to a Restate workflow handler, within this scope.
865
+ def workflow_send(service, handler, key, arg, delay: nil, limit_key: nil,
866
+ idempotency_key: nil, headers: nil, input_serde: NOT_SET)
867
+ @ctx.workflow_send(
868
+ service, handler, key, arg,
869
+ delay: delay, idempotency_key: idempotency_key, headers: headers,
870
+ input_serde: input_serde, scope: @scope, limit_key: limit_key
871
+ )
872
+ end
873
+ end
740
874
  end
741
875
  end
@@ -33,10 +33,13 @@ module Restate
33
33
  #
34
34
  # @example
35
35
  # Greeter.call.greet("World").await
36
+ # Greeter.call(scope: "tenant1", limit_key: "tenant1/user42").greet("World").await
36
37
  #
38
+ # @param scope [String, nil] optional scope to route the call within (see {Restate.scope})
39
+ # @param limit_key [String, nil] optional concurrency limit key within the scope
37
40
  # @return [ServiceCallProxy]
38
- def self.call
39
- ServiceCallProxy.new(self, call_method: :service_call)
41
+ def self.call(scope: nil, limit_key: nil)
42
+ ServiceCallProxy.new(self, call_method: :service_call, scope: scope, limit_key: limit_key)
40
43
  end
41
44
 
42
45
  # Returns a send proxy for fluent fire-and-forget sends to this service.
@@ -44,11 +47,14 @@ module Restate
44
47
  # @example
45
48
  # Greeter.send!.greet("World")
46
49
  # Greeter.send!(delay: 60).greet("World")
50
+ # Greeter.send!(scope: "tenant1", limit_key: "tenant1/user42").greet("World")
47
51
  #
48
52
  # @param delay [Numeric, nil] optional delay in seconds
53
+ # @param scope [String, nil] optional scope to route the send within (see {Restate.scope})
54
+ # @param limit_key [String, nil] optional concurrency limit key within the scope
49
55
  # @return [ServiceSendProxy]
50
- def self.send!(delay: nil)
51
- ServiceSendProxy.new(self, send_method: :service_send, delay: delay)
56
+ def self.send!(delay: nil, scope: nil, limit_key: nil)
57
+ ServiceSendProxy.new(self, send_method: :service_send, delay: delay, scope: scope, limit_key: limit_key)
52
58
  end
53
59
 
54
60
  def self._service_kind
@@ -14,16 +14,23 @@ module Restate
14
14
  # # Instead of: ctx.object_call(Counter, :add, "key", 5)
15
15
  # Counter.call("key").add(5)
16
16
  #
17
+ # # Scoped call (preview): route within a scope with an optional limit key
18
+ # Greeter.call(scope: "tenant1", limit_key: "tenant1/user42").greet("World")
19
+ #
17
20
  # @!visibility private
18
21
  class ServiceCallProxy
19
- def initialize(service_class, key: nil, call_method: :service_call)
22
+ def initialize(service_class, key: nil, call_method: :service_call, scope: nil, limit_key: nil)
20
23
  @service_class = service_class
21
24
  @key = key
22
25
  @call_method = call_method
26
+ @scope = scope
27
+ @limit_key = limit_key
23
28
  end
24
29
 
25
30
  def method_missing(handler_name, arg = nil, **opts)
26
31
  ctx = Restate.fetch_context!
32
+ opts[:scope] = @scope unless @scope.nil? || opts.key?(:scope)
33
+ opts[:limit_key] = @limit_key unless @limit_key.nil? || opts.key?(:limit_key)
27
34
  if @key
28
35
  ctx.public_send(@call_method, @service_class, handler_name, @key, arg, **opts)
29
36
  else
@@ -47,18 +54,25 @@ module Restate
47
54
  # # Instead of: ctx.object_send(Counter, :add, "key", 5, delay: 60)
48
55
  # Counter.send!("key", delay: 60).add(5)
49
56
  #
57
+ # # Scoped send (preview): route within a scope with an optional limit key
58
+ # Greeter.send!(scope: "tenant1", limit_key: "tenant1/user42").greet("World")
59
+ #
50
60
  # @!visibility private
51
61
  class ServiceSendProxy
52
- def initialize(service_class, key: nil, send_method: :service_send, delay: nil)
62
+ def initialize(service_class, key: nil, send_method: :service_send, delay: nil, scope: nil, limit_key: nil)
53
63
  @service_class = service_class
54
64
  @key = key
55
65
  @send_method = send_method
56
66
  @delay = delay
67
+ @scope = scope
68
+ @limit_key = limit_key
57
69
  end
58
70
 
59
71
  def method_missing(handler_name, arg = nil, **opts)
60
72
  ctx = Restate.fetch_context!
61
73
  opts[:delay] = @delay if @delay
74
+ opts[:scope] = @scope unless @scope.nil? || opts.key?(:scope)
75
+ opts[:limit_key] = @limit_key unless @limit_key.nil? || opts.key?(:limit_key)
62
76
  if @key
63
77
  ctx.public_send(@send_method, @service_class, handler_name, @key, arg, **opts)
64
78
  else
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Restate
5
- VERSION = '1.1.0'
5
+ VERSION = '1.2.0'
6
6
  end
@@ -52,11 +52,14 @@ module Restate
52
52
  #
53
53
  # @example
54
54
  # Counter.call("my-key").add(5).await
55
+ # Counter.call("my-key", scope: "tenant1", limit_key: "tenant1/user42").add(5).await
55
56
  #
56
57
  # @param key [String] the object key
58
+ # @param scope [String, nil] optional scope to route the call within (see {Restate.scope})
59
+ # @param limit_key [String, nil] optional concurrency limit key within the scope
57
60
  # @return [ServiceCallProxy]
58
- def self.call(key)
59
- ServiceCallProxy.new(self, key: key, call_method: :object_call)
61
+ def self.call(key, scope: nil, limit_key: nil)
62
+ ServiceCallProxy.new(self, key: key, call_method: :object_call, scope: scope, limit_key: limit_key)
60
63
  end
61
64
 
62
65
  # Returns a send proxy for fluent fire-and-forget sends to this virtual object.
@@ -64,12 +67,15 @@ module Restate
64
67
  # @example
65
68
  # Counter.send!("my-key").add(5)
66
69
  # Counter.send!("my-key", delay: 60).add(5)
70
+ # Counter.send!("my-key", scope: "tenant1", limit_key: "tenant1/user42").add(5)
67
71
  #
68
72
  # @param key [String] the object key
69
73
  # @param delay [Numeric, nil] optional delay in seconds
74
+ # @param scope [String, nil] optional scope to route the send within (see {Restate.scope})
75
+ # @param limit_key [String, nil] optional concurrency limit key within the scope
70
76
  # @return [ServiceSendProxy]
71
- def self.send!(key, delay: nil)
72
- ServiceSendProxy.new(self, key: key, send_method: :object_send, delay: delay)
77
+ def self.send!(key, delay: nil, scope: nil, limit_key: nil)
78
+ ServiceSendProxy.new(self, key: key, send_method: :object_send, delay: delay, scope: scope, limit_key: limit_key)
73
79
  end
74
80
 
75
81
  def self._service_kind
data/lib/restate/vm.rb CHANGED
@@ -18,7 +18,8 @@ end
18
18
 
19
19
  module Restate
20
20
  # Ruby-side data types for VM results
21
- Invocation = Struct.new(:invocation_id, :random_seed, :headers, :input_buffer, :key, keyword_init: true)
21
+ Invocation = Struct.new(:invocation_id, :random_seed, :headers, :input_buffer, :key,
22
+ :scope, :limit_key, :idempotency_key, keyword_init: true)
22
23
  Failure = Struct.new(:code, :message, :stacktrace, :metadata, keyword_init: true)
23
24
 
24
25
  class NotReady; end
@@ -116,7 +117,10 @@ module Restate
116
117
  random_seed: inp.random_seed,
117
118
  headers: headers,
118
119
  input_buffer: inp.input.b,
119
- key: inp.key
120
+ key: inp.key,
121
+ scope: inp.scope,
122
+ limit_key: inp.limit_key,
123
+ idempotency_key: inp.idempotency_key
120
124
  )
121
125
  end
122
126
 
@@ -145,16 +149,19 @@ module Restate
145
149
  @vm.sys_sleep(millis, name)
146
150
  end
147
151
 
148
- def sys_call(service:, handler:, parameter:, key: nil, idempotency_key: nil, headers: nil)
149
- # Rust side expects 6 args: (service, handler, buffer, key_or_nil, idem_key_or_nil, headers_or_nil)
152
+ def sys_call(service:, handler:, parameter:, key: nil, idempotency_key: nil, headers: nil,
153
+ scope: nil, limit_key: nil)
154
+ # Rust side expects 8 args:
155
+ # (service, handler, buffer, key_or_nil, idem_key_or_nil, headers_or_nil, scope_or_nil, limit_key_or_nil)
150
156
  hdr_array = headers&.map { |k, v| [k, v] }
151
- @vm.sys_call(service, handler, parameter, key, idempotency_key, hdr_array)
157
+ @vm.sys_call(service, handler, parameter, key, idempotency_key, hdr_array, scope, limit_key)
152
158
  end
153
159
 
154
- def sys_send(service:, handler:, parameter:, key: nil, delay: nil, idempotency_key: nil, headers: nil)
155
- # Rust side expects 7 args
160
+ def sys_send(service:, handler:, parameter:, key: nil, delay: nil, idempotency_key: nil, headers: nil,
161
+ scope: nil, limit_key: nil)
162
+ # Rust side expects 9 args
156
163
  hdr_array = headers&.map { |k, v| [k, v] }
157
- @vm.sys_send(service, handler, parameter, key, delay, idempotency_key, hdr_array)
164
+ @vm.sys_send(service, handler, parameter, key, delay, idempotency_key, hdr_array, scope, limit_key)
158
165
  end
159
166
 
160
167
  def sys_run(name)
@@ -52,11 +52,14 @@ module Restate
52
52
  #
53
53
  # @example
54
54
  # UserSignup.call("user42").run("user@example.com").await
55
+ # UserSignup.call("user42", scope: "tenant1", limit_key: "tenant1/user42").run(email).await
55
56
  #
56
57
  # @param key [String] the workflow key
58
+ # @param scope [String, nil] optional scope to route the call within (see {Restate.scope})
59
+ # @param limit_key [String, nil] optional concurrency limit key within the scope
57
60
  # @return [ServiceCallProxy]
58
- def self.call(key)
59
- ServiceCallProxy.new(self, key: key, call_method: :workflow_call)
61
+ def self.call(key, scope: nil, limit_key: nil)
62
+ ServiceCallProxy.new(self, key: key, call_method: :workflow_call, scope: scope, limit_key: limit_key)
60
63
  end
61
64
 
62
65
  # Returns a send proxy for fluent fire-and-forget sends to this workflow.
@@ -64,12 +67,19 @@ module Restate
64
67
  # @example
65
68
  # UserSignup.send!("user42").run("user@example.com")
66
69
  # UserSignup.send!("user42", delay: 60).run("user@example.com")
70
+ # UserSignup.send!("user42", scope: "tenant1", limit_key: "tenant1/user42").run(email)
67
71
  #
68
72
  # @param key [String] the workflow key
69
73
  # @param delay [Numeric, nil] optional delay in seconds
74
+ # @param scope [String, nil] optional scope to route the send within (see {Restate.scope})
75
+ # @param limit_key [String, nil] optional concurrency limit key within the scope
70
76
  # @return [ServiceSendProxy]
71
- def self.send!(key, delay: nil)
72
- ServiceSendProxy.new(self, key: key, send_method: :workflow_send, delay: delay)
77
+ def self.send!(key, delay: nil, scope: nil, limit_key: nil)
78
+ ServiceSendProxy.new(
79
+ self,
80
+ key: key, send_method: :workflow_send,
81
+ delay: delay, scope: scope, limit_key: limit_key
82
+ )
73
83
  end
74
84
 
75
85
  def self._service_kind
data/lib/restate.rb CHANGED
@@ -219,15 +219,27 @@ module Restate # rubocop:disable Metrics/ModuleLength
219
219
  end
220
220
 
221
221
  # Durably call a handler using raw bytes (no serialization).
222
- def generic_call(service, handler, arg, key: nil, idempotency_key: nil, headers: nil)
223
- fetch_context!.generic_call(service, handler, arg, key: key,
224
- idempotency_key: idempotency_key, headers: headers)
222
+ def generic_call(service, handler, arg, key: nil, idempotency_key: nil, headers: nil,
223
+ scope: nil, limit_key: nil)
224
+ fetch_context!.generic_call(service, handler, arg, key: key, idempotency_key: idempotency_key,
225
+ headers: headers, scope: scope, limit_key: limit_key)
225
226
  end
226
227
 
227
228
  # Fire-and-forget send using raw bytes (no serialization).
228
- def generic_send(service, handler, arg, key: nil, delay: nil, idempotency_key: nil, headers: nil)
229
+ def generic_send(service, handler, arg, key: nil, delay: nil, idempotency_key: nil, headers: nil,
230
+ scope: nil, limit_key: nil)
229
231
  fetch_context!.generic_send(service, handler, arg, key: key, delay: delay,
230
- idempotency_key: idempotency_key, headers: headers)
232
+ idempotency_key: idempotency_key, headers: headers,
233
+ scope: scope, limit_key: limit_key)
234
+ end
235
+
236
+ # Returns a +ScopedContext+ that routes all outgoing calls within the given scope.
237
+ # See {Restate::Context#scope} for the full semantics and constraints.
238
+ #
239
+ # @example
240
+ # Restate.scope("tenant1").service_call(Greeter, :greet, "World", limit_key: "tenant1/user42")
241
+ def scope(scope)
242
+ fetch_context!.scope(scope)
231
243
  end
232
244
 
233
245
  # ── Awakeables ──
data/sig/restate.rbs CHANGED
@@ -35,8 +35,9 @@ module Restate
35
35
  def self.object_send: (untyped service, (String | Symbol) handler, String key, untyped arg, ?delay: Numeric?, ?idempotency_key: String?, ?headers: Hash[String, String]?, ?input_serde: untyped) -> SendHandle
36
36
  def self.workflow_call: (untyped service, (String | Symbol) handler, String key, untyped arg, ?idempotency_key: String?, ?headers: Hash[String, String]?, ?input_serde: untyped, ?output_serde: untyped) -> DurableCallFuture
37
37
  def self.workflow_send: (untyped service, (String | Symbol) handler, String key, untyped arg, ?delay: Numeric?, ?idempotency_key: String?, ?headers: Hash[String, String]?, ?input_serde: untyped) -> SendHandle
38
- def self.generic_call: (String service, String handler, String arg, ?key: String?, ?idempotency_key: String?, ?headers: Hash[String, String]?) -> DurableCallFuture
39
- def self.generic_send: (String service, String handler, String arg, ?key: String?, ?delay: Numeric?, ?idempotency_key: String?, ?headers: Hash[String, String]?) -> SendHandle
38
+ def self.generic_call: (String service, String handler, String arg, ?key: String?, ?idempotency_key: String?, ?headers: Hash[String, String]?, ?scope: String?, ?limit_key: String?) -> DurableCallFuture
39
+ def self.generic_send: (String service, String handler, String arg, ?key: String?, ?delay: Numeric?, ?idempotency_key: String?, ?headers: Hash[String, String]?, ?scope: String?, ?limit_key: String?) -> SendHandle
40
+ def self.scope: (String scope) -> Server::ScopedContext
40
41
 
41
42
  # ── Awakeables ──
42
43
 
@@ -197,11 +198,11 @@ module Restate
197
198
  # ── Service proxies ──
198
199
 
199
200
  class ServiceCallProxy
200
- def initialize: (untyped service_class, ?key: String?, ?call_method: Symbol) -> void
201
+ def initialize: (untyped service_class, ?key: String?, ?call_method: Symbol, ?scope: String?, ?limit_key: String?) -> void
201
202
  end
202
203
 
203
204
  class ServiceSendProxy
204
- def initialize: (untyped service_class, ?key: String?, ?send_method: Symbol, ?delay: Numeric?) -> void
205
+ def initialize: (untyped service_class, ?key: String?, ?send_method: Symbol, ?delay: Numeric?, ?scope: String?, ?limit_key: String?) -> void
205
206
  end
206
207
 
207
208
  # ── Service classes (declared but not checked — heavy metaprogramming) ──
@@ -225,6 +226,16 @@ module Restate
225
226
 
226
227
  class Context
227
228
  end
229
+
230
+ class ScopedContext
231
+ def initialize: (untyped ctx, String scope) -> void
232
+ def service_call: (untyped service, (String | Symbol) handler, untyped arg, ?key: String?, ?limit_key: String?, ?idempotency_key: String?, ?headers: Hash[String, String]?, ?input_serde: untyped, ?output_serde: untyped) -> DurableCallFuture
233
+ def service_send: (untyped service, (String | Symbol) handler, untyped arg, ?key: String?, ?delay: Numeric?, ?limit_key: String?, ?idempotency_key: String?, ?headers: Hash[String, String]?, ?input_serde: untyped) -> SendHandle
234
+ def object_call: (untyped service, (String | Symbol) handler, String key, untyped arg, ?limit_key: String?, ?idempotency_key: String?, ?headers: Hash[String, String]?, ?input_serde: untyped, ?output_serde: untyped) -> DurableCallFuture
235
+ def object_send: (untyped service, (String | Symbol) handler, String key, untyped arg, ?delay: Numeric?, ?limit_key: String?, ?idempotency_key: String?, ?headers: Hash[String, String]?, ?input_serde: untyped) -> SendHandle
236
+ def workflow_call: (untyped service, (String | Symbol) handler, String key, untyped arg, ?limit_key: String?, ?idempotency_key: String?, ?headers: Hash[String, String]?, ?input_serde: untyped, ?output_serde: untyped) -> DurableCallFuture
237
+ def workflow_send: (untyped service, (String | Symbol) handler, String key, untyped arg, ?delay: Numeric?, ?limit_key: String?, ?idempotency_key: String?, ?headers: Hash[String, String]?, ?input_serde: untyped) -> SendHandle
238
+ end
228
239
  end
229
240
 
230
241
  module JsonSerde
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: restate-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Restate Developers
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-07-02 00:00:00.000000000 Z
11
+ date: 2026-07-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async