restate-sdk 0.10.0 → 0.12.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/endpoint.rb +2 -2
- data/lib/restate/middleware/deadlock_detection.rb +216 -0
- data/lib/restate/server/context.rb +680 -0
- data/lib/restate/server/handler.rb +269 -0
- data/lib/restate/server.rb +7 -264
- data/lib/restate/version.rb +1 -1
- data/lib/restate/vm.rb +13 -0
- data/lib/restate.rb +18 -1
- data/sig/restate.rbs +14 -2
- metadata +5 -3
- data/lib/restate/server_context.rb +0 -652
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
# typed: ignore
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'async'
|
|
5
|
+
require 'async/queue'
|
|
6
|
+
require 'logger'
|
|
7
|
+
require_relative 'context'
|
|
8
|
+
|
|
9
|
+
module Restate
|
|
10
|
+
module Server
|
|
11
|
+
# Rack-compatible application that handles Restate protocol requests.
|
|
12
|
+
# Designed to work with Falcon for HTTP/2 bidirectional streaming.
|
|
13
|
+
#
|
|
14
|
+
# Routes:
|
|
15
|
+
# GET /discover → service manifest
|
|
16
|
+
# GET /health → health check
|
|
17
|
+
# POST /invoke/:service/:handler → handler invocation
|
|
18
|
+
class Handler
|
|
19
|
+
SDK_VERSION = Internal::SDK_VERSION
|
|
20
|
+
X_RESTATE_SERVER = "restate-sdk-ruby/#{SDK_VERSION}".freeze
|
|
21
|
+
|
|
22
|
+
LOGGER = Logger.new($stdout, progname: 'Restate::Server::Handler')
|
|
23
|
+
|
|
24
|
+
def initialize(endpoint)
|
|
25
|
+
@endpoint = endpoint
|
|
26
|
+
@identity_verifier = Internal::IdentityVerifier.new(endpoint.identity_keys)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Rack interface
|
|
30
|
+
def call(env)
|
|
31
|
+
path = env['PATH_INFO'] || '/'
|
|
32
|
+
parsed = parse_path(path)
|
|
33
|
+
|
|
34
|
+
case parsed[:type]
|
|
35
|
+
when :health
|
|
36
|
+
health_response
|
|
37
|
+
when :discover
|
|
38
|
+
handle_discover(env)
|
|
39
|
+
when :invocation
|
|
40
|
+
handle_invocation(env, parsed[:service], parsed[:handler])
|
|
41
|
+
else
|
|
42
|
+
not_found_response
|
|
43
|
+
end
|
|
44
|
+
rescue StandardError => e
|
|
45
|
+
LOGGER.error("Exception in Restate server: #{e.inspect}")
|
|
46
|
+
LOGGER.error(e.backtrace&.join("\n")) if e.backtrace
|
|
47
|
+
error_response(500, 'Internal server error')
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def parse_path(path)
|
|
53
|
+
segments = path.split('/').reject(&:empty?)
|
|
54
|
+
|
|
55
|
+
# Check for /invoke/:service/:handler
|
|
56
|
+
if segments.length >= 3
|
|
57
|
+
invoke_idx = segments.rindex('invoke')
|
|
58
|
+
if invoke_idx && segments.length > invoke_idx + 2
|
|
59
|
+
return {
|
|
60
|
+
type: :invocation,
|
|
61
|
+
service: segments[invoke_idx + 1],
|
|
62
|
+
handler: segments[invoke_idx + 2]
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
case segments.last
|
|
68
|
+
when 'health'
|
|
69
|
+
{ type: :health }
|
|
70
|
+
when 'discover'
|
|
71
|
+
{ type: :discover }
|
|
72
|
+
else
|
|
73
|
+
{ type: :unknown }
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def health_response
|
|
78
|
+
[200, { 'content-type' => 'application/json', 'x-restate-server' => X_RESTATE_SERVER }, ['{"status":"ok"}']]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def not_found_response
|
|
82
|
+
[404, { 'x-restate-server' => X_RESTATE_SERVER }, ['']]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def error_response(status, message)
|
|
86
|
+
[status, { 'content-type' => 'text/plain', 'x-restate-server' => X_RESTATE_SERVER }, [message]]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def handle_discover(env)
|
|
90
|
+
# Detect HTTP version for protocol mode
|
|
91
|
+
http_version = env['HTTP_VERSION'] || env['SERVER_PROTOCOL'] || 'HTTP/1.1'
|
|
92
|
+
discovered_as = http_version.include?('2') ? 'bidi' : 'request_response'
|
|
93
|
+
|
|
94
|
+
# Negotiate discovery protocol version from Accept header
|
|
95
|
+
accept = env['HTTP_ACCEPT'] || ''
|
|
96
|
+
version = negotiate_version(accept)
|
|
97
|
+
return error_response(415, "Unsupported discovery version: #{accept}") unless version
|
|
98
|
+
|
|
99
|
+
begin
|
|
100
|
+
json = Discovery.compute_discovery_json(@endpoint, version, discovered_as)
|
|
101
|
+
content_type = "application/vnd.restate.endpointmanifest.v#{version}+json"
|
|
102
|
+
[
|
|
103
|
+
200,
|
|
104
|
+
{
|
|
105
|
+
'content-type' => content_type,
|
|
106
|
+
'x-restate-server' => X_RESTATE_SERVER
|
|
107
|
+
},
|
|
108
|
+
[json]
|
|
109
|
+
]
|
|
110
|
+
rescue StandardError => e
|
|
111
|
+
error_response(500, "Error computing discovery: #{e.message}")
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def negotiate_version(accept)
|
|
116
|
+
if accept.include?('application/vnd.restate.endpointmanifest.v4+json')
|
|
117
|
+
4
|
|
118
|
+
elsif accept.include?('application/vnd.restate.endpointmanifest.v3+json')
|
|
119
|
+
3
|
|
120
|
+
elsif accept.include?('application/vnd.restate.endpointmanifest.v2+json')
|
|
121
|
+
2
|
|
122
|
+
elsif accept.empty?
|
|
123
|
+
2
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def handle_invocation(env, service_name, handler_name)
|
|
128
|
+
# Verify identity
|
|
129
|
+
request_headers = extract_headers(env)
|
|
130
|
+
path = env['PATH_INFO'] || '/'
|
|
131
|
+
begin
|
|
132
|
+
@identity_verifier.verify(request_headers, path)
|
|
133
|
+
rescue Internal::IdentityVerificationError
|
|
134
|
+
return [401, { 'x-restate-server' => X_RESTATE_SERVER }, ['']]
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Find the service and handler
|
|
138
|
+
service = @endpoint.services[service_name]
|
|
139
|
+
return not_found_response unless service
|
|
140
|
+
|
|
141
|
+
handler = service.handlers[handler_name]
|
|
142
|
+
return not_found_response unless handler
|
|
143
|
+
|
|
144
|
+
# Process the invocation with streaming
|
|
145
|
+
process_invocation(env, handler, request_headers)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def process_invocation(env, handler, request_headers)
|
|
149
|
+
vm = VMWrapper.new(request_headers)
|
|
150
|
+
status, response_headers = vm.get_response_head
|
|
151
|
+
|
|
152
|
+
# Streaming response body — output chunks are sent to Restate as they're
|
|
153
|
+
# produced. This is critical for BidiStream mode where the VM needs output
|
|
154
|
+
# acknowledged before it can make further progress.
|
|
155
|
+
output_queue = Async::Queue.new
|
|
156
|
+
send_output = ->(chunk) { output_queue.enqueue(chunk) }
|
|
157
|
+
|
|
158
|
+
# Input queue bridges the HTTP body reader and the handler's progress loop.
|
|
159
|
+
input_queue = Async::Queue.new
|
|
160
|
+
|
|
161
|
+
# Read request body chunks and feed to VM until ready to execute,
|
|
162
|
+
# then continue feeding remaining chunks via the input queue.
|
|
163
|
+
rack_input = env['rack.input']
|
|
164
|
+
ready = false
|
|
165
|
+
if rack_input
|
|
166
|
+
# Feed chunks until the VM has enough to start execution
|
|
167
|
+
while (chunk = rack_input.read_partial(16_384))
|
|
168
|
+
vm.notify_input(chunk.b) unless chunk.empty?
|
|
169
|
+
if vm.is_ready_to_execute
|
|
170
|
+
ready = true
|
|
171
|
+
break
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
vm.notify_input_closed unless ready
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
invocation = vm.sys_input
|
|
178
|
+
|
|
179
|
+
# Spawn a background task to continue reading remaining input
|
|
180
|
+
if ready
|
|
181
|
+
Async do
|
|
182
|
+
while (chunk = rack_input.read_partial(16_384))
|
|
183
|
+
input_queue.enqueue(chunk.b) unless chunk.empty?
|
|
184
|
+
end
|
|
185
|
+
input_queue.enqueue(:eof)
|
|
186
|
+
rescue StandardError => e
|
|
187
|
+
LOGGER.error("Input reader error: #{e.inspect}")
|
|
188
|
+
input_queue.enqueue(:disconnected)
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
context = Context.new(
|
|
193
|
+
vm: vm,
|
|
194
|
+
handler: handler,
|
|
195
|
+
invocation: invocation,
|
|
196
|
+
send_output: send_output,
|
|
197
|
+
input_queue: input_queue,
|
|
198
|
+
middleware: @endpoint.middleware,
|
|
199
|
+
outbound_middleware: @endpoint.outbound_middleware
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# Spawn the handler as an async task so the response body can stream
|
|
203
|
+
# output concurrently.
|
|
204
|
+
Async do
|
|
205
|
+
begin
|
|
206
|
+
context.enter
|
|
207
|
+
rescue DisconnectedError
|
|
208
|
+
# Client disconnected
|
|
209
|
+
rescue StandardError => e
|
|
210
|
+
LOGGER.error("Exception in handler: #{e.inspect}")
|
|
211
|
+
ensure
|
|
212
|
+
# Signal that the attempt is finished — wakes any waiters on
|
|
213
|
+
# ctx.request.attempt_finished_event and cancels pending background pool jobs.
|
|
214
|
+
context.on_attempt_finished
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Drain remaining output from VM
|
|
218
|
+
loop do
|
|
219
|
+
chunk = vm.take_output
|
|
220
|
+
break if chunk.nil? || chunk.empty?
|
|
221
|
+
|
|
222
|
+
output_queue.enqueue(chunk)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Signal end of output
|
|
226
|
+
output_queue.enqueue(nil)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
body = StreamingBody.new(output_queue)
|
|
230
|
+
|
|
231
|
+
merged_headers = response_headers.to_h { |pair| [pair[0], pair[1]] }
|
|
232
|
+
merged_headers['x-restate-server'] = X_RESTATE_SERVER
|
|
233
|
+
|
|
234
|
+
[status, merged_headers, body]
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Rack 3 streaming body that yields chunks from an Async::Queue.
|
|
238
|
+
# Terminates when nil is dequeued.
|
|
239
|
+
class StreamingBody
|
|
240
|
+
def initialize(queue)
|
|
241
|
+
@queue = queue
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def each
|
|
245
|
+
loop do
|
|
246
|
+
chunk = @queue.dequeue
|
|
247
|
+
break if chunk.nil?
|
|
248
|
+
|
|
249
|
+
yield chunk
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def extract_headers(env)
|
|
255
|
+
headers = []
|
|
256
|
+
env.each do |key, value|
|
|
257
|
+
next unless key.start_with?('HTTP_')
|
|
258
|
+
|
|
259
|
+
header_name = key.byteslice(5..).tr('_', '-').downcase!
|
|
260
|
+
headers << [header_name, value]
|
|
261
|
+
end
|
|
262
|
+
# Also include content-type and content-length if present
|
|
263
|
+
headers << ['content-type', env['CONTENT_TYPE']] if env['CONTENT_TYPE']
|
|
264
|
+
headers << ['content-length', env['CONTENT_LENGTH']] if env['CONTENT_LENGTH']
|
|
265
|
+
headers
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
data/lib/restate/server.rb
CHANGED
|
@@ -1,266 +1,9 @@
|
|
|
1
|
-
# typed: ignore
|
|
2
1
|
# frozen_string_literal: true
|
|
3
2
|
|
|
4
|
-
require
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
#
|
|
12
|
-
# Routes:
|
|
13
|
-
# GET /discover → service manifest
|
|
14
|
-
# GET /health → health check
|
|
15
|
-
# POST /invoke/:service/:handler → handler invocation
|
|
16
|
-
class Server
|
|
17
|
-
SDK_VERSION = Internal::SDK_VERSION
|
|
18
|
-
X_RESTATE_SERVER = "restate-sdk-ruby/#{SDK_VERSION}".freeze
|
|
19
|
-
|
|
20
|
-
LOGGER = Logger.new($stdout, progname: 'Restate::Server')
|
|
21
|
-
|
|
22
|
-
def initialize(endpoint)
|
|
23
|
-
@endpoint = endpoint
|
|
24
|
-
@identity_verifier = Internal::IdentityVerifier.new(endpoint.identity_keys)
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
# Rack interface
|
|
28
|
-
def call(env)
|
|
29
|
-
path = env['PATH_INFO'] || '/'
|
|
30
|
-
parsed = parse_path(path)
|
|
31
|
-
|
|
32
|
-
case parsed[:type]
|
|
33
|
-
when :health
|
|
34
|
-
health_response
|
|
35
|
-
when :discover
|
|
36
|
-
handle_discover(env)
|
|
37
|
-
when :invocation
|
|
38
|
-
handle_invocation(env, parsed[:service], parsed[:handler])
|
|
39
|
-
else
|
|
40
|
-
not_found_response
|
|
41
|
-
end
|
|
42
|
-
rescue StandardError => e
|
|
43
|
-
LOGGER.error("Exception in Restate server: #{e.inspect}")
|
|
44
|
-
LOGGER.error(e.backtrace&.join("\n")) if e.backtrace
|
|
45
|
-
error_response(500, 'Internal server error')
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
private
|
|
49
|
-
|
|
50
|
-
def parse_path(path)
|
|
51
|
-
segments = path.split('/').reject(&:empty?)
|
|
52
|
-
|
|
53
|
-
# Check for /invoke/:service/:handler
|
|
54
|
-
if segments.length >= 3
|
|
55
|
-
invoke_idx = segments.rindex('invoke')
|
|
56
|
-
if invoke_idx && segments.length > invoke_idx + 2
|
|
57
|
-
return {
|
|
58
|
-
type: :invocation,
|
|
59
|
-
service: segments[invoke_idx + 1],
|
|
60
|
-
handler: segments[invoke_idx + 2]
|
|
61
|
-
}
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
case segments.last
|
|
66
|
-
when 'health'
|
|
67
|
-
{ type: :health }
|
|
68
|
-
when 'discover'
|
|
69
|
-
{ type: :discover }
|
|
70
|
-
else
|
|
71
|
-
{ type: :unknown }
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def health_response
|
|
76
|
-
[200, { 'content-type' => 'application/json', 'x-restate-server' => X_RESTATE_SERVER }, ['{"status":"ok"}']]
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def not_found_response
|
|
80
|
-
[404, { 'x-restate-server' => X_RESTATE_SERVER }, ['']]
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def error_response(status, message)
|
|
84
|
-
[status, { 'content-type' => 'text/plain', 'x-restate-server' => X_RESTATE_SERVER }, [message]]
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def handle_discover(env)
|
|
88
|
-
# Detect HTTP version for protocol mode
|
|
89
|
-
http_version = env['HTTP_VERSION'] || env['SERVER_PROTOCOL'] || 'HTTP/1.1'
|
|
90
|
-
discovered_as = http_version.include?('2') ? 'bidi' : 'request_response'
|
|
91
|
-
|
|
92
|
-
# Negotiate discovery protocol version from Accept header
|
|
93
|
-
accept = env['HTTP_ACCEPT'] || ''
|
|
94
|
-
version = negotiate_version(accept)
|
|
95
|
-
return error_response(415, "Unsupported discovery version: #{accept}") unless version
|
|
96
|
-
|
|
97
|
-
begin
|
|
98
|
-
json = Discovery.compute_discovery_json(@endpoint, version, discovered_as)
|
|
99
|
-
content_type = "application/vnd.restate.endpointmanifest.v#{version}+json"
|
|
100
|
-
[
|
|
101
|
-
200,
|
|
102
|
-
{
|
|
103
|
-
'content-type' => content_type,
|
|
104
|
-
'x-restate-server' => X_RESTATE_SERVER
|
|
105
|
-
},
|
|
106
|
-
[json]
|
|
107
|
-
]
|
|
108
|
-
rescue StandardError => e
|
|
109
|
-
error_response(500, "Error computing discovery: #{e.message}")
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def negotiate_version(accept)
|
|
114
|
-
if accept.include?('application/vnd.restate.endpointmanifest.v4+json')
|
|
115
|
-
4
|
|
116
|
-
elsif accept.include?('application/vnd.restate.endpointmanifest.v3+json')
|
|
117
|
-
3
|
|
118
|
-
elsif accept.include?('application/vnd.restate.endpointmanifest.v2+json')
|
|
119
|
-
2
|
|
120
|
-
elsif accept.empty?
|
|
121
|
-
2
|
|
122
|
-
end
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
def handle_invocation(env, service_name, handler_name)
|
|
126
|
-
# Verify identity
|
|
127
|
-
request_headers = extract_headers(env)
|
|
128
|
-
path = env['PATH_INFO'] || '/'
|
|
129
|
-
begin
|
|
130
|
-
@identity_verifier.verify(request_headers, path)
|
|
131
|
-
rescue Internal::IdentityVerificationError
|
|
132
|
-
return [401, { 'x-restate-server' => X_RESTATE_SERVER }, ['']]
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
# Find the service and handler
|
|
136
|
-
service = @endpoint.services[service_name]
|
|
137
|
-
return not_found_response unless service
|
|
138
|
-
|
|
139
|
-
handler = service.handlers[handler_name]
|
|
140
|
-
return not_found_response unless handler
|
|
141
|
-
|
|
142
|
-
# Process the invocation with streaming
|
|
143
|
-
process_invocation(env, handler, request_headers)
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
def process_invocation(env, handler, request_headers)
|
|
147
|
-
vm = VMWrapper.new(request_headers)
|
|
148
|
-
status, response_headers = vm.get_response_head
|
|
149
|
-
|
|
150
|
-
# Streaming response body — output chunks are sent to Restate as they're
|
|
151
|
-
# produced. This is critical for BidiStream mode where the VM needs output
|
|
152
|
-
# acknowledged before it can make further progress.
|
|
153
|
-
output_queue = Async::Queue.new
|
|
154
|
-
send_output = ->(chunk) { output_queue.enqueue(chunk) }
|
|
155
|
-
|
|
156
|
-
# Input queue bridges the HTTP body reader and the handler's progress loop.
|
|
157
|
-
input_queue = Async::Queue.new
|
|
158
|
-
|
|
159
|
-
# Read request body chunks and feed to VM until ready to execute,
|
|
160
|
-
# then continue feeding remaining chunks via the input queue.
|
|
161
|
-
rack_input = env['rack.input']
|
|
162
|
-
ready = false
|
|
163
|
-
if rack_input
|
|
164
|
-
# Feed chunks until the VM has enough to start execution
|
|
165
|
-
while (chunk = rack_input.read_partial(16_384))
|
|
166
|
-
vm.notify_input(chunk.b) unless chunk.empty?
|
|
167
|
-
if vm.is_ready_to_execute
|
|
168
|
-
ready = true
|
|
169
|
-
break
|
|
170
|
-
end
|
|
171
|
-
end
|
|
172
|
-
vm.notify_input_closed unless ready
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
invocation = vm.sys_input
|
|
176
|
-
|
|
177
|
-
# Spawn a background task to continue reading remaining input
|
|
178
|
-
if ready
|
|
179
|
-
Async do
|
|
180
|
-
while (chunk = rack_input.read_partial(16_384))
|
|
181
|
-
input_queue.enqueue(chunk.b) unless chunk.empty?
|
|
182
|
-
end
|
|
183
|
-
input_queue.enqueue(:eof)
|
|
184
|
-
rescue StandardError => e
|
|
185
|
-
LOGGER.error("Input reader error: #{e.inspect}")
|
|
186
|
-
input_queue.enqueue(:disconnected)
|
|
187
|
-
end
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
context = ServerContext.new(
|
|
191
|
-
vm: vm,
|
|
192
|
-
handler: handler,
|
|
193
|
-
invocation: invocation,
|
|
194
|
-
send_output: send_output,
|
|
195
|
-
input_queue: input_queue,
|
|
196
|
-
middleware: @endpoint.middleware,
|
|
197
|
-
outbound_middleware: @endpoint.outbound_middleware
|
|
198
|
-
)
|
|
199
|
-
|
|
200
|
-
# Spawn the handler as an async task so the response body can stream
|
|
201
|
-
# output concurrently.
|
|
202
|
-
Async do
|
|
203
|
-
begin
|
|
204
|
-
context.enter
|
|
205
|
-
rescue DisconnectedError
|
|
206
|
-
# Client disconnected
|
|
207
|
-
rescue StandardError => e
|
|
208
|
-
LOGGER.error("Exception in handler: #{e.inspect}")
|
|
209
|
-
ensure
|
|
210
|
-
# Signal that the attempt is finished — wakes any waiters on
|
|
211
|
-
# ctx.request.attempt_finished_event and cancels pending background pool jobs.
|
|
212
|
-
context.on_attempt_finished
|
|
213
|
-
end
|
|
214
|
-
|
|
215
|
-
# Drain remaining output from VM
|
|
216
|
-
loop do
|
|
217
|
-
chunk = vm.take_output
|
|
218
|
-
break if chunk.nil? || chunk.empty?
|
|
219
|
-
|
|
220
|
-
output_queue.enqueue(chunk)
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
# Signal end of output
|
|
224
|
-
output_queue.enqueue(nil)
|
|
225
|
-
end
|
|
226
|
-
|
|
227
|
-
body = StreamingBody.new(output_queue)
|
|
228
|
-
|
|
229
|
-
merged_headers = response_headers.to_h { |pair| [pair[0], pair[1]] }
|
|
230
|
-
merged_headers['x-restate-server'] = X_RESTATE_SERVER
|
|
231
|
-
|
|
232
|
-
[status, merged_headers, body]
|
|
233
|
-
end
|
|
234
|
-
|
|
235
|
-
# Rack 3 streaming body that yields chunks from an Async::Queue.
|
|
236
|
-
# Terminates when nil is dequeued.
|
|
237
|
-
class StreamingBody
|
|
238
|
-
def initialize(queue)
|
|
239
|
-
@queue = queue
|
|
240
|
-
end
|
|
241
|
-
|
|
242
|
-
def each
|
|
243
|
-
loop do
|
|
244
|
-
chunk = @queue.dequeue
|
|
245
|
-
break if chunk.nil?
|
|
246
|
-
|
|
247
|
-
yield chunk
|
|
248
|
-
end
|
|
249
|
-
end
|
|
250
|
-
end
|
|
251
|
-
|
|
252
|
-
def extract_headers(env)
|
|
253
|
-
headers = []
|
|
254
|
-
env.each do |key, value|
|
|
255
|
-
next unless key.start_with?('HTTP_')
|
|
256
|
-
|
|
257
|
-
header_name = key.byteslice(5..).tr('_', '-').downcase!
|
|
258
|
-
headers << [header_name, value]
|
|
259
|
-
end
|
|
260
|
-
# Also include content-type and content-length if present
|
|
261
|
-
headers << ['content-type', env['CONTENT_TYPE']] if env['CONTENT_TYPE']
|
|
262
|
-
headers << ['content-length', env['CONTENT_LENGTH']] if env['CONTENT_LENGTH']
|
|
263
|
-
headers
|
|
264
|
-
end
|
|
265
|
-
end
|
|
266
|
-
end
|
|
3
|
+
# Convenience entry point: `require "restate/server"` loads the core SDK
|
|
4
|
+
# plus the server module (Rack app + async execution context).
|
|
5
|
+
#
|
|
6
|
+
# Use this when you want both core and server loaded eagerly, e.g.:
|
|
7
|
+
# gem "restate-sdk", require: "restate/server"
|
|
8
|
+
require_relative '../restate'
|
|
9
|
+
require_relative 'server/handler'
|
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
|
@@ -11,13 +11,13 @@ require_relative 'restate/service_dsl'
|
|
|
11
11
|
require_relative 'restate/service'
|
|
12
12
|
require_relative 'restate/virtual_object'
|
|
13
13
|
require_relative 'restate/workflow'
|
|
14
|
-
require_relative 'restate/server_context'
|
|
15
14
|
require_relative 'restate/durable_future'
|
|
16
15
|
require_relative 'restate/discovery'
|
|
17
16
|
require_relative 'restate/endpoint'
|
|
18
17
|
require_relative 'restate/service_proxy'
|
|
19
18
|
require_relative 'restate/config'
|
|
20
19
|
require_relative 'restate/client'
|
|
20
|
+
require_relative 'restate/middleware/deadlock_detection'
|
|
21
21
|
require_relative 'restate/railtie' if defined?(Rails::Railtie)
|
|
22
22
|
|
|
23
23
|
# Restate Ruby SDK — build resilient applications with durable execution.
|
|
@@ -241,6 +241,23 @@ module Restate # rubocop:disable Metrics/ModuleLength
|
|
|
241
241
|
fetch_context!.reject_awakeable(awakeable_id, message, code: code)
|
|
242
242
|
end
|
|
243
243
|
|
|
244
|
+
# ── Signals ──
|
|
245
|
+
|
|
246
|
+
# Wait for a named signal addressed to this invocation. Returns a DurableFuture.
|
|
247
|
+
def signal(name, serde: JsonSerde)
|
|
248
|
+
fetch_context!.signal(name, serde: serde)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Send a success value to a named signal on another invocation.
|
|
252
|
+
def resolve_signal(invocation_id, name, payload, serde: JsonSerde)
|
|
253
|
+
fetch_context!.resolve_signal(invocation_id, name, payload, serde: serde)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Send a terminal failure to a named signal on another invocation.
|
|
257
|
+
def reject_signal(invocation_id, name, message, code: 500)
|
|
258
|
+
fetch_context!.reject_signal(invocation_id, name, message, code: code)
|
|
259
|
+
end
|
|
260
|
+
|
|
244
261
|
# ── Promises (Workflow only) ──
|
|
245
262
|
|
|
246
263
|
# 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
|
|
@@ -179,8 +185,14 @@ module Restate
|
|
|
179
185
|
|
|
180
186
|
# ── Internal ──
|
|
181
187
|
|
|
182
|
-
|
|
183
|
-
|
|
188
|
+
module Server
|
|
189
|
+
class Handler
|
|
190
|
+
def initialize: (untyped endpoint) -> void
|
|
191
|
+
def call: (Hash[String, untyped] env) -> [Integer, Hash[String, String], untyped]
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
class Context
|
|
195
|
+
end
|
|
184
196
|
end
|
|
185
197
|
|
|
186
198
|
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: 0.
|
|
4
|
+
version: 0.12.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-13 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: async
|
|
@@ -64,10 +64,12 @@ 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
|
|
70
|
-
- lib/restate/
|
|
71
|
+
- lib/restate/server/context.rb
|
|
72
|
+
- lib/restate/server/handler.rb
|
|
71
73
|
- lib/restate/service.rb
|
|
72
74
|
- lib/restate/service_dsl.rb
|
|
73
75
|
- lib/restate/service_proxy.rb
|