restate-sdk 0.4.3
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/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 +127 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Restate
|
|
5
|
+
# A stateless Restate service.
|
|
6
|
+
#
|
|
7
|
+
# @example
|
|
8
|
+
# class Greeter < Restate::Service
|
|
9
|
+
# handler def greet(name)
|
|
10
|
+
# "Hello, #{name}!"
|
|
11
|
+
# end
|
|
12
|
+
# end
|
|
13
|
+
class Service
|
|
14
|
+
extend T::Sig
|
|
15
|
+
extend ServiceDSL
|
|
16
|
+
|
|
17
|
+
# Register a handler method on this service.
|
|
18
|
+
# Use as: +handler def my_method(arg)+ or +handler :my_method, input: String+
|
|
19
|
+
#
|
|
20
|
+
# @param method_name [Symbol] name of the method to register
|
|
21
|
+
# @param opts [Hash] handler options (+input:+, +output:+, +accept:+, +content_type:+)
|
|
22
|
+
# @return [Symbol] the method name
|
|
23
|
+
def self.handler(method_name = nil, **opts)
|
|
24
|
+
if method_name.is_a?(String)
|
|
25
|
+
raise ArgumentError,
|
|
26
|
+
"handler expects a Symbol (use `handler def #{method_name}(...)` or `handler :#{method_name}`)"
|
|
27
|
+
end
|
|
28
|
+
return method_name unless method_name.is_a?(Symbol)
|
|
29
|
+
|
|
30
|
+
_register_handler(method_name, **T.unsafe({ kind: nil, **opts }))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self._service_kind
|
|
34
|
+
'service'
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Restate
|
|
5
|
+
# Shared class-level DSL for defining Restate services via class inheritance.
|
|
6
|
+
#
|
|
7
|
+
# Extended into Service, VirtualObject, and Workflow. Provides the +handler+,
|
|
8
|
+
# +shared+, +main+, and +service_name+ class macros.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# class Counter < Restate::VirtualObject
|
|
12
|
+
# handler def add(addend)
|
|
13
|
+
# ctx = Restate.current_object_context
|
|
14
|
+
# old = ctx.get('counter') || 0
|
|
15
|
+
# ctx.set('counter', old + addend)
|
|
16
|
+
# old + addend
|
|
17
|
+
# end
|
|
18
|
+
# end
|
|
19
|
+
module ServiceDSL # rubocop:disable Metrics/ModuleLength
|
|
20
|
+
# Called when a subclass is created; initializes the handler registry.
|
|
21
|
+
def inherited(subclass) # rubocop:disable Metrics/MethodLength
|
|
22
|
+
super
|
|
23
|
+
subclass.instance_variable_set(:@_handler_registry, {})
|
|
24
|
+
subclass.instance_variable_set(:@_service_name, nil)
|
|
25
|
+
subclass.instance_variable_set(:@_handlers, nil)
|
|
26
|
+
subclass.instance_variable_set(:@_enable_lazy_state, nil)
|
|
27
|
+
subclass.instance_variable_set(:@_description, nil)
|
|
28
|
+
subclass.instance_variable_set(:@_metadata, nil)
|
|
29
|
+
subclass.instance_variable_set(:@_inactivity_timeout, nil)
|
|
30
|
+
subclass.instance_variable_set(:@_abort_timeout, nil)
|
|
31
|
+
subclass.instance_variable_set(:@_journal_retention, nil)
|
|
32
|
+
subclass.instance_variable_set(:@_idempotency_retention, nil)
|
|
33
|
+
subclass.instance_variable_set(:@_ingress_private, nil)
|
|
34
|
+
subclass.instance_variable_set(:@_invocation_retry_policy, nil)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Get or set the service name. Defaults to the unqualified class name.
|
|
38
|
+
#
|
|
39
|
+
# @param name [String, nil] when provided, sets the service name
|
|
40
|
+
# @return [String] the current service name
|
|
41
|
+
def service_name(name = nil)
|
|
42
|
+
if name
|
|
43
|
+
@_service_name = name
|
|
44
|
+
else
|
|
45
|
+
@_service_name || T.unsafe(self).name&.split('::')&.last
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Enable lazy state loading for all handlers in this service.
|
|
50
|
+
# When enabled, state is fetched on demand rather than pre-loaded.
|
|
51
|
+
#
|
|
52
|
+
# @param value [Boolean] whether to enable lazy state
|
|
53
|
+
def enable_lazy_state(value = true) # rubocop:disable Style/OptionalBooleanParameter
|
|
54
|
+
@_enable_lazy_state = value
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Set or get a human-readable description for this service.
|
|
58
|
+
#
|
|
59
|
+
# @param text [String, nil] when provided, sets the description
|
|
60
|
+
# @return [String, nil] the current description
|
|
61
|
+
def description(text = nil)
|
|
62
|
+
if text
|
|
63
|
+
@_description = text
|
|
64
|
+
else
|
|
65
|
+
@_description
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Set or get metadata for this service.
|
|
70
|
+
#
|
|
71
|
+
# @param hash [Hash, nil] when provided, sets the metadata
|
|
72
|
+
# @return [Hash, nil] the current metadata
|
|
73
|
+
def metadata(hash = nil)
|
|
74
|
+
if hash
|
|
75
|
+
@_metadata = hash
|
|
76
|
+
else
|
|
77
|
+
@_metadata
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Set the inactivity timeout (in seconds) for this service.
|
|
82
|
+
#
|
|
83
|
+
# @param seconds [Numeric] timeout in seconds
|
|
84
|
+
def inactivity_timeout(seconds)
|
|
85
|
+
@_inactivity_timeout = seconds
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Set the abort timeout (in seconds) for this service.
|
|
89
|
+
#
|
|
90
|
+
# @param seconds [Numeric] timeout in seconds
|
|
91
|
+
def abort_timeout(seconds)
|
|
92
|
+
@_abort_timeout = seconds
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Set the journal retention (in seconds) for this service.
|
|
96
|
+
#
|
|
97
|
+
# @param seconds [Numeric] retention in seconds
|
|
98
|
+
def journal_retention(seconds)
|
|
99
|
+
@_journal_retention = seconds
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Set the idempotency retention (in seconds) for this service.
|
|
103
|
+
#
|
|
104
|
+
# @param seconds [Numeric] retention in seconds
|
|
105
|
+
def idempotency_retention(seconds)
|
|
106
|
+
@_idempotency_retention = seconds
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Mark this service as private to the ingress.
|
|
110
|
+
#
|
|
111
|
+
# @param value [Boolean] whether the service is ingress-private
|
|
112
|
+
def ingress_private(value = true) # rubocop:disable Style/OptionalBooleanParameter
|
|
113
|
+
@_ingress_private = value
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Set the invocation retry policy for this service.
|
|
117
|
+
#
|
|
118
|
+
# @param initial_interval [Numeric, nil] initial retry interval in seconds
|
|
119
|
+
# @param max_interval [Numeric, nil] maximum retry interval in seconds
|
|
120
|
+
# @param max_attempts [Integer, nil] maximum number of retry attempts
|
|
121
|
+
# @param exponentiation_factor [Numeric, nil] backoff exponentiation factor
|
|
122
|
+
# @param on_max_attempts [Symbol, String, nil] action on max attempts (:pause or :kill)
|
|
123
|
+
def invocation_retry_policy(initial_interval: nil, max_interval: nil, max_attempts: nil,
|
|
124
|
+
exponentiation_factor: nil, on_max_attempts: nil)
|
|
125
|
+
@_invocation_retry_policy = {
|
|
126
|
+
initial_interval: initial_interval,
|
|
127
|
+
max_interval: max_interval,
|
|
128
|
+
max_attempts: max_attempts,
|
|
129
|
+
exponentiation_factor: exponentiation_factor,
|
|
130
|
+
on_max_attempts: on_max_attempts
|
|
131
|
+
}.compact
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Returns the ServiceTag for this class-based service.
|
|
135
|
+
# Subclasses must define +_service_kind+.
|
|
136
|
+
#
|
|
137
|
+
# @return [ServiceTag]
|
|
138
|
+
def service_tag
|
|
139
|
+
ServiceTag.new(kind: T.unsafe(self)._service_kind, name: service_name,
|
|
140
|
+
description: @_description, metadata: @_metadata)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Returns the service-level lazy state setting (nil if not set).
|
|
144
|
+
def lazy_state?
|
|
145
|
+
@_enable_lazy_state
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Returns the service-level inactivity timeout (nil if not set).
|
|
149
|
+
def svc_inactivity_timeout
|
|
150
|
+
@_inactivity_timeout
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Returns the service-level abort timeout (nil if not set).
|
|
154
|
+
def svc_abort_timeout
|
|
155
|
+
@_abort_timeout
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Returns the service-level journal retention (nil if not set).
|
|
159
|
+
def svc_journal_retention
|
|
160
|
+
@_journal_retention
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Returns the service-level idempotency retention (nil if not set).
|
|
164
|
+
def svc_idempotency_retention
|
|
165
|
+
@_idempotency_retention
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Returns the service-level ingress private setting (nil if not set).
|
|
169
|
+
def svc_ingress_private
|
|
170
|
+
@_ingress_private
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Returns the service-level invocation retry policy (nil if not set).
|
|
174
|
+
def svc_invocation_retry_policy
|
|
175
|
+
@_invocation_retry_policy
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Returns a hash of handler name (String) to Handler.
|
|
179
|
+
# Built lazily on first access and cached.
|
|
180
|
+
#
|
|
181
|
+
# @return [Hash{String => Handler}]
|
|
182
|
+
def handlers
|
|
183
|
+
@_handlers ||= _build_handlers # rubocop:disable Naming/MemoizedInstanceVariableName
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
private
|
|
187
|
+
|
|
188
|
+
# Store handler metadata for later building.
|
|
189
|
+
def _register_handler(method_name, kind:, **opts)
|
|
190
|
+
@_handlers = nil # invalidate cache
|
|
191
|
+
@_handler_registry[method_name.to_s] = { kind: kind, **opts }
|
|
192
|
+
method_name
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Build Handler structs from the registry using instance_method + bind.
|
|
196
|
+
def _build_handlers # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
|
197
|
+
tag = service_tag
|
|
198
|
+
result = {}
|
|
199
|
+
instance = T.unsafe(self).allocate
|
|
200
|
+
|
|
201
|
+
@_handler_registry.each do |name, meta| # rubocop:disable Metrics/BlockLength
|
|
202
|
+
input_serde = Serde.resolve(meta[:input])
|
|
203
|
+
output_serde = Serde.resolve(meta[:output])
|
|
204
|
+
|
|
205
|
+
handler_io = HandlerIO.new(
|
|
206
|
+
accept: meta[:accept] || 'application/json',
|
|
207
|
+
content_type: meta[:content_type] || 'application/json',
|
|
208
|
+
input_serde: input_serde,
|
|
209
|
+
output_serde: output_serde
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
um = T.unsafe(self).instance_method(name)
|
|
213
|
+
arity = um.arity.abs
|
|
214
|
+
unless [0, 1].include?(arity)
|
|
215
|
+
Kernel.raise ArgumentError, "handler '#{name}' must accept 0 or 1 parameters ([input]), got #{arity}"
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
bound = um.bind(instance)
|
|
219
|
+
|
|
220
|
+
result[name] = Handler.new(
|
|
221
|
+
service_tag: tag,
|
|
222
|
+
handler_io: handler_io,
|
|
223
|
+
kind: meta[:kind],
|
|
224
|
+
name: name,
|
|
225
|
+
callable: bound,
|
|
226
|
+
arity: arity,
|
|
227
|
+
enable_lazy_state: meta[:enable_lazy_state],
|
|
228
|
+
description: meta[:description],
|
|
229
|
+
metadata: meta[:metadata],
|
|
230
|
+
inactivity_timeout: meta[:inactivity_timeout],
|
|
231
|
+
abort_timeout: meta[:abort_timeout],
|
|
232
|
+
journal_retention: meta[:journal_retention],
|
|
233
|
+
idempotency_retention: meta[:idempotency_retention],
|
|
234
|
+
workflow_completion_retention: meta[:workflow_completion_retention],
|
|
235
|
+
ingress_private: meta[:ingress_private],
|
|
236
|
+
invocation_retry_policy: meta[:invocation_retry_policy]
|
|
237
|
+
)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
result
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# typed: false
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'restate'
|
|
5
|
+
require 'socket'
|
|
6
|
+
require 'net/http'
|
|
7
|
+
require 'json'
|
|
8
|
+
require 'uri'
|
|
9
|
+
require 'testcontainers'
|
|
10
|
+
|
|
11
|
+
module Restate
|
|
12
|
+
# Test harness for running Restate services with a real Restate server.
|
|
13
|
+
# Opt-in via `require "restate/testing"` (not loaded by default).
|
|
14
|
+
#
|
|
15
|
+
# Requires Docker and the testcontainers-core gem.
|
|
16
|
+
#
|
|
17
|
+
# Block-based (recommended):
|
|
18
|
+
# Restate::Testing.start(Greeter, Counter) do |env|
|
|
19
|
+
# env.ingress_url # => "http://localhost:32771"
|
|
20
|
+
# env.admin_url # => "http://localhost:32772"
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# Manual lifecycle (for RSpec before/after hooks):
|
|
24
|
+
# harness = Restate::Testing::RestateTestHarness.new(Greeter, Counter)
|
|
25
|
+
# harness.start
|
|
26
|
+
# harness.ingress_url
|
|
27
|
+
# harness.stop
|
|
28
|
+
module Testing
|
|
29
|
+
# Starts a Restate test environment with the given services.
|
|
30
|
+
# When a block is given, stops automatically on block exit.
|
|
31
|
+
# Without a block, returns the harness for manual lifecycle management.
|
|
32
|
+
def self.start(*services, **options)
|
|
33
|
+
harness = RestateTestHarness.new(*services, **options)
|
|
34
|
+
harness.start
|
|
35
|
+
|
|
36
|
+
if block_given?
|
|
37
|
+
begin
|
|
38
|
+
yield harness
|
|
39
|
+
ensure
|
|
40
|
+
harness.stop
|
|
41
|
+
end
|
|
42
|
+
else
|
|
43
|
+
harness
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Manages the lifecycle of an SDK server and a Restate container for testing.
|
|
48
|
+
class RestateTestHarness
|
|
49
|
+
attr_reader :ingress_url, :admin_url
|
|
50
|
+
|
|
51
|
+
# @param services [Array<Class>] Service classes to register.
|
|
52
|
+
# @param restate_image [String] Docker image for Restate server.
|
|
53
|
+
# @param always_replay [Boolean] Force replay on every suspension point.
|
|
54
|
+
# @param disable_retries [Boolean] Disable Restate retry policy.
|
|
55
|
+
def initialize(*services,
|
|
56
|
+
restate_image: 'docker.io/restatedev/restate:latest',
|
|
57
|
+
always_replay: false,
|
|
58
|
+
disable_retries: false)
|
|
59
|
+
@services = services
|
|
60
|
+
@restate_image = restate_image
|
|
61
|
+
@always_replay = always_replay
|
|
62
|
+
@disable_retries = disable_retries
|
|
63
|
+
@server_thread = nil
|
|
64
|
+
@container = nil
|
|
65
|
+
@port = nil
|
|
66
|
+
@ingress_url = nil
|
|
67
|
+
@admin_url = nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def start
|
|
71
|
+
@port = find_free_port
|
|
72
|
+
rack_app = Restate.endpoint(*@services).app
|
|
73
|
+
start_sdk_server(rack_app)
|
|
74
|
+
wait_for_tcp(@port)
|
|
75
|
+
start_restate_container
|
|
76
|
+
register_sdk
|
|
77
|
+
self
|
|
78
|
+
rescue StandardError => e
|
|
79
|
+
stop
|
|
80
|
+
raise e
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def stop
|
|
84
|
+
stop_restate_container
|
|
85
|
+
stop_sdk_server
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def find_free_port
|
|
91
|
+
server = TCPServer.new('0.0.0.0', 0)
|
|
92
|
+
port = server.addr[1]
|
|
93
|
+
server.close
|
|
94
|
+
port
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def start_sdk_server(rack_app)
|
|
98
|
+
require 'async'
|
|
99
|
+
require 'async/http/endpoint'
|
|
100
|
+
require 'falcon/server'
|
|
101
|
+
|
|
102
|
+
port = @port
|
|
103
|
+
ready = Queue.new
|
|
104
|
+
|
|
105
|
+
@server_thread = Thread.new do
|
|
106
|
+
Async do
|
|
107
|
+
endpoint = Async::HTTP::Endpoint.parse("http://0.0.0.0:#{port}")
|
|
108
|
+
middleware = Falcon::Server.middleware(rack_app, cache: false)
|
|
109
|
+
server = Falcon::Server.new(middleware, endpoint)
|
|
110
|
+
ready.push(true)
|
|
111
|
+
server.run
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
ready.pop
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def wait_for_tcp(port, timeout: 10)
|
|
119
|
+
deadline = Time.now + timeout
|
|
120
|
+
loop do
|
|
121
|
+
TCPSocket.new('127.0.0.1', port).close
|
|
122
|
+
return
|
|
123
|
+
rescue Errno::ECONNREFUSED, Errno::ECONNRESET
|
|
124
|
+
raise "SDK server failed to start on port #{port} within #{timeout}s" if Time.now > deadline
|
|
125
|
+
|
|
126
|
+
sleep 0.1
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def start_restate_container
|
|
131
|
+
env = {
|
|
132
|
+
'RESTATE_LOG_FILTER' => 'restate=info',
|
|
133
|
+
'RESTATE_BOOTSTRAP_NUM_PARTITIONS' => '1',
|
|
134
|
+
'RESTATE_DEFAULT_NUM_PARTITIONS' => '1',
|
|
135
|
+
'RESTATE_SHUTDOWN_TIMEOUT' => '10s',
|
|
136
|
+
'RESTATE_ROCKSDB_TOTAL_MEMORY_SIZE' => '32 MB',
|
|
137
|
+
'RESTATE_WORKER__INVOKER__IN_MEMORY_QUEUE_LENGTH_LIMIT' => '64',
|
|
138
|
+
'RESTATE_WORKER__INVOKER__INACTIVITY_TIMEOUT' => @always_replay ? '0s' : '10m',
|
|
139
|
+
'RESTATE_WORKER__INVOKER__ABORT_TIMEOUT' => '10m'
|
|
140
|
+
}
|
|
141
|
+
env['RESTATE_WORKER__INVOKER__RETRY_POLICY__TYPE'] = 'none' if @disable_retries
|
|
142
|
+
|
|
143
|
+
@container = RestateContainer.new(@restate_image)
|
|
144
|
+
@container.with_exposed_ports(8080, 9070)
|
|
145
|
+
@container.with_env(env)
|
|
146
|
+
@container.start
|
|
147
|
+
|
|
148
|
+
@container.wait_for_http(path: '/restate/health', container_port: 8080, status: 200, timeout: 30)
|
|
149
|
+
@container.wait_for_http(path: '/health', container_port: 9070, status: 200, timeout: 30)
|
|
150
|
+
|
|
151
|
+
@ingress_url = "http://#{@container.host}:#{@container.mapped_port(8080)}"
|
|
152
|
+
@admin_url = "http://#{@container.host}:#{@container.mapped_port(9070)}"
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def register_sdk
|
|
156
|
+
uri = URI("#{@admin_url}/deployments")
|
|
157
|
+
sdk_url = "http://host.docker.internal:#{@port}"
|
|
158
|
+
|
|
159
|
+
request = Net::HTTP::Post.new(uri)
|
|
160
|
+
request['Content-Type'] = 'application/json'
|
|
161
|
+
request.body = JSON.generate({ uri: sdk_url })
|
|
162
|
+
|
|
163
|
+
response = Net::HTTP.start(uri.hostname, uri.port) { |http| http.request(request) }
|
|
164
|
+
return if response.code.start_with?('2')
|
|
165
|
+
|
|
166
|
+
raise "Failed to register SDK at #{sdk_url}: #{response.code} #{response.body}"
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def stop_restate_container
|
|
170
|
+
return unless @container
|
|
171
|
+
|
|
172
|
+
@container.stop!
|
|
173
|
+
@container.remove(force: true)
|
|
174
|
+
rescue StandardError
|
|
175
|
+
# Ignore cleanup errors
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def stop_sdk_server
|
|
179
|
+
@server_thread&.kill
|
|
180
|
+
@server_thread&.join(5)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Testcontainers::DockerContainer subclass that adds ExtraHosts support
|
|
185
|
+
# so the Restate container can reach the host-bound SDK server.
|
|
186
|
+
class RestateContainer < Testcontainers::DockerContainer
|
|
187
|
+
private
|
|
188
|
+
|
|
189
|
+
def _container_create_options
|
|
190
|
+
options = super
|
|
191
|
+
options['HostConfig'] ||= {}
|
|
192
|
+
options['HostConfig']['ExtraHosts'] = ['host.docker.internal:host-gateway']
|
|
193
|
+
options
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Restate
|
|
5
|
+
# A keyed virtual object with durable state.
|
|
6
|
+
#
|
|
7
|
+
# @example
|
|
8
|
+
# class Counter < Restate::VirtualObject
|
|
9
|
+
# handler def add(addend)
|
|
10
|
+
# ctx = Restate.current_object_context
|
|
11
|
+
# old = ctx.get("count") || 0
|
|
12
|
+
# ctx.set("count", old + addend)
|
|
13
|
+
# old + addend
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# shared def get
|
|
17
|
+
# ctx = Restate.current_object_context
|
|
18
|
+
# ctx.get("count") || 0
|
|
19
|
+
# end
|
|
20
|
+
# end
|
|
21
|
+
class VirtualObject
|
|
22
|
+
extend T::Sig
|
|
23
|
+
extend ServiceDSL
|
|
24
|
+
|
|
25
|
+
# Register an exclusive handler. Use as: +handler def my_method(arg)+
|
|
26
|
+
#
|
|
27
|
+
# @param method_name [Symbol] name of the method to register
|
|
28
|
+
# @param kind [Symbol] concurrency mode (+:exclusive+ or +:shared+)
|
|
29
|
+
# @param opts [Hash] handler options (+input:+, +output:+, +accept:+, +content_type:+)
|
|
30
|
+
# @return [Symbol] the method name
|
|
31
|
+
def self.handler(method_name = nil, kind: :exclusive, **opts)
|
|
32
|
+
if method_name.is_a?(String)
|
|
33
|
+
raise ArgumentError,
|
|
34
|
+
"handler expects a Symbol (use `handler def #{method_name}(...)` or `handler :#{method_name}`)"
|
|
35
|
+
end
|
|
36
|
+
return method_name unless method_name.is_a?(Symbol)
|
|
37
|
+
|
|
38
|
+
_register_handler(method_name, **T.unsafe({ kind: kind.to_s, **opts }))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Register a shared (concurrent-access) handler.
|
|
42
|
+
#
|
|
43
|
+
# @param method_name [Symbol] name of the method to register
|
|
44
|
+
# @return [Symbol] the method name
|
|
45
|
+
def self.shared(method_name, **opts)
|
|
46
|
+
if method_name.is_a?(String)
|
|
47
|
+
raise ArgumentError,
|
|
48
|
+
"handler expects a Symbol (use `shared def #{method_name}(...)` or `shared :#{method_name}`)"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
_register_handler(method_name, **T.unsafe({ kind: 'shared', **opts }))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self._service_kind
|
|
55
|
+
'object'
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|