restate-sdk 0.11.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/lib/restate/endpoint.rb +2 -2
- 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.rb +0 -1
- data/sig/restate.rbs +8 -2
- metadata +4 -3
- data/lib/restate/server_context.rb +0 -678
|
@@ -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.rb
CHANGED
|
@@ -11,7 +11,6 @@ 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'
|
data/sig/restate.rbs
CHANGED
|
@@ -185,8 +185,14 @@ module Restate
|
|
|
185
185
|
|
|
186
186
|
# ── Internal ──
|
|
187
187
|
|
|
188
|
-
|
|
189
|
-
|
|
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
|
|
190
196
|
end
|
|
191
197
|
|
|
192
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
|
|
@@ -68,7 +68,8 @@ files:
|
|
|
68
68
|
- lib/restate/railtie.rb
|
|
69
69
|
- lib/restate/serde.rb
|
|
70
70
|
- lib/restate/server.rb
|
|
71
|
-
- lib/restate/
|
|
71
|
+
- lib/restate/server/context.rb
|
|
72
|
+
- lib/restate/server/handler.rb
|
|
72
73
|
- lib/restate/service.rb
|
|
73
74
|
- lib/restate/service_dsl.rb
|
|
74
75
|
- lib/restate/service_proxy.rb
|