restate-sdk 0.6.0 → 0.7.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/README.md +16 -4
- data/ext/restate_internal/Cargo.toml +1 -1
- data/lib/restate/client.rb +181 -0
- data/lib/restate/config.rb +42 -0
- data/lib/restate/handler.rb +4 -3
- data/lib/restate/service.rb +22 -0
- data/lib/restate/service_dsl.rb +66 -2
- data/lib/restate/service_proxy.rb +84 -0
- data/lib/restate/version.rb +1 -1
- data/lib/restate/virtual_object.rb +24 -0
- data/lib/restate/workflow.rb +24 -0
- data/lib/restate.rb +297 -48
- data/lib/tapioca/dsl/compilers/restate.rb +4 -5
- data/rbi/restate-sdk.rbi +293 -18
- metadata +5 -2
data/lib/restate.rb
CHANGED
|
@@ -16,9 +16,21 @@ require_relative 'restate/server_context'
|
|
|
16
16
|
require_relative 'restate/durable_future'
|
|
17
17
|
require_relative 'restate/discovery'
|
|
18
18
|
require_relative 'restate/endpoint'
|
|
19
|
+
require_relative 'restate/service_proxy'
|
|
20
|
+
require_relative 'restate/config'
|
|
21
|
+
require_relative 'restate/client'
|
|
19
22
|
|
|
20
23
|
# Restate Ruby SDK — build resilient applications with durable execution.
|
|
21
|
-
|
|
24
|
+
#
|
|
25
|
+
# All handler-facing operations are available as module methods on +Restate+.
|
|
26
|
+
# Inside a handler, call +Restate.run_sync+, +Restate.sleep+, +Restate.get+,
|
|
27
|
+
# etc. directly — no context parameter needed.
|
|
28
|
+
#
|
|
29
|
+
# The context is stored in +Thread.current[]+ (fiber-scoped in Ruby 3.0+).
|
|
30
|
+
# We intentionally use +Thread.current[]+ rather than +Fiber[]+ (Ruby 3.2+)
|
|
31
|
+
# because +Thread.current[]+ is NOT inherited by child fibers, which prevents
|
|
32
|
+
# accidental context leaks when Async spawns child tasks for run blocks.
|
|
33
|
+
module Restate # rubocop:disable Metrics/ModuleLength
|
|
22
34
|
extend T::Sig
|
|
23
35
|
|
|
24
36
|
module_function
|
|
@@ -44,61 +56,42 @@ module Restate
|
|
|
44
56
|
ep
|
|
45
57
|
end
|
|
46
58
|
|
|
47
|
-
# ──
|
|
48
|
-
#
|
|
49
|
-
# The SDK passes the context as the first argument to every handler.
|
|
50
|
-
# It is also stored in fiber-local storage (Thread.current[], which is
|
|
51
|
-
# fiber-scoped in Ruby). These methods retrieve it with the appropriate
|
|
52
|
-
# type for IDE completion.
|
|
53
|
-
#
|
|
54
|
-
# Use these from nested helper methods that don't have +ctx+ in scope.
|
|
55
|
-
|
|
56
|
-
# Returns the current context for a Service handler.
|
|
57
|
-
# Raises if called outside a Restate handler.
|
|
58
|
-
#
|
|
59
|
-
# @return [Context]
|
|
60
|
-
sig { returns(Context) }
|
|
61
|
-
def current_context
|
|
62
|
-
fetch_context!
|
|
63
|
-
end
|
|
59
|
+
# ── Global configuration ──
|
|
64
60
|
|
|
65
|
-
#
|
|
66
|
-
# Raises if not inside a VirtualObject exclusive handler.
|
|
61
|
+
# Configure the SDK globally. Settings are used by +Restate.client+.
|
|
67
62
|
#
|
|
68
|
-
# @
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
63
|
+
# @example
|
|
64
|
+
# Restate.configure do |c|
|
|
65
|
+
# c.ingress_url = "http://localhost:8080"
|
|
66
|
+
# c.admin_url = "http://localhost:9070"
|
|
67
|
+
# end
|
|
68
|
+
sig { params(_block: T.proc.params(arg0: Config).void).void }
|
|
69
|
+
def configure(&_block)
|
|
70
|
+
yield config
|
|
72
71
|
end
|
|
73
72
|
|
|
74
|
-
# Returns the
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
sig { returns(ObjectSharedContext) }
|
|
80
|
-
def current_shared_context
|
|
81
|
-
fetch_context!(service_kind: 'object', handler_kind: 'shared')
|
|
73
|
+
# Returns the global configuration. Creates a default one on first access.
|
|
74
|
+
sig { returns(Config) }
|
|
75
|
+
def config
|
|
76
|
+
@config = T.let(@config, T.nilable(Config)) unless defined?(@config)
|
|
77
|
+
@config ||= Config.new
|
|
82
78
|
end
|
|
83
79
|
|
|
84
|
-
# Returns
|
|
85
|
-
#
|
|
80
|
+
# Returns a pre-configured Client using the global +config+.
|
|
81
|
+
# Creates a new Client on each call (stateless — safe to discard).
|
|
86
82
|
#
|
|
87
|
-
# @
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
83
|
+
# @example
|
|
84
|
+
# Restate.client.service(Greeter).greet("World")
|
|
85
|
+
# Restate.client.resolve_awakeable(id, payload)
|
|
86
|
+
# Restate.client.create_deployment("http://localhost:9080")
|
|
87
|
+
sig { returns(Client) }
|
|
88
|
+
def client
|
|
89
|
+
cfg = config
|
|
90
|
+
Client.new(ingress_url: cfg.ingress_url, admin_url: cfg.admin_url,
|
|
91
|
+
ingress_headers: cfg.ingress_headers, admin_headers: cfg.admin_headers)
|
|
91
92
|
end
|
|
92
93
|
|
|
93
|
-
#
|
|
94
|
-
# Read-only state: +get+ and +state_keys+ only, no +set+/+clear+.
|
|
95
|
-
# Raises if not inside a Workflow shared handler.
|
|
96
|
-
#
|
|
97
|
-
# @return [WorkflowSharedContext]
|
|
98
|
-
sig { returns(WorkflowSharedContext) }
|
|
99
|
-
def current_shared_workflow_context
|
|
100
|
-
fetch_context!(service_kind: 'workflow', handler_kind: 'shared')
|
|
101
|
-
end
|
|
94
|
+
# ── Context accessor (internal) ──
|
|
102
95
|
|
|
103
96
|
# @!visibility private
|
|
104
97
|
sig do
|
|
@@ -108,7 +101,7 @@ module Restate
|
|
|
108
101
|
ctx = Thread.current[:restate_context]
|
|
109
102
|
unless ctx
|
|
110
103
|
Kernel.raise 'Not inside a Restate handler. ' \
|
|
111
|
-
'
|
|
104
|
+
'Restate.* methods can only be called during handler execution.'
|
|
112
105
|
end
|
|
113
106
|
|
|
114
107
|
if service_kind
|
|
@@ -127,4 +120,260 @@ module Restate
|
|
|
127
120
|
|
|
128
121
|
T.cast(ctx, ServerContext)
|
|
129
122
|
end
|
|
123
|
+
|
|
124
|
+
# ── Durable execution ──
|
|
125
|
+
|
|
126
|
+
# Execute a durable side effect. The block runs at most once; the result
|
|
127
|
+
# is journaled and replayed on retries. Returns a DurableFuture.
|
|
128
|
+
sig do
|
|
129
|
+
params(name: String, serde: T.untyped, retry_policy: T.nilable(RunRetryPolicy),
|
|
130
|
+
background: T::Boolean, action: T.proc.returns(T.untyped)).returns(DurableFuture)
|
|
131
|
+
end
|
|
132
|
+
def run(name, serde: JsonSerde, retry_policy: nil, background: false, &action)
|
|
133
|
+
fetch_context!.run(name, serde: serde, retry_policy: retry_policy, background: background, &action)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Convenience shortcut for +run(...).await+. Returns the result directly.
|
|
137
|
+
sig do
|
|
138
|
+
params(name: String, serde: T.untyped, retry_policy: T.nilable(RunRetryPolicy),
|
|
139
|
+
background: T::Boolean, action: T.proc.returns(T.untyped)).returns(T.untyped)
|
|
140
|
+
end
|
|
141
|
+
def run_sync(name, serde: JsonSerde, retry_policy: nil, background: false, &action)
|
|
142
|
+
fetch_context!.run_sync(name, serde: serde, retry_policy: retry_policy, background: background, &action)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Durable timer that survives handler restarts.
|
|
146
|
+
sig { params(seconds: Numeric).returns(DurableFuture) }
|
|
147
|
+
def sleep(seconds)
|
|
148
|
+
fetch_context!.sleep(seconds)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# ── State operations (VirtualObject / Workflow) ──
|
|
152
|
+
|
|
153
|
+
# Durably retrieve a state entry. Returns nil if unset.
|
|
154
|
+
sig { params(name: String, serde: T.untyped).returns(T.untyped) }
|
|
155
|
+
def get(name, serde: JsonSerde)
|
|
156
|
+
fetch_context!.get(name, serde: serde)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Durably retrieve a state entry, returning a DurableFuture instead of blocking.
|
|
160
|
+
sig { params(name: String, serde: T.untyped).returns(DurableFuture) }
|
|
161
|
+
def get_async(name, serde: JsonSerde)
|
|
162
|
+
fetch_context!.get_async(name, serde: serde)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Durably set a state entry.
|
|
166
|
+
sig { params(name: String, value: T.untyped, serde: T.untyped).void }
|
|
167
|
+
def set(name, value, serde: JsonSerde)
|
|
168
|
+
fetch_context!.set(name, value, serde: serde)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Durably remove a single state entry.
|
|
172
|
+
sig { params(name: String).void }
|
|
173
|
+
def clear(name)
|
|
174
|
+
fetch_context!.clear(name)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Durably remove all state entries.
|
|
178
|
+
sig { void }
|
|
179
|
+
def clear_all
|
|
180
|
+
fetch_context!.clear_all
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# List all state entry names.
|
|
184
|
+
sig { returns(T.untyped) }
|
|
185
|
+
def state_keys
|
|
186
|
+
fetch_context!.state_keys
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# List all state entry names, returning a DurableFuture.
|
|
190
|
+
sig { returns(DurableFuture) }
|
|
191
|
+
def state_keys_async
|
|
192
|
+
fetch_context!.state_keys_async
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# ── Service communication ──
|
|
196
|
+
|
|
197
|
+
# Durably call a handler on a Restate service.
|
|
198
|
+
sig do
|
|
199
|
+
params(service: T.any(String, T::Class[T.anything]), handler: T.any(String, Symbol),
|
|
200
|
+
arg: T.untyped, key: T.nilable(String), idempotency_key: T.nilable(String),
|
|
201
|
+
headers: T.nilable(T::Hash[String, String]),
|
|
202
|
+
input_serde: T.untyped, output_serde: T.untyped).returns(DurableCallFuture)
|
|
203
|
+
end
|
|
204
|
+
def service_call(service, handler, arg, key: nil, idempotency_key: nil, headers: nil,
|
|
205
|
+
input_serde: NOT_SET, output_serde: NOT_SET)
|
|
206
|
+
ctx = fetch_context!
|
|
207
|
+
ctx.service_call(service, handler, arg, key: key, idempotency_key: idempotency_key,
|
|
208
|
+
headers: headers, input_serde: input_serde, output_serde: output_serde)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Fire-and-forget send to a Restate service handler.
|
|
212
|
+
sig do
|
|
213
|
+
params(service: T.any(String, T::Class[T.anything]), handler: T.any(String, Symbol),
|
|
214
|
+
arg: T.untyped, key: T.nilable(String), delay: T.nilable(Numeric),
|
|
215
|
+
idempotency_key: T.nilable(String), headers: T.nilable(T::Hash[String, String]),
|
|
216
|
+
input_serde: T.untyped).returns(SendHandle)
|
|
217
|
+
end
|
|
218
|
+
def service_send(service, handler, arg, key: nil, delay: nil, idempotency_key: nil,
|
|
219
|
+
headers: nil, input_serde: NOT_SET)
|
|
220
|
+
ctx = fetch_context!
|
|
221
|
+
ctx.service_send(service, handler, arg, key: key, delay: delay, idempotency_key: idempotency_key,
|
|
222
|
+
headers: headers, input_serde: input_serde)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Durably call a handler on a Restate virtual object.
|
|
226
|
+
sig do
|
|
227
|
+
params(service: T.any(String, T::Class[T.anything]), handler: T.any(String, Symbol),
|
|
228
|
+
key: String, arg: T.untyped, idempotency_key: T.nilable(String),
|
|
229
|
+
headers: T.nilable(T::Hash[String, String]),
|
|
230
|
+
input_serde: T.untyped, output_serde: T.untyped).returns(DurableCallFuture)
|
|
231
|
+
end
|
|
232
|
+
def object_call(service, handler, key, arg, idempotency_key: nil, headers: nil,
|
|
233
|
+
input_serde: NOT_SET, output_serde: NOT_SET)
|
|
234
|
+
ctx = fetch_context!
|
|
235
|
+
ctx.object_call(service, handler, key, arg, idempotency_key: idempotency_key,
|
|
236
|
+
headers: headers, input_serde: input_serde, output_serde: output_serde)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Fire-and-forget send to a Restate virtual object handler.
|
|
240
|
+
sig do
|
|
241
|
+
params(service: T.any(String, T::Class[T.anything]), handler: T.any(String, Symbol),
|
|
242
|
+
key: String, arg: T.untyped, delay: T.nilable(Numeric),
|
|
243
|
+
idempotency_key: T.nilable(String), headers: T.nilable(T::Hash[String, String]),
|
|
244
|
+
input_serde: T.untyped).returns(SendHandle)
|
|
245
|
+
end
|
|
246
|
+
def object_send(service, handler, key, arg, delay: nil, idempotency_key: nil,
|
|
247
|
+
headers: nil, input_serde: NOT_SET)
|
|
248
|
+
ctx = fetch_context!
|
|
249
|
+
ctx.object_send(service, handler, key, arg, delay: delay, idempotency_key: idempotency_key,
|
|
250
|
+
headers: headers, input_serde: input_serde)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Durably call a handler on a Restate workflow.
|
|
254
|
+
sig do
|
|
255
|
+
params(service: T.any(String, T::Class[T.anything]), handler: T.any(String, Symbol),
|
|
256
|
+
key: String, arg: T.untyped, idempotency_key: T.nilable(String),
|
|
257
|
+
headers: T.nilable(T::Hash[String, String]),
|
|
258
|
+
input_serde: T.untyped, output_serde: T.untyped).returns(DurableCallFuture)
|
|
259
|
+
end
|
|
260
|
+
def workflow_call(service, handler, key, arg, idempotency_key: nil, headers: nil,
|
|
261
|
+
input_serde: NOT_SET, output_serde: NOT_SET)
|
|
262
|
+
ctx = fetch_context!
|
|
263
|
+
ctx.workflow_call(service, handler, key, arg,
|
|
264
|
+
idempotency_key: idempotency_key, headers: headers,
|
|
265
|
+
input_serde: input_serde, output_serde: output_serde)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Fire-and-forget send to a Restate workflow handler.
|
|
269
|
+
sig do
|
|
270
|
+
params(service: T.any(String, T::Class[T.anything]), handler: T.any(String, Symbol),
|
|
271
|
+
key: String, arg: T.untyped, delay: T.nilable(Numeric),
|
|
272
|
+
idempotency_key: T.nilable(String), headers: T.nilable(T::Hash[String, String]),
|
|
273
|
+
input_serde: T.untyped).returns(SendHandle)
|
|
274
|
+
end
|
|
275
|
+
def workflow_send(service, handler, key, arg, delay: nil, idempotency_key: nil,
|
|
276
|
+
headers: nil, input_serde: NOT_SET)
|
|
277
|
+
ctx = fetch_context!
|
|
278
|
+
ctx.workflow_send(service, handler, key, arg, delay: delay, idempotency_key: idempotency_key,
|
|
279
|
+
headers: headers, input_serde: input_serde)
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Durably call a handler using raw bytes (no serialization).
|
|
283
|
+
sig do
|
|
284
|
+
params(service: String, handler: String, arg: String,
|
|
285
|
+
key: T.nilable(String), idempotency_key: T.nilable(String),
|
|
286
|
+
headers: T.nilable(T::Hash[String, String])).returns(DurableCallFuture)
|
|
287
|
+
end
|
|
288
|
+
def generic_call(service, handler, arg, key: nil, idempotency_key: nil, headers: nil)
|
|
289
|
+
fetch_context!.generic_call(service, handler, arg, key: key,
|
|
290
|
+
idempotency_key: idempotency_key, headers: headers)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# Fire-and-forget send using raw bytes (no serialization).
|
|
294
|
+
sig do
|
|
295
|
+
params(service: String, handler: String, arg: String,
|
|
296
|
+
key: T.nilable(String), delay: T.nilable(Numeric),
|
|
297
|
+
idempotency_key: T.nilable(String), headers: T.nilable(T::Hash[String, String])).returns(SendHandle)
|
|
298
|
+
end
|
|
299
|
+
def generic_send(service, handler, arg, key: nil, delay: nil, idempotency_key: nil, headers: nil)
|
|
300
|
+
fetch_context!.generic_send(service, handler, arg, key: key, delay: delay,
|
|
301
|
+
idempotency_key: idempotency_key, headers: headers)
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# ── Awakeables ──
|
|
305
|
+
|
|
306
|
+
# Create an awakeable for external callbacks. Returns [awakeable_id, DurableFuture].
|
|
307
|
+
sig { params(serde: T.untyped).returns([String, DurableFuture]) }
|
|
308
|
+
def awakeable(serde: JsonSerde)
|
|
309
|
+
fetch_context!.awakeable(serde: serde)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# Resolve an awakeable with a success value.
|
|
313
|
+
sig { params(awakeable_id: String, payload: T.untyped, serde: T.untyped).void }
|
|
314
|
+
def resolve_awakeable(awakeable_id, payload, serde: JsonSerde)
|
|
315
|
+
fetch_context!.resolve_awakeable(awakeable_id, payload, serde: serde)
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Reject an awakeable with a terminal failure.
|
|
319
|
+
sig { params(awakeable_id: String, message: String, code: Integer).void }
|
|
320
|
+
def reject_awakeable(awakeable_id, message, code: 500)
|
|
321
|
+
fetch_context!.reject_awakeable(awakeable_id, message, code: code)
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# ── Promises (Workflow only) ──
|
|
325
|
+
|
|
326
|
+
# Get a durable promise value, blocking until resolved.
|
|
327
|
+
sig { params(name: String, serde: T.untyped).returns(T.untyped) }
|
|
328
|
+
def promise(name, serde: JsonSerde)
|
|
329
|
+
fetch_context!.promise(name, serde: serde)
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# Peek at a durable promise without blocking. Returns nil if not yet resolved.
|
|
333
|
+
sig { params(name: String, serde: T.untyped).returns(T.untyped) }
|
|
334
|
+
def peek_promise(name, serde: JsonSerde)
|
|
335
|
+
fetch_context!.peek_promise(name, serde: serde)
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# Resolve a durable promise with a value.
|
|
339
|
+
sig { params(name: String, payload: T.untyped, serde: T.untyped).void }
|
|
340
|
+
def resolve_promise(name, payload, serde: JsonSerde)
|
|
341
|
+
fetch_context!.resolve_promise(name, payload, serde: serde)
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# Reject a durable promise with a terminal failure.
|
|
345
|
+
sig { params(name: String, message: String, code: Integer).void }
|
|
346
|
+
def reject_promise(name, message, code: 500)
|
|
347
|
+
fetch_context!.reject_promise(name, message, code: code)
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# ── Futures ──
|
|
351
|
+
|
|
352
|
+
# Wait until any of the given futures completes. Returns [completed, remaining].
|
|
353
|
+
sig { params(futures: T::Array[DurableFuture]).returns([T::Array[DurableFuture], T::Array[DurableFuture]]) }
|
|
354
|
+
def wait_any(*futures)
|
|
355
|
+
T.unsafe(fetch_context!).wait_any(*futures)
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# ── Request metadata ──
|
|
359
|
+
|
|
360
|
+
# Returns metadata about the current invocation (id, headers, raw body).
|
|
361
|
+
sig { returns(T.untyped) }
|
|
362
|
+
def request
|
|
363
|
+
fetch_context!.request
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# Returns the key for this virtual object or workflow invocation.
|
|
367
|
+
sig { returns(String) }
|
|
368
|
+
def key
|
|
369
|
+
fetch_context!.key
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
# ── Invocation control ──
|
|
373
|
+
|
|
374
|
+
# Request cancellation of another invocation.
|
|
375
|
+
sig { params(invocation_id: String).void }
|
|
376
|
+
def cancel_invocation(invocation_id)
|
|
377
|
+
fetch_context!.cancel_invocation(invocation_id)
|
|
378
|
+
end
|
|
130
379
|
end
|
|
@@ -10,8 +10,8 @@ module Tapioca
|
|
|
10
10
|
module Compilers
|
|
11
11
|
# Generates Sorbet sigs for Restate handler methods.
|
|
12
12
|
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
13
|
+
# Handlers take 0 or 1 parameters (the input). Context is implicit
|
|
14
|
+
# via +Restate.*+ module methods.
|
|
15
15
|
#
|
|
16
16
|
# Usage:
|
|
17
17
|
# bundle exec tapioca dsl
|
|
@@ -58,9 +58,8 @@ module Tapioca
|
|
|
58
58
|
def decorate # rubocop:disable Metrics/MethodLength
|
|
59
59
|
root.create_path(constant) do |klass|
|
|
60
60
|
constant.handlers.each do |name, handler|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if handler.arity == 2
|
|
61
|
+
params = []
|
|
62
|
+
if handler.arity == 1
|
|
64
63
|
input_type = resolve_input_type(handler)
|
|
65
64
|
params << create_param('input', type: input_type)
|
|
66
65
|
end
|