restate-sdk 0.10.0 → 0.11.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 +4 -4
- data/Cargo.lock +1 -1
- data/ext/restate_internal/Cargo.toml +1 -1
- data/ext/restate_internal/src/lib.rs +52 -0
- data/lib/restate/context.rb +12 -0
- data/lib/restate/middleware/deadlock_detection.rb +216 -0
- data/lib/restate/server_context.rb +41 -15
- data/lib/restate/version.rb +1 -1
- data/lib/restate/vm.rb +13 -0
- data/lib/restate.rb +18 -0
- data/sig/restate.rbs +6 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 55781f5d8508c22ecf7c6e2a161f658403790fc0538e1028725e0c9c5d33ef0b
|
|
4
|
+
data.tar.gz: ec0b68be695aaafd60188b54544533a0ee58166a3898a9d9d194b71754678fde
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 31521448b4cb29fc65cf59edbe4c46bfb88216bb565dbe18eff31ed0a58d7b68e80bd2d59ac338d422df424d2b121a96e2171eee692e9cd6a35373746313c69c
|
|
7
|
+
data.tar.gz: a89ab1dc4a5921fd8cb73f8e9ca85229fb67a6d1468b1109f067bb298158c7022a18ba392f6f9cfd74265b2d0210289a006765e7e267ee77f56f6823eda07db2
|
data/Cargo.lock
CHANGED
|
@@ -794,6 +794,49 @@ impl RbVM {
|
|
|
794
794
|
.map_err(core_error_to_magnus)
|
|
795
795
|
}
|
|
796
796
|
|
|
797
|
+
// ── Signals ──
|
|
798
|
+
|
|
799
|
+
fn sys_signal(&self, signal_name: String) -> Result<u32, Error> {
|
|
800
|
+
self.vm
|
|
801
|
+
.borrow_mut()
|
|
802
|
+
.create_signal_handle(signal_name)
|
|
803
|
+
.map(Into::into)
|
|
804
|
+
.map_err(core_error_to_magnus)
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
fn sys_complete_signal_success(
|
|
808
|
+
&self,
|
|
809
|
+
target_invocation_id: String,
|
|
810
|
+
signal_name: String,
|
|
811
|
+
buffer: RString,
|
|
812
|
+
) -> Result<(), Error> {
|
|
813
|
+
let bytes: Vec<u8> = unsafe { buffer.as_slice().to_vec() };
|
|
814
|
+
self.vm
|
|
815
|
+
.borrow_mut()
|
|
816
|
+
.sys_complete_signal(
|
|
817
|
+
target_invocation_id,
|
|
818
|
+
signal_name,
|
|
819
|
+
NonEmptyValue::Success(bytes.into()),
|
|
820
|
+
)
|
|
821
|
+
.map_err(core_error_to_magnus)
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
fn sys_complete_signal_failure(
|
|
825
|
+
&self,
|
|
826
|
+
target_invocation_id: String,
|
|
827
|
+
signal_name: String,
|
|
828
|
+
failure: &RbFailure,
|
|
829
|
+
) -> Result<(), Error> {
|
|
830
|
+
self.vm
|
|
831
|
+
.borrow_mut()
|
|
832
|
+
.sys_complete_signal(
|
|
833
|
+
target_invocation_id,
|
|
834
|
+
signal_name,
|
|
835
|
+
NonEmptyValue::Failure(failure.clone().into()),
|
|
836
|
+
)
|
|
837
|
+
.map_err(core_error_to_magnus)
|
|
838
|
+
}
|
|
839
|
+
|
|
797
840
|
// ── Cancel invocation ──
|
|
798
841
|
|
|
799
842
|
fn sys_cancel_invocation(&self, target_invocation_id: String) -> Result<(), Error> {
|
|
@@ -1074,6 +1117,15 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
1074
1117
|
"sys_cancel_invocation",
|
|
1075
1118
|
method!(RbVM::sys_cancel_invocation, 1),
|
|
1076
1119
|
)?;
|
|
1120
|
+
vm_class.define_method("sys_signal", method!(RbVM::sys_signal, 1))?;
|
|
1121
|
+
vm_class.define_method(
|
|
1122
|
+
"sys_complete_signal_success",
|
|
1123
|
+
method!(RbVM::sys_complete_signal_success, 3),
|
|
1124
|
+
)?;
|
|
1125
|
+
vm_class.define_method(
|
|
1126
|
+
"sys_complete_signal_failure",
|
|
1127
|
+
method!(RbVM::sys_complete_signal_failure, 3),
|
|
1128
|
+
)?;
|
|
1077
1129
|
|
|
1078
1130
|
// IdentityVerifier
|
|
1079
1131
|
let iv_class = internal.define_class("IdentityVerifier", ruby.class_object())?;
|
data/lib/restate/context.rb
CHANGED
|
@@ -135,6 +135,18 @@ module Restate
|
|
|
135
135
|
# Reject an awakeable with a terminal failure.
|
|
136
136
|
def reject_awakeable(awakeable_id, message, code: 500); end
|
|
137
137
|
|
|
138
|
+
# Wait for a named signal addressed to this invocation.
|
|
139
|
+
# Returns a DurableFuture that resolves once another invocation calls
|
|
140
|
+
# +resolve_signal+ or +reject_signal+ with the same name targeting this
|
|
141
|
+
# invocation's id.
|
|
142
|
+
def signal(name, serde: JsonSerde); end
|
|
143
|
+
|
|
144
|
+
# Send a success value to a named signal on another invocation.
|
|
145
|
+
def resolve_signal(invocation_id, name, payload, serde: JsonSerde); end
|
|
146
|
+
|
|
147
|
+
# Send a terminal failure to a named signal on another invocation.
|
|
148
|
+
def reject_signal(invocation_id, name, message, code: 500); end
|
|
149
|
+
|
|
138
150
|
# Request cancellation of another invocation.
|
|
139
151
|
def cancel_invocation(invocation_id); end
|
|
140
152
|
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# typed: false
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'base64'
|
|
5
|
+
require 'set'
|
|
6
|
+
|
|
7
|
+
module Restate
|
|
8
|
+
module Middleware
|
|
9
|
+
# Detects VirtualObject deadlocks caused by re-entrant calls to a VO whose
|
|
10
|
+
# exclusive handler is still running higher up the call chain.
|
|
11
|
+
#
|
|
12
|
+
# == The problem
|
|
13
|
+
#
|
|
14
|
+
# Restate VirtualObjects serialize exclusive handler access per key. If handler A
|
|
15
|
+
# on VO key "x" calls handler B on the same VO key "x", the call will block
|
|
16
|
+
# forever — the key is already locked by A. This is a deadlock.
|
|
17
|
+
#
|
|
18
|
+
# == How it works
|
|
19
|
+
#
|
|
20
|
+
# This middleware tracks which VO keys are held by the current call chain and
|
|
21
|
+
# propagates that information via a header on every outbound call.
|
|
22
|
+
#
|
|
23
|
+
# === Inbound side
|
|
24
|
+
#
|
|
25
|
+
# 1. Reads the held-locks header from the incoming request.
|
|
26
|
+
# 2. If the current handler is an exclusive VO handler targeting a key already
|
|
27
|
+
# in the set → deadlock. Raises a {DeadlockError} immediately.
|
|
28
|
+
# 3. If this handler is an exclusive VO handler, appends its lock to the set
|
|
29
|
+
# so further downstream calls propagate it.
|
|
30
|
+
#
|
|
31
|
+
# === Outbound side
|
|
32
|
+
#
|
|
33
|
+
# Injects the held-locks header into every outbound service call. When
|
|
34
|
+
# handler metadata is available (the target service class is known), only
|
|
35
|
+
# raises for exclusive handlers — shared handler calls are safe. Falls
|
|
36
|
+
# back to raising for any same-service call when metadata is unavailable
|
|
37
|
+
# (e.g., calling by string name to an external service).
|
|
38
|
+
#
|
|
39
|
+
# == Wire format
|
|
40
|
+
#
|
|
41
|
+
# Lock entries are encoded as +base64url(service).base64url(key)+ and
|
|
42
|
+
# separated by commas. Base64url encoding ensures arbitrary service names
|
|
43
|
+
# and keys (including those containing +.+, +,+, or non-ASCII characters)
|
|
44
|
+
# are handled correctly.
|
|
45
|
+
#
|
|
46
|
+
# == Journal determinism
|
|
47
|
+
#
|
|
48
|
+
# The held-locks header is deterministic across replays: its value depends only
|
|
49
|
+
# on the execution path, which Restate's journal guarantees is identical on
|
|
50
|
+
# every replay.
|
|
51
|
+
#
|
|
52
|
+
# == Usage
|
|
53
|
+
#
|
|
54
|
+
# endpoint = Restate.endpoint(MyVirtualObject)
|
|
55
|
+
# endpoint.use(Restate::Middleware::DeadlockDetection::Inbound)
|
|
56
|
+
# endpoint.use_outbound(Restate::Middleware::DeadlockDetection::Outbound)
|
|
57
|
+
#
|
|
58
|
+
module DeadlockDetection
|
|
59
|
+
HEADER = 'x-restate-held-locks'
|
|
60
|
+
ENTRY_SEPARATOR = ','
|
|
61
|
+
FIELD_SEPARATOR = '.'
|
|
62
|
+
DEADLOCK_STATUS_CODE = 409
|
|
63
|
+
|
|
64
|
+
THREAD_KEY = :restate_held_exclusive_locks
|
|
65
|
+
|
|
66
|
+
class << self
|
|
67
|
+
# Returns the current set of held exclusive locks for this fiber.
|
|
68
|
+
# Each entry is a two-element array: [service_name, key].
|
|
69
|
+
#
|
|
70
|
+
# @return [Set<Array<String>>]
|
|
71
|
+
def held_locks
|
|
72
|
+
Thread.current[THREAD_KEY] || Set.new
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# @param locks [Set<Array<String>>]
|
|
76
|
+
def held_locks=(locks)
|
|
77
|
+
Thread.current[THREAD_KEY] = locks
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Encodes a [service, key] pair into a wire-safe string.
|
|
81
|
+
def encode_lock(service, key)
|
|
82
|
+
b64_svc = Base64.urlsafe_encode64(service, padding: false)
|
|
83
|
+
b64_key = Base64.urlsafe_encode64(key, padding: false)
|
|
84
|
+
"#{b64_svc}#{FIELD_SEPARATOR}#{b64_key}"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Decodes a wire-format lock string into [service, key].
|
|
88
|
+
# Returns nil if the format is invalid.
|
|
89
|
+
def decode_lock(encoded)
|
|
90
|
+
parts = encoded.split(FIELD_SEPARATOR, 2)
|
|
91
|
+
return nil unless parts.length == 2
|
|
92
|
+
|
|
93
|
+
svc = Base64.urlsafe_decode64(parts[0]).force_encoding('UTF-8')
|
|
94
|
+
key = Base64.urlsafe_decode64(parts[1]).force_encoding('UTF-8')
|
|
95
|
+
[svc, key]
|
|
96
|
+
rescue ArgumentError
|
|
97
|
+
nil
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Serializes a set of [service, key] lock pairs into a header value.
|
|
101
|
+
def encode_header(locks)
|
|
102
|
+
locks.map { |svc, key| encode_lock(svc, key) }.join(ENTRY_SEPARATOR)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Deserializes a header value into a Set of [service, key] pairs.
|
|
106
|
+
def decode_header(raw)
|
|
107
|
+
return Set.new if raw.nil? || raw.to_s.empty?
|
|
108
|
+
|
|
109
|
+
entries = raw.to_s.split(ENTRY_SEPARATOR).filter_map do |entry|
|
|
110
|
+
decode_lock(entry.strip)
|
|
111
|
+
end
|
|
112
|
+
Set.new(entries)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Error raised when a deadlock is detected.
|
|
117
|
+
#
|
|
118
|
+
# Uses status code 409 (Conflict) to signal that retrying won't help.
|
|
119
|
+
class DeadlockError < Restate::TerminalError
|
|
120
|
+
def initialize(message)
|
|
121
|
+
super(message, status_code: DEADLOCK_STATUS_CODE)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Inbound middleware that checks for and tracks VO locks.
|
|
126
|
+
#
|
|
127
|
+
# Register with: +endpoint.use(Restate::Middleware::DeadlockDetection::Inbound)+
|
|
128
|
+
#
|
|
129
|
+
# @example
|
|
130
|
+
# endpoint = Restate.endpoint(MyVirtualObject)
|
|
131
|
+
# endpoint.use(Restate::Middleware::DeadlockDetection::Inbound)
|
|
132
|
+
class Inbound
|
|
133
|
+
def call(handler, ctx)
|
|
134
|
+
previous = DeadlockDetection.held_locks
|
|
135
|
+
incoming = parse_locks(ctx)
|
|
136
|
+
check_and_track_lock!(handler, ctx, incoming)
|
|
137
|
+
DeadlockDetection.held_locks = incoming
|
|
138
|
+
yield
|
|
139
|
+
ensure
|
|
140
|
+
DeadlockDetection.held_locks = previous
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
private
|
|
144
|
+
|
|
145
|
+
def check_and_track_lock!(handler, ctx, incoming)
|
|
146
|
+
return unless handler.service_tag.kind == 'object'
|
|
147
|
+
return unless handler.kind == 'exclusive'
|
|
148
|
+
|
|
149
|
+
key = ctx.respond_to?(:key) ? ctx.key : nil
|
|
150
|
+
return unless key
|
|
151
|
+
|
|
152
|
+
svc = handler.service_tag.name
|
|
153
|
+
lock = [svc, key]
|
|
154
|
+
raise_deadlock!(svc, handler.name, key, incoming) if incoming.include?(lock)
|
|
155
|
+
incoming << lock
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def raise_deadlock!(svc, handler_name, key, locks)
|
|
159
|
+
held = locks.map { |s, k| "#{s}:#{k}" }.join(', ')
|
|
160
|
+
msg = "Deadlock detected: #{svc}##{handler_name} on key '#{key}' " \
|
|
161
|
+
'called while an exclusive handler holds the same VO key. ' \
|
|
162
|
+
"Held locks: #{held}. " \
|
|
163
|
+
'This call will never complete.'
|
|
164
|
+
Kernel.raise DeadlockError, msg
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def parse_locks(ctx)
|
|
168
|
+
headers = ctx.request.headers
|
|
169
|
+
raw = headers.is_a?(Hash) ? headers[HEADER] : nil
|
|
170
|
+
DeadlockDetection.decode_header(raw)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Outbound middleware that propagates held locks via headers.
|
|
175
|
+
#
|
|
176
|
+
# When handler metadata is available (via Thread.current[:restate_outbound_handler_meta]),
|
|
177
|
+
# shared handler calls are allowed through — only exclusive handlers can deadlock.
|
|
178
|
+
# When metadata is unavailable (external service called by string name), falls
|
|
179
|
+
# back to raising for any same-service call.
|
|
180
|
+
#
|
|
181
|
+
# Register with: +endpoint.use_outbound(Restate::Middleware::DeadlockDetection::Outbound)+
|
|
182
|
+
#
|
|
183
|
+
# @example
|
|
184
|
+
# endpoint = Restate.endpoint(MyVirtualObject)
|
|
185
|
+
# endpoint.use_outbound(Restate::Middleware::DeadlockDetection::Outbound)
|
|
186
|
+
class Outbound
|
|
187
|
+
def call(service, handler, headers)
|
|
188
|
+
locks = DeadlockDetection.held_locks
|
|
189
|
+
propagate_and_check!(service, handler, headers, locks) if locks.any?
|
|
190
|
+
yield
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
private
|
|
194
|
+
|
|
195
|
+
def propagate_and_check!(service, handler, headers, locks)
|
|
196
|
+
headers[HEADER] = DeadlockDetection.encode_header(locks)
|
|
197
|
+
|
|
198
|
+
held_lock = locks.find { |svc, _key| svc == service }
|
|
199
|
+
return unless held_lock
|
|
200
|
+
|
|
201
|
+
return if target_shared?
|
|
202
|
+
|
|
203
|
+
msg = "Deadlock detected: outbound call to #{service}##{handler} " \
|
|
204
|
+
"while exclusive lock held on #{held_lock[0]}:#{held_lock[1]}. " \
|
|
205
|
+
'This call will block forever.'
|
|
206
|
+
Kernel.raise DeadlockError, msg
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def target_shared?
|
|
210
|
+
meta = Thread.current[:restate_outbound_handler_meta]
|
|
211
|
+
meta&.kind == 'shared'
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
@@ -206,7 +206,7 @@ module Restate
|
|
|
206
206
|
in_serde = resolve_serde(input_serde, handler_meta, :input_serde)
|
|
207
207
|
out_serde = resolve_serde(output_serde, handler_meta, :output_serde)
|
|
208
208
|
parameter = in_serde.serialize(arg)
|
|
209
|
-
with_outbound_middleware(svc_name, handler_name, headers) do |hdrs|
|
|
209
|
+
with_outbound_middleware(svc_name, handler_name, headers, handler_meta: handler_meta) do |hdrs|
|
|
210
210
|
call_handle = @vm.sys_call(
|
|
211
211
|
service: svc_name, handler: handler_name, parameter: parameter,
|
|
212
212
|
key: key, idempotency_key: idempotency_key, headers: hdrs
|
|
@@ -223,7 +223,7 @@ module Restate
|
|
|
223
223
|
in_serde = resolve_serde(input_serde, handler_meta, :input_serde)
|
|
224
224
|
parameter = in_serde.serialize(arg)
|
|
225
225
|
delay_ms = delay ? (delay * 1000).to_i : nil
|
|
226
|
-
with_outbound_middleware(svc_name, handler_name, headers) do |hdrs|
|
|
226
|
+
with_outbound_middleware(svc_name, handler_name, headers, handler_meta: handler_meta) do |hdrs|
|
|
227
227
|
invocation_id_handle = @vm.sys_send(
|
|
228
228
|
service: svc_name, handler: handler_name, parameter: parameter,
|
|
229
229
|
key: key, delay: delay_ms, idempotency_key: idempotency_key, headers: hdrs
|
|
@@ -239,7 +239,7 @@ module Restate
|
|
|
239
239
|
in_serde = resolve_serde(input_serde, handler_meta, :input_serde)
|
|
240
240
|
out_serde = resolve_serde(output_serde, handler_meta, :output_serde)
|
|
241
241
|
parameter = in_serde.serialize(arg)
|
|
242
|
-
with_outbound_middleware(svc_name, handler_name, headers) do |hdrs|
|
|
242
|
+
with_outbound_middleware(svc_name, handler_name, headers, handler_meta: handler_meta) do |hdrs|
|
|
243
243
|
call_handle = @vm.sys_call(
|
|
244
244
|
service: svc_name, handler: handler_name, parameter: parameter,
|
|
245
245
|
key: key, idempotency_key: idempotency_key, headers: hdrs
|
|
@@ -256,7 +256,7 @@ module Restate
|
|
|
256
256
|
in_serde = resolve_serde(input_serde, handler_meta, :input_serde)
|
|
257
257
|
parameter = in_serde.serialize(arg)
|
|
258
258
|
delay_ms = delay ? (delay * 1000).to_i : nil
|
|
259
|
-
with_outbound_middleware(svc_name, handler_name, headers) do |hdrs|
|
|
259
|
+
with_outbound_middleware(svc_name, handler_name, headers, handler_meta: handler_meta) do |hdrs|
|
|
260
260
|
invocation_id_handle = @vm.sys_send(
|
|
261
261
|
service: svc_name, handler: handler_name, parameter: parameter,
|
|
262
262
|
key: key, delay: delay_ms, idempotency_key: idempotency_key, headers: hdrs
|
|
@@ -298,6 +298,25 @@ module Restate
|
|
|
298
298
|
@vm.sys_complete_awakeable_failure(awakeable_id, failure)
|
|
299
299
|
end
|
|
300
300
|
|
|
301
|
+
# ── Signals ──
|
|
302
|
+
|
|
303
|
+
# Wait for a named signal addressed to this invocation. Returns a DurableFuture.
|
|
304
|
+
def signal(name, serde: JsonSerde)
|
|
305
|
+
handle = @vm.sys_signal(name)
|
|
306
|
+
DurableFuture.new(self, handle, serde: serde)
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# Send a success value to a named signal on another invocation.
|
|
310
|
+
def resolve_signal(invocation_id, name, payload, serde: JsonSerde)
|
|
311
|
+
@vm.sys_complete_signal_success(invocation_id, name, serde.serialize(payload))
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# Send a terminal failure to a named signal on another invocation.
|
|
315
|
+
def reject_signal(invocation_id, name, message, code: 500)
|
|
316
|
+
failure = Failure.new(code: code, message: message)
|
|
317
|
+
@vm.sys_complete_signal_failure(invocation_id, name, failure)
|
|
318
|
+
end
|
|
319
|
+
|
|
301
320
|
# ── Promises (Workflow API) ──
|
|
302
321
|
|
|
303
322
|
# Gets a durable promise value, blocking until resolved.
|
|
@@ -472,18 +491,25 @@ module Restate
|
|
|
472
491
|
# Runs outbound middleware chain (Sidekiq client middleware pattern).
|
|
473
492
|
# Each middleware gets +call(service, handler, headers)+ and must +yield+
|
|
474
493
|
# to continue the chain. The block at the end performs the actual VM call.
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
494
|
+
#
|
|
495
|
+
# The optional +handler_meta+ (a Handler struct from resolve_call_target)
|
|
496
|
+
# is exposed via Thread.current[:restate_outbound_handler_meta] so that
|
|
497
|
+
# middleware can inspect the target handler's kind without changing the
|
|
498
|
+
# middleware interface.
|
|
499
|
+
def with_outbound_middleware(service, handler, headers, handler_meta: nil, &action)
|
|
500
|
+
return action.call(headers) if @outbound_middleware.empty?
|
|
501
|
+
|
|
502
|
+
h = headers || {}
|
|
503
|
+
previous_meta = Thread.current[:restate_outbound_handler_meta]
|
|
504
|
+
Thread.current[:restate_outbound_handler_meta] = handler_meta
|
|
505
|
+
chain = ->(hdrs) { action.call(hdrs) }
|
|
506
|
+
@outbound_middleware.reverse_each do |mw|
|
|
507
|
+
prev = chain
|
|
508
|
+
chain = ->(hdrs) { mw.call(service, handler, hdrs) { prev.call(hdrs) } }
|
|
486
509
|
end
|
|
510
|
+
chain.call(h)
|
|
511
|
+
ensure
|
|
512
|
+
Thread.current[:restate_outbound_handler_meta] = previous_meta
|
|
487
513
|
end
|
|
488
514
|
|
|
489
515
|
# ── Call target resolution ──
|
data/lib/restate/version.rb
CHANGED
data/lib/restate/vm.rb
CHANGED
|
@@ -223,6 +223,19 @@ module Restate
|
|
|
223
223
|
@vm.sys_cancel_invocation(invocation_id)
|
|
224
224
|
end
|
|
225
225
|
|
|
226
|
+
def sys_signal(name)
|
|
227
|
+
@vm.sys_signal(name)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def sys_complete_signal_success(invocation_id, name, value)
|
|
231
|
+
@vm.sys_complete_signal_success(invocation_id, name, value)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def sys_complete_signal_failure(invocation_id, name, failure)
|
|
235
|
+
native_failure = Internal::Failure.new(failure.code, failure.message, nil)
|
|
236
|
+
@vm.sys_complete_signal_failure(invocation_id, name, native_failure)
|
|
237
|
+
end
|
|
238
|
+
|
|
226
239
|
private
|
|
227
240
|
|
|
228
241
|
def map_do_progress(result)
|
data/lib/restate.rb
CHANGED
|
@@ -18,6 +18,7 @@ require_relative 'restate/endpoint'
|
|
|
18
18
|
require_relative 'restate/service_proxy'
|
|
19
19
|
require_relative 'restate/config'
|
|
20
20
|
require_relative 'restate/client'
|
|
21
|
+
require_relative 'restate/middleware/deadlock_detection'
|
|
21
22
|
require_relative 'restate/railtie' if defined?(Rails::Railtie)
|
|
22
23
|
|
|
23
24
|
# Restate Ruby SDK — build resilient applications with durable execution.
|
|
@@ -241,6 +242,23 @@ module Restate # rubocop:disable Metrics/ModuleLength
|
|
|
241
242
|
fetch_context!.reject_awakeable(awakeable_id, message, code: code)
|
|
242
243
|
end
|
|
243
244
|
|
|
245
|
+
# ── Signals ──
|
|
246
|
+
|
|
247
|
+
# Wait for a named signal addressed to this invocation. Returns a DurableFuture.
|
|
248
|
+
def signal(name, serde: JsonSerde)
|
|
249
|
+
fetch_context!.signal(name, serde: serde)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Send a success value to a named signal on another invocation.
|
|
253
|
+
def resolve_signal(invocation_id, name, payload, serde: JsonSerde)
|
|
254
|
+
fetch_context!.resolve_signal(invocation_id, name, payload, serde: serde)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Send a terminal failure to a named signal on another invocation.
|
|
258
|
+
def reject_signal(invocation_id, name, message, code: 500)
|
|
259
|
+
fetch_context!.reject_signal(invocation_id, name, message, code: code)
|
|
260
|
+
end
|
|
261
|
+
|
|
244
262
|
# ── Promises (Workflow only) ──
|
|
245
263
|
|
|
246
264
|
# Get a durable promise value, blocking until resolved.
|
data/sig/restate.rbs
CHANGED
|
@@ -44,6 +44,12 @@ module Restate
|
|
|
44
44
|
def self.resolve_awakeable: (String awakeable_id, untyped payload, ?serde: untyped) -> void
|
|
45
45
|
def self.reject_awakeable: (String awakeable_id, String message, ?code: Integer) -> void
|
|
46
46
|
|
|
47
|
+
# ── Signals ──
|
|
48
|
+
|
|
49
|
+
def self.signal: (String name, ?serde: untyped) -> DurableFuture
|
|
50
|
+
def self.resolve_signal: (String invocation_id, String name, untyped payload, ?serde: untyped) -> void
|
|
51
|
+
def self.reject_signal: (String invocation_id, String name, String message, ?code: Integer) -> void
|
|
52
|
+
|
|
47
53
|
# ── Promises ──
|
|
48
54
|
|
|
49
55
|
def self.promise: (String name, ?serde: untyped) -> untyped
|
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: 0.
|
|
4
|
+
version: 0.11.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-05-
|
|
11
|
+
date: 2026-05-12 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: async
|
|
@@ -64,6 +64,7 @@ files:
|
|
|
64
64
|
- lib/restate/errors.rb
|
|
65
65
|
- lib/restate/handler.rb
|
|
66
66
|
- lib/restate/introspection.rb
|
|
67
|
+
- lib/restate/middleware/deadlock_detection.rb
|
|
67
68
|
- lib/restate/railtie.rb
|
|
68
69
|
- lib/restate/serde.rb
|
|
69
70
|
- lib/restate/server.rb
|