restate-sdk 0.4.3-aarch64-linux
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 +7 -0
- data/Cargo.lock +1040 -0
- data/Cargo.toml +8 -0
- data/LICENSE +21 -0
- data/README.md +133 -0
- data/ext/restate_internal/Cargo.toml +16 -0
- data/ext/restate_internal/extconf.rb +4 -0
- data/ext/restate_internal/src/lib.rs +1094 -0
- data/lib/restate/3.2/restate_internal.so +0 -0
- data/lib/restate/3.3/restate_internal.so +0 -0
- data/lib/restate/3.4/restate_internal.so +0 -0
- data/lib/restate/4.0/restate_internal.so +0 -0
- data/lib/restate/context.rb +336 -0
- data/lib/restate/discovery.rb +150 -0
- data/lib/restate/durable_future.rb +131 -0
- data/lib/restate/endpoint.rb +69 -0
- data/lib/restate/errors.rb +60 -0
- data/lib/restate/handler.rb +51 -0
- data/lib/restate/serde.rb +313 -0
- data/lib/restate/server.rb +280 -0
- data/lib/restate/server_context.rb +812 -0
- data/lib/restate/service.rb +37 -0
- data/lib/restate/service_dsl.rb +243 -0
- data/lib/restate/testing.rb +197 -0
- data/lib/restate/version.rb +6 -0
- data/lib/restate/virtual_object.rb +58 -0
- data/lib/restate/vm.rb +325 -0
- data/lib/restate/workflow.rb +57 -0
- data/lib/restate.rb +130 -0
- data/lib/tapioca/dsl/compilers/restate.rb +45 -0
- metadata +133 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# rubocop:disable Metrics/ModuleLength,Metrics/ParameterLists,Style/EmptyMethod
|
|
5
|
+
module Restate
|
|
6
|
+
# Signals when the current invocation attempt has finished — either the handler
|
|
7
|
+
# completed, the connection was lost, or a transient error occurred.
|
|
8
|
+
#
|
|
9
|
+
# Use this to clean up attempt-scoped resources (open connections, temp files,
|
|
10
|
+
# etc.) that should not outlive the current attempt.
|
|
11
|
+
#
|
|
12
|
+
# Available via +ctx.request.attempt_finished_event+.
|
|
13
|
+
#
|
|
14
|
+
# @example Cancel a long-running HTTP call when the attempt finishes
|
|
15
|
+
# event = ctx.request.attempt_finished_event
|
|
16
|
+
# ctx.run('call-api') do
|
|
17
|
+
# # poll event.set? periodically, or pass it to your HTTP client
|
|
18
|
+
# end
|
|
19
|
+
class AttemptFinishedEvent
|
|
20
|
+
extend T::Sig
|
|
21
|
+
|
|
22
|
+
sig { void }
|
|
23
|
+
def initialize
|
|
24
|
+
@mutex = T.let(Mutex.new, Mutex)
|
|
25
|
+
@set = T.let(false, T::Boolean)
|
|
26
|
+
@waiters = T.let([], T::Array[Thread::Queue])
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Returns true if the attempt has finished.
|
|
30
|
+
sig { returns(T::Boolean) }
|
|
31
|
+
def set?
|
|
32
|
+
@set
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Blocks the current fiber/thread until the attempt finishes.
|
|
36
|
+
sig { void }
|
|
37
|
+
def wait
|
|
38
|
+
return if @set
|
|
39
|
+
|
|
40
|
+
waiter = T.let(nil, T.nilable(Thread::Queue))
|
|
41
|
+
@mutex.synchronize do
|
|
42
|
+
unless @set
|
|
43
|
+
waiter = Thread::Queue.new
|
|
44
|
+
@waiters << waiter
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
waiter&.pop
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Marks the event as set and wakes all waiters.
|
|
51
|
+
# Called internally by the SDK when the attempt ends.
|
|
52
|
+
sig { void }
|
|
53
|
+
def set!
|
|
54
|
+
@mutex.synchronize do
|
|
55
|
+
@set = true
|
|
56
|
+
@waiters.each { |w| w.push(true) }
|
|
57
|
+
@waiters.clear
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Request metadata available to handlers via +ctx.request+.
|
|
63
|
+
#
|
|
64
|
+
# @!attribute [r] id
|
|
65
|
+
# @return [String] the invocation ID
|
|
66
|
+
# @!attribute [r] headers
|
|
67
|
+
# @return [Hash{String => String}] request headers
|
|
68
|
+
# @!attribute [r] body
|
|
69
|
+
# @return [String] raw input bytes
|
|
70
|
+
# @!attribute [r] attempt_finished_event
|
|
71
|
+
# @return [AttemptFinishedEvent] signaled when this attempt ends
|
|
72
|
+
Request = Struct.new(:id, :headers, :body, :attempt_finished_event, keyword_init: true)
|
|
73
|
+
|
|
74
|
+
# Base context interface for all Restate handlers.
|
|
75
|
+
#
|
|
76
|
+
# Provides durable execution (+run+, +run_sync+), timers (+sleep+),
|
|
77
|
+
# service-to-service calls, awakeables, and request metadata.
|
|
78
|
+
#
|
|
79
|
+
# @see ObjectContext for VirtualObject handlers (adds state operations)
|
|
80
|
+
# @see WorkflowContext for Workflow handlers (adds promise operations)
|
|
81
|
+
module Context
|
|
82
|
+
extend T::Sig
|
|
83
|
+
extend T::Helpers
|
|
84
|
+
|
|
85
|
+
abstract!
|
|
86
|
+
|
|
87
|
+
# Execute a durable side effect. The block runs at most once; the result
|
|
88
|
+
# is journaled and replayed on retries.
|
|
89
|
+
#
|
|
90
|
+
# Pass +background: true+ to offload the block to a real OS Thread,
|
|
91
|
+
# keeping the fiber event loop responsive for CPU-intensive work.
|
|
92
|
+
sig do
|
|
93
|
+
abstract.params(
|
|
94
|
+
name: String, serde: T.untyped, retry_policy: T.nilable(RunRetryPolicy),
|
|
95
|
+
background: T::Boolean, action: T.proc.returns(T.untyped)
|
|
96
|
+
).returns(DurableFuture)
|
|
97
|
+
end
|
|
98
|
+
def run(name, serde: JsonSerde, retry_policy: nil, background: false, &action); end
|
|
99
|
+
|
|
100
|
+
# Convenience shortcut for +run(...).await+. Returns the result directly.
|
|
101
|
+
# Accepts all the same options as +run+, including +background: true+.
|
|
102
|
+
sig do
|
|
103
|
+
abstract.params(
|
|
104
|
+
name: String, serde: T.untyped, retry_policy: T.nilable(RunRetryPolicy),
|
|
105
|
+
background: T::Boolean, action: T.proc.returns(T.untyped)
|
|
106
|
+
).returns(T.untyped)
|
|
107
|
+
end
|
|
108
|
+
def run_sync(name, serde: JsonSerde, retry_policy: nil, background: false, &action); end
|
|
109
|
+
|
|
110
|
+
# Durable timer that survives handler restarts.
|
|
111
|
+
sig { params(seconds: Numeric).returns(DurableFuture) }
|
|
112
|
+
def sleep(seconds) # rubocop:disable Lint/UnusedMethodArgument
|
|
113
|
+
Kernel.raise NotImplementedError
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Durably call a handler on a Restate service.
|
|
117
|
+
sig do
|
|
118
|
+
abstract.params(
|
|
119
|
+
service: T.any(String, T::Class[T.anything]), handler: T.any(String, Symbol),
|
|
120
|
+
arg: T.untyped, key: T.nilable(String), idempotency_key: T.nilable(String),
|
|
121
|
+
headers: T.nilable(T::Hash[String, String]), input_serde: T.untyped, output_serde: T.untyped
|
|
122
|
+
).returns(DurableCallFuture)
|
|
123
|
+
end
|
|
124
|
+
def service_call(service, handler, arg, key: nil, idempotency_key: nil, headers: nil,
|
|
125
|
+
input_serde: NOT_SET, output_serde: NOT_SET)
|
|
126
|
+
end
|
|
127
|
+
# Fire-and-forget send to a Restate service handler.
|
|
128
|
+
sig do
|
|
129
|
+
abstract.params(
|
|
130
|
+
service: T.any(String, T::Class[T.anything]), handler: T.any(String, Symbol),
|
|
131
|
+
arg: T.untyped, key: T.nilable(String), delay: T.nilable(Numeric),
|
|
132
|
+
idempotency_key: T.nilable(String), headers: T.nilable(T::Hash[String, String]),
|
|
133
|
+
input_serde: T.untyped
|
|
134
|
+
).returns(SendHandle)
|
|
135
|
+
end
|
|
136
|
+
def service_send(service, handler, arg, key: nil, delay: nil, idempotency_key: nil,
|
|
137
|
+
headers: nil, input_serde: NOT_SET)
|
|
138
|
+
end
|
|
139
|
+
# Durably call a handler on a Restate virtual object.
|
|
140
|
+
sig do
|
|
141
|
+
abstract.params(
|
|
142
|
+
service: T.any(String, T::Class[T.anything]), handler: T.any(String, Symbol),
|
|
143
|
+
key: String, arg: T.untyped, idempotency_key: T.nilable(String),
|
|
144
|
+
headers: T.nilable(T::Hash[String, String]), input_serde: T.untyped, output_serde: T.untyped
|
|
145
|
+
).returns(DurableCallFuture)
|
|
146
|
+
end
|
|
147
|
+
def object_call(service, handler, key, arg, idempotency_key: nil, headers: nil,
|
|
148
|
+
input_serde: NOT_SET, output_serde: NOT_SET)
|
|
149
|
+
end
|
|
150
|
+
# Fire-and-forget send to a Restate virtual object handler.
|
|
151
|
+
sig do
|
|
152
|
+
abstract.params(
|
|
153
|
+
service: T.any(String, T::Class[T.anything]), handler: T.any(String, Symbol),
|
|
154
|
+
key: String, arg: T.untyped, delay: T.nilable(Numeric),
|
|
155
|
+
idempotency_key: T.nilable(String), headers: T.nilable(T::Hash[String, String]),
|
|
156
|
+
input_serde: T.untyped
|
|
157
|
+
).returns(SendHandle)
|
|
158
|
+
end
|
|
159
|
+
def object_send(service, handler, key, arg, delay: nil, idempotency_key: nil,
|
|
160
|
+
headers: nil, input_serde: NOT_SET)
|
|
161
|
+
end
|
|
162
|
+
# Durably call a handler on a Restate workflow.
|
|
163
|
+
sig do
|
|
164
|
+
abstract.params(
|
|
165
|
+
service: T.any(String, T::Class[T.anything]), handler: T.any(String, Symbol),
|
|
166
|
+
key: String, arg: T.untyped, idempotency_key: T.nilable(String),
|
|
167
|
+
headers: T.nilable(T::Hash[String, String]), input_serde: T.untyped, output_serde: T.untyped
|
|
168
|
+
).returns(DurableCallFuture)
|
|
169
|
+
end
|
|
170
|
+
def workflow_call(service, handler, key, arg, idempotency_key: nil, headers: nil,
|
|
171
|
+
input_serde: NOT_SET, output_serde: NOT_SET)
|
|
172
|
+
end
|
|
173
|
+
# Fire-and-forget send to a Restate workflow handler.
|
|
174
|
+
sig do
|
|
175
|
+
abstract.params(
|
|
176
|
+
service: T.any(String, T::Class[T.anything]), handler: T.any(String, Symbol),
|
|
177
|
+
key: String, arg: T.untyped, delay: T.nilable(Numeric),
|
|
178
|
+
idempotency_key: T.nilable(String), headers: T.nilable(T::Hash[String, String]),
|
|
179
|
+
input_serde: T.untyped
|
|
180
|
+
).returns(SendHandle)
|
|
181
|
+
end
|
|
182
|
+
def workflow_send(service, handler, key, arg, delay: nil, idempotency_key: nil,
|
|
183
|
+
headers: nil, input_serde: NOT_SET)
|
|
184
|
+
end
|
|
185
|
+
# Durably call a handler using raw bytes (no serialization).
|
|
186
|
+
sig do
|
|
187
|
+
abstract.params(
|
|
188
|
+
service: String, handler: String, arg: String,
|
|
189
|
+
key: T.nilable(String), idempotency_key: T.nilable(String),
|
|
190
|
+
headers: T.nilable(T::Hash[String, String])
|
|
191
|
+
).returns(DurableCallFuture)
|
|
192
|
+
end
|
|
193
|
+
def generic_call(service, handler, arg, key: nil, idempotency_key: nil, headers: nil); end
|
|
194
|
+
|
|
195
|
+
# Fire-and-forget send using raw bytes (no serialization).
|
|
196
|
+
sig do
|
|
197
|
+
abstract.params(
|
|
198
|
+
service: String, handler: String, arg: String,
|
|
199
|
+
key: T.nilable(String), delay: T.nilable(Numeric),
|
|
200
|
+
idempotency_key: T.nilable(String), headers: T.nilable(T::Hash[String, String])
|
|
201
|
+
).returns(SendHandle)
|
|
202
|
+
end
|
|
203
|
+
def generic_send(service, handler, arg, key: nil, delay: nil, idempotency_key: nil, headers: nil); end
|
|
204
|
+
|
|
205
|
+
# Create an awakeable for external callbacks.
|
|
206
|
+
# Returns [awakeable_id, DurableFuture].
|
|
207
|
+
sig { abstract.params(serde: T.untyped).returns([String, DurableFuture]) }
|
|
208
|
+
def awakeable(serde: JsonSerde); end
|
|
209
|
+
|
|
210
|
+
# Resolve an awakeable with a success value.
|
|
211
|
+
sig { abstract.params(awakeable_id: String, payload: T.untyped, serde: T.untyped).void }
|
|
212
|
+
def resolve_awakeable(awakeable_id, payload, serde: JsonSerde); end
|
|
213
|
+
|
|
214
|
+
# Reject an awakeable with a terminal failure.
|
|
215
|
+
sig { abstract.params(awakeable_id: String, message: String, code: Integer).void }
|
|
216
|
+
def reject_awakeable(awakeable_id, message, code: 500); end
|
|
217
|
+
|
|
218
|
+
# Request cancellation of another invocation.
|
|
219
|
+
sig { abstract.params(invocation_id: String).void }
|
|
220
|
+
def cancel_invocation(invocation_id); end
|
|
221
|
+
|
|
222
|
+
# Wait until any of the given futures completes.
|
|
223
|
+
# Returns [completed, remaining].
|
|
224
|
+
sig { abstract.params(futures: DurableFuture).returns([T::Array[DurableFuture], T::Array[DurableFuture]]) }
|
|
225
|
+
def wait_any(*futures); end
|
|
226
|
+
|
|
227
|
+
# Returns metadata about the current invocation.
|
|
228
|
+
sig { abstract.returns(Request) }
|
|
229
|
+
def request; end
|
|
230
|
+
|
|
231
|
+
# Returns the key for this virtual object or workflow invocation.
|
|
232
|
+
sig { abstract.returns(String) }
|
|
233
|
+
def key; end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Context interface for VirtualObject shared handlers (read-only state).
|
|
237
|
+
# Extends {Context} with +get+, +state_keys+, and +key+ — but no mutations.
|
|
238
|
+
module ObjectSharedContext
|
|
239
|
+
extend T::Sig
|
|
240
|
+
extend T::Helpers
|
|
241
|
+
|
|
242
|
+
abstract!
|
|
243
|
+
include Context
|
|
244
|
+
|
|
245
|
+
# Durably retrieve a state entry. Returns nil if unset.
|
|
246
|
+
sig { abstract.params(name: String, serde: T.untyped).returns(T.untyped) }
|
|
247
|
+
def get(name, serde: JsonSerde); end
|
|
248
|
+
|
|
249
|
+
# Durably retrieve a state entry, returning a DurableFuture instead of blocking.
|
|
250
|
+
sig { abstract.params(name: String, serde: T.untyped).returns(DurableFuture) }
|
|
251
|
+
def get_async(name, serde: JsonSerde); end
|
|
252
|
+
|
|
253
|
+
# List all state entry names.
|
|
254
|
+
sig { abstract.returns(T.untyped) }
|
|
255
|
+
def state_keys; end
|
|
256
|
+
|
|
257
|
+
# List all state entry names, returning a DurableFuture instead of blocking.
|
|
258
|
+
sig { abstract.returns(DurableFuture) }
|
|
259
|
+
def state_keys_async; end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Context interface for VirtualObject exclusive handlers (full state access).
|
|
263
|
+
# Extends {ObjectSharedContext} with mutating state operations.
|
|
264
|
+
module ObjectContext
|
|
265
|
+
extend T::Sig
|
|
266
|
+
extend T::Helpers
|
|
267
|
+
|
|
268
|
+
abstract!
|
|
269
|
+
include ObjectSharedContext
|
|
270
|
+
|
|
271
|
+
# Durably set a state entry.
|
|
272
|
+
sig { abstract.params(name: String, value: T.untyped, serde: T.untyped).void }
|
|
273
|
+
def set(name, value, serde: JsonSerde); end
|
|
274
|
+
|
|
275
|
+
# Durably remove a single state entry.
|
|
276
|
+
sig { abstract.params(name: String).void }
|
|
277
|
+
def clear(name); end
|
|
278
|
+
|
|
279
|
+
# Durably remove all state entries.
|
|
280
|
+
sig { abstract.void }
|
|
281
|
+
def clear_all; end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# Context interface for Workflow shared handlers (read-only state + promises).
|
|
285
|
+
# Extends {ObjectSharedContext} with durable promise operations.
|
|
286
|
+
module WorkflowSharedContext
|
|
287
|
+
extend T::Sig
|
|
288
|
+
extend T::Helpers
|
|
289
|
+
|
|
290
|
+
abstract!
|
|
291
|
+
include ObjectSharedContext
|
|
292
|
+
|
|
293
|
+
# Get a durable promise value, blocking until resolved.
|
|
294
|
+
sig { abstract.params(name: String, serde: T.untyped).returns(T.untyped) }
|
|
295
|
+
def promise(name, serde: JsonSerde); end
|
|
296
|
+
|
|
297
|
+
# Peek at a durable promise without blocking. Returns nil if not yet resolved.
|
|
298
|
+
sig { abstract.params(name: String, serde: T.untyped).returns(T.untyped) }
|
|
299
|
+
def peek_promise(name, serde: JsonSerde); end
|
|
300
|
+
|
|
301
|
+
# Resolve a durable promise with a value.
|
|
302
|
+
sig { abstract.params(name: String, payload: T.untyped, serde: T.untyped).void }
|
|
303
|
+
def resolve_promise(name, payload, serde: JsonSerde); end
|
|
304
|
+
|
|
305
|
+
# Reject a durable promise with a terminal failure.
|
|
306
|
+
sig { abstract.params(name: String, message: String, code: Integer).void }
|
|
307
|
+
def reject_promise(name, message, code: 500); end
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# Context interface for Workflow main handler (full state + promises).
|
|
311
|
+
# Extends {ObjectContext} with durable promise operations.
|
|
312
|
+
module WorkflowContext
|
|
313
|
+
extend T::Sig
|
|
314
|
+
extend T::Helpers
|
|
315
|
+
|
|
316
|
+
abstract!
|
|
317
|
+
include ObjectContext
|
|
318
|
+
|
|
319
|
+
# Get a durable promise value, blocking until resolved.
|
|
320
|
+
sig { abstract.params(name: String, serde: T.untyped).returns(T.untyped) }
|
|
321
|
+
def promise(name, serde: JsonSerde); end
|
|
322
|
+
|
|
323
|
+
# Peek at a durable promise without blocking. Returns nil if not yet resolved.
|
|
324
|
+
sig { abstract.params(name: String, serde: T.untyped).returns(T.untyped) }
|
|
325
|
+
def peek_promise(name, serde: JsonSerde); end
|
|
326
|
+
|
|
327
|
+
# Resolve a durable promise with a value.
|
|
328
|
+
sig { abstract.params(name: String, payload: T.untyped, serde: T.untyped).void }
|
|
329
|
+
def resolve_promise(name, payload, serde: JsonSerde); end
|
|
330
|
+
|
|
331
|
+
# Reject a durable promise with a terminal failure.
|
|
332
|
+
sig { abstract.params(name: String, message: String, code: Integer).void }
|
|
333
|
+
def reject_promise(name, message, code: 500); end
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
# rubocop:enable Metrics/ModuleLength,Metrics/ParameterLists,Style/EmptyMethod
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
module Restate
|
|
7
|
+
module Discovery # rubocop:disable Metrics/ModuleLength
|
|
8
|
+
extend T::Sig
|
|
9
|
+
|
|
10
|
+
PROTOCOL_MODES = T.let({
|
|
11
|
+
'bidi' => 'BIDI_STREAM',
|
|
12
|
+
'request_response' => 'REQUEST_RESPONSE'
|
|
13
|
+
}.freeze, T::Hash[String, String])
|
|
14
|
+
|
|
15
|
+
SERVICE_TYPES = T.let({
|
|
16
|
+
'service' => 'SERVICE',
|
|
17
|
+
'object' => 'VIRTUAL_OBJECT',
|
|
18
|
+
'workflow' => 'WORKFLOW'
|
|
19
|
+
}.freeze, T::Hash[String, String])
|
|
20
|
+
|
|
21
|
+
HANDLER_TYPES = T.let({
|
|
22
|
+
'exclusive' => 'EXCLUSIVE',
|
|
23
|
+
'shared' => 'SHARED',
|
|
24
|
+
'workflow' => 'WORKFLOW'
|
|
25
|
+
}.freeze, T::Hash[String, String])
|
|
26
|
+
|
|
27
|
+
module_function
|
|
28
|
+
|
|
29
|
+
# Generate the discovery JSON for the given endpoint.
|
|
30
|
+
sig { params(endpoint: Endpoint, _version: Integer, discovered_as: String).returns(String) }
|
|
31
|
+
def compute_discovery_json(endpoint, _version, discovered_as)
|
|
32
|
+
ep = compute_discovery(endpoint, discovered_as)
|
|
33
|
+
JSON.generate(ep, allow_nan: false)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Build the discovery hash for the endpoint.
|
|
37
|
+
sig { params(endpoint: Endpoint, discovered_as: String).returns(T::Hash[Symbol, T.untyped]) }
|
|
38
|
+
def compute_discovery(endpoint, discovered_as)
|
|
39
|
+
services = endpoint.services.values.map do |service|
|
|
40
|
+
build_service(service)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
protocol_mode = PROTOCOL_MODES.fetch(endpoint.protocol || discovered_as)
|
|
44
|
+
|
|
45
|
+
compact(
|
|
46
|
+
protocolMode: protocol_mode,
|
|
47
|
+
minProtocolVersion: 5,
|
|
48
|
+
maxProtocolVersion: 5,
|
|
49
|
+
services: services
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
sig { params(service: T.untyped).returns(T::Hash[Symbol, T.untyped]) }
|
|
54
|
+
def build_service(service) # rubocop:disable Metrics/AbcSize
|
|
55
|
+
service_type = SERVICE_TYPES.fetch(service.service_tag.kind)
|
|
56
|
+
|
|
57
|
+
handlers = service.handlers.values.map do |handler|
|
|
58
|
+
build_handler(handler)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
svc_name = service.service_name
|
|
62
|
+
|
|
63
|
+
result = compact(
|
|
64
|
+
name: svc_name,
|
|
65
|
+
ty: service_type,
|
|
66
|
+
handlers: handlers,
|
|
67
|
+
documentation: service.service_tag.description,
|
|
68
|
+
metadata: service.service_tag.metadata,
|
|
69
|
+
enableLazyState: service.lazy_state?,
|
|
70
|
+
inactivityTimeout: seconds_to_ms(service.svc_inactivity_timeout),
|
|
71
|
+
abortTimeout: seconds_to_ms(service.svc_abort_timeout),
|
|
72
|
+
journalRetention: seconds_to_ms(service.svc_journal_retention),
|
|
73
|
+
idempotencyRetention: seconds_to_ms(service.svc_idempotency_retention),
|
|
74
|
+
ingressPrivate: service.svc_ingress_private
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
policy = service.svc_invocation_retry_policy
|
|
78
|
+
merge_retry_policy!(result, policy) if policy
|
|
79
|
+
|
|
80
|
+
result
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
sig { params(handler: T.untyped).returns(T::Hash[Symbol, T.untyped]) }
|
|
84
|
+
def build_handler(handler) # rubocop:disable Metrics/AbcSize
|
|
85
|
+
ty = handler.kind ? HANDLER_TYPES.fetch(handler.kind) : nil
|
|
86
|
+
|
|
87
|
+
input_payload = {
|
|
88
|
+
required: false,
|
|
89
|
+
contentType: handler.handler_io.accept,
|
|
90
|
+
jsonSchema: handler.handler_io.input_serde.json_schema
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
output_payload = {
|
|
94
|
+
setContentTypeIfEmpty: false,
|
|
95
|
+
contentType: handler.handler_io.content_type,
|
|
96
|
+
jsonSchema: handler.handler_io.output_serde.json_schema
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
result = compact(
|
|
100
|
+
name: handler.name,
|
|
101
|
+
ty: ty,
|
|
102
|
+
input: compact(**input_payload),
|
|
103
|
+
output: compact(**output_payload),
|
|
104
|
+
enableLazyState: handler.enable_lazy_state,
|
|
105
|
+
documentation: handler.description,
|
|
106
|
+
metadata: handler.metadata,
|
|
107
|
+
inactivityTimeout: seconds_to_ms(handler.inactivity_timeout),
|
|
108
|
+
abortTimeout: seconds_to_ms(handler.abort_timeout),
|
|
109
|
+
journalRetention: seconds_to_ms(handler.journal_retention),
|
|
110
|
+
idempotencyRetention: seconds_to_ms(handler.idempotency_retention),
|
|
111
|
+
workflowCompletionRetention: seconds_to_ms(handler.workflow_completion_retention),
|
|
112
|
+
ingressPrivate: handler.ingress_private
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
merge_retry_policy!(result, handler.invocation_retry_policy) if handler.invocation_retry_policy
|
|
116
|
+
|
|
117
|
+
result
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Convert seconds to milliseconds (integer). Returns nil if input is nil.
|
|
121
|
+
sig { params(seconds: T.nilable(Numeric)).returns(T.nilable(Integer)) }
|
|
122
|
+
def seconds_to_ms(seconds)
|
|
123
|
+
return nil if seconds.nil?
|
|
124
|
+
|
|
125
|
+
(seconds * 1000).to_i
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Merge retry policy fields (flattened) into the target hash.
|
|
129
|
+
sig { params(target: T::Hash[Symbol, T.untyped], policy: T.nilable(T::Hash[Symbol, T.untyped])).void }
|
|
130
|
+
def merge_retry_policy!(target, policy) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity
|
|
131
|
+
return if policy.nil? || policy.empty?
|
|
132
|
+
|
|
133
|
+
target[:retryPolicyInitialInterval] = seconds_to_ms(policy[:initial_interval]) if policy[:initial_interval]
|
|
134
|
+
target[:retryPolicyMaxInterval] = seconds_to_ms(policy[:max_interval]) if policy[:max_interval]
|
|
135
|
+
target[:retryPolicyMaxAttempts] = policy[:max_attempts] if policy[:max_attempts]
|
|
136
|
+
target[:retryPolicyExponentiationFactor] = policy[:exponentiation_factor] if policy[:exponentiation_factor]
|
|
137
|
+
target[:retryPolicyOnMaxAttempts] = policy[:on_max_attempts].to_s.upcase if policy[:on_max_attempts]
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Remove nil values from a hash (non-recursive for top level, recursive for nested).
|
|
141
|
+
sig { params(kwargs: T.untyped).returns(T::Hash[Symbol, T.untyped]) }
|
|
142
|
+
def compact(**kwargs)
|
|
143
|
+
kwargs.each_with_object({}) do |(k, v), result|
|
|
144
|
+
next if v.nil?
|
|
145
|
+
|
|
146
|
+
result[k] = v.is_a?(Hash) ? compact(**v) : v
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Restate
|
|
5
|
+
# A durable future wrapping a VM handle. Lazily resolves on first +await+ and caches the result.
|
|
6
|
+
# Returned by +ctx.run+ and +ctx.sleep+.
|
|
7
|
+
class DurableFuture
|
|
8
|
+
extend T::Sig
|
|
9
|
+
|
|
10
|
+
sig { returns(Integer) }
|
|
11
|
+
attr_reader :handle
|
|
12
|
+
|
|
13
|
+
sig { params(ctx: ServerContext, handle: Integer, serde: T.untyped).void }
|
|
14
|
+
def initialize(ctx, handle, serde: nil)
|
|
15
|
+
@ctx = T.let(ctx, ServerContext)
|
|
16
|
+
@handle = T.let(handle, Integer)
|
|
17
|
+
@serde = T.let(serde, T.untyped)
|
|
18
|
+
@resolved = T.let(false, T::Boolean)
|
|
19
|
+
@value = T.let(nil, T.untyped)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Block until the result is available and return it. Caches across calls.
|
|
23
|
+
#
|
|
24
|
+
# @return [Object] the deserialized result
|
|
25
|
+
sig { returns(T.untyped) }
|
|
26
|
+
def await
|
|
27
|
+
unless @resolved
|
|
28
|
+
raw = @ctx.resolve_handle(@handle)
|
|
29
|
+
@value = @serde ? @serde.deserialize(raw) : raw
|
|
30
|
+
@resolved = true
|
|
31
|
+
end
|
|
32
|
+
@value
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Check whether the future has completed (non-blocking).
|
|
36
|
+
#
|
|
37
|
+
# @return [Boolean]
|
|
38
|
+
sig { returns(T::Boolean) }
|
|
39
|
+
def completed?
|
|
40
|
+
@resolved || @ctx.completed?(@handle)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# A durable future for service/object/workflow calls.
|
|
45
|
+
# Adds +invocation_id+ and +cancel+ on top of DurableFuture.
|
|
46
|
+
# Returned by +ctx.service_call+, +ctx.object_call+, +ctx.workflow_call+.
|
|
47
|
+
class DurableCallFuture < DurableFuture
|
|
48
|
+
extend T::Sig
|
|
49
|
+
|
|
50
|
+
sig do
|
|
51
|
+
params(
|
|
52
|
+
ctx: ServerContext,
|
|
53
|
+
result_handle: Integer,
|
|
54
|
+
invocation_id_handle: Integer,
|
|
55
|
+
output_serde: T.untyped
|
|
56
|
+
).void
|
|
57
|
+
end
|
|
58
|
+
def initialize(ctx, result_handle, invocation_id_handle, output_serde:)
|
|
59
|
+
super(ctx, result_handle)
|
|
60
|
+
@invocation_id_handle = T.let(invocation_id_handle, Integer)
|
|
61
|
+
@output_serde = T.let(output_serde, T.untyped)
|
|
62
|
+
@invocation_id_resolved = T.let(false, T::Boolean)
|
|
63
|
+
@invocation_id_value = T.let(nil, T.untyped)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Block until the result is available and return it. Deserializes via +output_serde+.
|
|
67
|
+
sig { returns(T.untyped) }
|
|
68
|
+
def await
|
|
69
|
+
unless @resolved
|
|
70
|
+
raw = @ctx.resolve_handle(@handle)
|
|
71
|
+
@value = if raw.nil? || @output_serde.nil?
|
|
72
|
+
raw
|
|
73
|
+
else
|
|
74
|
+
@output_serde.deserialize(raw)
|
|
75
|
+
end
|
|
76
|
+
@resolved = true
|
|
77
|
+
end
|
|
78
|
+
@value
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Returns the invocation ID of the remote call. Lazily resolved.
|
|
82
|
+
#
|
|
83
|
+
# @return [String] the invocation ID
|
|
84
|
+
sig { returns(String) }
|
|
85
|
+
def invocation_id
|
|
86
|
+
unless @invocation_id_resolved
|
|
87
|
+
@invocation_id_value = @ctx.resolve_handle(@invocation_id_handle)
|
|
88
|
+
@invocation_id_resolved = true
|
|
89
|
+
end
|
|
90
|
+
T.must(@invocation_id_value)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Cancel the remote invocation.
|
|
94
|
+
sig { void }
|
|
95
|
+
def cancel
|
|
96
|
+
@ctx.cancel_invocation(invocation_id)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# A handle for fire-and-forget send operations.
|
|
101
|
+
# Returned by +ctx.service_send+, +ctx.object_send+, +ctx.workflow_send+.
|
|
102
|
+
class SendHandle
|
|
103
|
+
extend T::Sig
|
|
104
|
+
|
|
105
|
+
sig { params(ctx: ServerContext, invocation_id_handle: Integer).void }
|
|
106
|
+
def initialize(ctx, invocation_id_handle)
|
|
107
|
+
@ctx = T.let(ctx, ServerContext)
|
|
108
|
+
@invocation_id_handle = T.let(invocation_id_handle, Integer)
|
|
109
|
+
@invocation_id_resolved = T.let(false, T::Boolean)
|
|
110
|
+
@invocation_id_value = T.let(nil, T.untyped)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Returns the invocation ID of the sent call. Lazily resolved.
|
|
114
|
+
#
|
|
115
|
+
# @return [String] the invocation ID
|
|
116
|
+
sig { returns(String) }
|
|
117
|
+
def invocation_id
|
|
118
|
+
unless @invocation_id_resolved
|
|
119
|
+
@invocation_id_value = @ctx.resolve_handle(@invocation_id_handle)
|
|
120
|
+
@invocation_id_resolved = true
|
|
121
|
+
end
|
|
122
|
+
T.must(@invocation_id_value)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Cancel the remote invocation.
|
|
126
|
+
sig { void }
|
|
127
|
+
def cancel
|
|
128
|
+
@ctx.cancel_invocation(invocation_id)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Restate
|
|
5
|
+
# Container for registered services. Bind services here, then create the Rack app.
|
|
6
|
+
class Endpoint
|
|
7
|
+
extend T::Sig
|
|
8
|
+
|
|
9
|
+
sig { returns(T::Hash[String, T.untyped]) }
|
|
10
|
+
attr_reader :services
|
|
11
|
+
|
|
12
|
+
sig { returns(T::Array[String]) }
|
|
13
|
+
attr_reader :identity_keys
|
|
14
|
+
|
|
15
|
+
sig { returns(T.nilable(String)) }
|
|
16
|
+
attr_accessor :protocol
|
|
17
|
+
|
|
18
|
+
sig { void }
|
|
19
|
+
def initialize
|
|
20
|
+
@services = T.let({}, T::Hash[String, T.untyped])
|
|
21
|
+
@protocol = T.let(nil, T.nilable(String))
|
|
22
|
+
@identity_keys = T.let([], T::Array[String])
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Bind one or more services to this endpoint.
|
|
26
|
+
#
|
|
27
|
+
# @param svcs [Array<Class<Service>, Class<VirtualObject>, Class<Workflow>>] services to bind
|
|
28
|
+
# @return [self]
|
|
29
|
+
# @raise [ArgumentError] if a service with the same name is already bound
|
|
30
|
+
sig { params(svcs: T.untyped).returns(T.self_type) }
|
|
31
|
+
def bind(*svcs)
|
|
32
|
+
svcs.each do |svc|
|
|
33
|
+
svc_name = svc.service_name
|
|
34
|
+
raise ArgumentError, "Service #{svc_name} already exists" if @services.key?(svc_name)
|
|
35
|
+
|
|
36
|
+
@services[svc_name] = svc
|
|
37
|
+
end
|
|
38
|
+
self
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Force bidirectional streaming protocol.
|
|
42
|
+
sig { returns(T.self_type) }
|
|
43
|
+
def streaming_protocol
|
|
44
|
+
@protocol = 'bidi'
|
|
45
|
+
self
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Force request/response protocol.
|
|
49
|
+
sig { returns(T.self_type) }
|
|
50
|
+
def request_response_protocol
|
|
51
|
+
@protocol = 'request_response'
|
|
52
|
+
self
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Add an identity key for request verification.
|
|
56
|
+
sig { params(key: String).returns(T.self_type) }
|
|
57
|
+
def identity_key(key)
|
|
58
|
+
@identity_keys << key
|
|
59
|
+
self
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Build and return the Rack-compatible application.
|
|
63
|
+
sig { returns(T.untyped) }
|
|
64
|
+
def app
|
|
65
|
+
require_relative 'server'
|
|
66
|
+
Server.new(self)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|