restate-sdk 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Cargo.lock +1 -1
- data/README.md +16 -4
- data/ext/restate_internal/Cargo.toml +1 -1
- data/lib/restate/client.rb +181 -0
- data/lib/restate/config.rb +42 -0
- data/lib/restate/handler.rb +4 -3
- data/lib/restate/service.rb +22 -0
- data/lib/restate/service_dsl.rb +66 -2
- data/lib/restate/service_proxy.rb +84 -0
- data/lib/restate/version.rb +1 -1
- data/lib/restate/virtual_object.rb +24 -0
- data/lib/restate/workflow.rb +24 -0
- data/lib/restate.rb +297 -48
- data/lib/tapioca/dsl/compilers/restate.rb +4 -5
- data/rbi/restate-sdk.rbi +293 -18
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6c57bd9821f10bb65901d7da4ad7f6a38f9e64f81f36bd6eece1f2a9efcb7d57
|
|
4
|
+
data.tar.gz: c924c4d0f17f82012edaafba7d9867b2f78f2aecdd6fecc0a8f17cbd84c5f89a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0153b6c412588938d243b1d806af7a4e38d9467a21296fd299b9a6c1da77296a208009a82ce264f7d2e6e722a03fcd7b33fa1e77525c736dde7f0286512ece83
|
|
7
|
+
data.tar.gz: 4218cc6a283aa5ddf29eaed2ffd9935769a098e58fa98d476687509da45c9ddf4e4bfc871b873cfe234c1239b44f4d4ad6810975e89b61e90017463b0a0bf3d7
|
data/Cargo.lock
CHANGED
data/README.md
CHANGED
|
@@ -13,8 +13,20 @@
|
|
|
13
13
|
require 'restate'
|
|
14
14
|
|
|
15
15
|
class Greeter < Restate::Service
|
|
16
|
-
handler def greet(
|
|
17
|
-
|
|
16
|
+
handler def greet(name)
|
|
17
|
+
Restate.run_sync('build-greeting') { "Hello, #{name}!" }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class Counter < Restate::VirtualObject
|
|
22
|
+
state :count, default: 0
|
|
23
|
+
|
|
24
|
+
handler def add(addend)
|
|
25
|
+
self.count += addend
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
shared def get
|
|
29
|
+
count
|
|
18
30
|
end
|
|
19
31
|
end
|
|
20
32
|
```
|
|
@@ -67,8 +79,8 @@ end
|
|
|
67
79
|
|
|
68
80
|
class EventService < Restate::Service
|
|
69
81
|
handler :register, input: RegistrationRequest, output: RegistrationResponse
|
|
70
|
-
def register(
|
|
71
|
-
registration_id =
|
|
82
|
+
def register(request)
|
|
83
|
+
registration_id = Restate.run_sync('create-registration') do
|
|
72
84
|
"reg_#{request.event_name}_#{rand(10_000)}"
|
|
73
85
|
end
|
|
74
86
|
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'net/http'
|
|
5
|
+
require 'json'
|
|
6
|
+
|
|
7
|
+
module Restate
|
|
8
|
+
# HTTP client for invoking Restate services and managing the Restate runtime
|
|
9
|
+
# from outside the Restate runtime.
|
|
10
|
+
#
|
|
11
|
+
# @example Via global config (recommended)
|
|
12
|
+
# Restate.configure do |c|
|
|
13
|
+
# c.ingress_url = "http://localhost:8080"
|
|
14
|
+
# c.admin_url = "http://localhost:9070"
|
|
15
|
+
# end
|
|
16
|
+
# client = Restate.client
|
|
17
|
+
# result = client.service(Greeter).greet("World")
|
|
18
|
+
#
|
|
19
|
+
# @example Standalone
|
|
20
|
+
# client = Restate::Client.new(ingress_url: "http://localhost:8080",
|
|
21
|
+
# admin_url: "http://localhost:9070")
|
|
22
|
+
#
|
|
23
|
+
# @example Service invocation
|
|
24
|
+
# client.service("Greeter").greet("World")
|
|
25
|
+
# client.object("Counter", "my-key").add(5)
|
|
26
|
+
# client.workflow("UserSignup", "user42").run("user@example.com")
|
|
27
|
+
#
|
|
28
|
+
# @example Admin operations
|
|
29
|
+
# client.resolve_awakeable(awakeable_id, "result")
|
|
30
|
+
# client.reject_awakeable(awakeable_id, "failed")
|
|
31
|
+
# client.cancel_invocation(invocation_id)
|
|
32
|
+
# client.create_deployment("http://localhost:9080")
|
|
33
|
+
class Client
|
|
34
|
+
extend T::Sig
|
|
35
|
+
|
|
36
|
+
sig do
|
|
37
|
+
params(ingress_url: String, admin_url: String,
|
|
38
|
+
ingress_headers: T::Hash[String, String],
|
|
39
|
+
admin_headers: T::Hash[String, String]).void
|
|
40
|
+
end
|
|
41
|
+
def initialize(ingress_url: 'http://localhost:8080', admin_url: 'http://localhost:9070',
|
|
42
|
+
ingress_headers: {}, admin_headers: {})
|
|
43
|
+
@ingress_url = ingress_url.chomp('/')
|
|
44
|
+
@admin_url = admin_url.chomp('/')
|
|
45
|
+
@ingress_headers = ingress_headers
|
|
46
|
+
@admin_headers = admin_headers
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# ── Service invocation proxies ──
|
|
50
|
+
|
|
51
|
+
# Returns a proxy for calling a stateless service.
|
|
52
|
+
sig { params(service: T.any(String, T::Class[T.anything])).returns(ClientServiceProxy) }
|
|
53
|
+
def service(service)
|
|
54
|
+
ClientServiceProxy.new(@ingress_url, resolve_name(service), nil, @ingress_headers)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Returns a proxy for calling a keyed virtual object.
|
|
58
|
+
sig { params(service: T.any(String, T::Class[T.anything]), key: String).returns(ClientServiceProxy) }
|
|
59
|
+
def object(service, key)
|
|
60
|
+
ClientServiceProxy.new(@ingress_url, resolve_name(service), key, @ingress_headers)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Returns a proxy for calling a workflow.
|
|
64
|
+
sig { params(service: T.any(String, T::Class[T.anything]), key: String).returns(ClientServiceProxy) }
|
|
65
|
+
def workflow(service, key)
|
|
66
|
+
ClientServiceProxy.new(@ingress_url, resolve_name(service), key, @ingress_headers)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# ── Awakeable operations ──
|
|
70
|
+
|
|
71
|
+
# Resolve an awakeable from outside the Restate runtime.
|
|
72
|
+
sig { params(awakeable_id: String, payload: T.untyped).void }
|
|
73
|
+
def resolve_awakeable(awakeable_id, payload)
|
|
74
|
+
post_ingress("/restate/awakeables/#{awakeable_id}/resolve", payload)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Reject an awakeable from outside the Restate runtime.
|
|
78
|
+
sig { params(awakeable_id: String, message: String, code: Integer).void }
|
|
79
|
+
def reject_awakeable(awakeable_id, message, code: 500)
|
|
80
|
+
post_ingress("/restate/awakeables/#{awakeable_id}/reject",
|
|
81
|
+
{ 'message' => message, 'code' => code })
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# ── Invocation management ──
|
|
85
|
+
|
|
86
|
+
# Cancel a running invocation.
|
|
87
|
+
sig { params(invocation_id: String).void }
|
|
88
|
+
def cancel_invocation(invocation_id)
|
|
89
|
+
post_admin("/restate/invocations/#{invocation_id}/cancel", nil)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Kill a running invocation (immediate termination, no cleanup).
|
|
93
|
+
sig { params(invocation_id: String).void }
|
|
94
|
+
def kill_invocation(invocation_id)
|
|
95
|
+
post_admin("/restate/invocations/#{invocation_id}/kill", nil)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
sig { params(service: T.any(String, T::Class[T.anything])).returns(String) }
|
|
101
|
+
def resolve_name(service)
|
|
102
|
+
if service.is_a?(Class) && service.respond_to?(:service_name)
|
|
103
|
+
T.unsafe(service).service_name
|
|
104
|
+
else
|
|
105
|
+
service.to_s
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
sig { params(path: String, body: T.untyped).returns(T.untyped) }
|
|
110
|
+
def post_ingress(path, body) # rubocop:disable Metrics/AbcSize
|
|
111
|
+
uri = URI("#{@ingress_url}#{path}")
|
|
112
|
+
request = Net::HTTP::Post.new(uri)
|
|
113
|
+
request['Content-Type'] = 'application/json'
|
|
114
|
+
@ingress_headers.each { |k, v| request[k] = v }
|
|
115
|
+
request.body = JSON.generate(body) if body
|
|
116
|
+
response = Net::HTTP.start(uri.hostname, uri.port,
|
|
117
|
+
use_ssl: uri.scheme == 'https',
|
|
118
|
+
read_timeout: 30) { |http| http.request(request) }
|
|
119
|
+
Kernel.raise "Restate ingress error: #{response.code} #{response.body}" unless response.is_a?(Net::HTTPSuccess)
|
|
120
|
+
parse_response(response)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
sig { params(path: String, body: T.untyped).returns(T.untyped) }
|
|
124
|
+
def post_admin(path, body) # rubocop:disable Metrics/AbcSize
|
|
125
|
+
uri = URI("#{@admin_url}#{path}")
|
|
126
|
+
request = Net::HTTP::Post.new(uri)
|
|
127
|
+
request['Content-Type'] = 'application/json'
|
|
128
|
+
@admin_headers.each { |k, v| request[k] = v }
|
|
129
|
+
request.body = JSON.generate(body) if body
|
|
130
|
+
response = Net::HTTP.start(uri.hostname, uri.port,
|
|
131
|
+
use_ssl: uri.scheme == 'https',
|
|
132
|
+
read_timeout: 30) { |http| http.request(request) }
|
|
133
|
+
Kernel.raise "Restate admin error: #{response.code} #{response.body}" unless response.is_a?(Net::HTTPSuccess)
|
|
134
|
+
parse_response(response)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
sig { params(response: Net::HTTPResponse).returns(T.untyped) }
|
|
138
|
+
def parse_response(response)
|
|
139
|
+
body = response.body
|
|
140
|
+
body && !body.empty? ? JSON.parse(body) : nil
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Proxy that sends HTTP requests to the Restate ingress for a specific service.
|
|
145
|
+
# Handler calls are forwarded via +method_missing+.
|
|
146
|
+
#
|
|
147
|
+
# @!visibility private
|
|
148
|
+
class ClientServiceProxy
|
|
149
|
+
extend T::Sig
|
|
150
|
+
|
|
151
|
+
sig do
|
|
152
|
+
params(base_url: String, service_name: String, key: T.nilable(String),
|
|
153
|
+
headers: T::Hash[String, String]).void
|
|
154
|
+
end
|
|
155
|
+
def initialize(base_url, service_name, key, headers)
|
|
156
|
+
@base_url = base_url
|
|
157
|
+
@service_name = service_name
|
|
158
|
+
@key = key
|
|
159
|
+
@headers = headers
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def method_missing(handler_name, arg = nil) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
|
163
|
+
path = @key ? "/#{@service_name}/#{@key}/#{handler_name}" : "/#{@service_name}/#{handler_name}"
|
|
164
|
+
uri = URI("#{@base_url}#{path}")
|
|
165
|
+
request = Net::HTTP::Post.new(uri)
|
|
166
|
+
request['Content-Type'] = 'application/json'
|
|
167
|
+
@headers.each { |k, v| request[k] = v }
|
|
168
|
+
request.body = JSON.generate(arg)
|
|
169
|
+
response = Net::HTTP.start(uri.hostname, uri.port,
|
|
170
|
+
use_ssl: uri.scheme == 'https',
|
|
171
|
+
read_timeout: 30) { |http| http.request(request) }
|
|
172
|
+
Kernel.raise "Restate ingress error: #{response.code} #{response.body}" unless response.is_a?(Net::HTTPSuccess)
|
|
173
|
+
body = response.body
|
|
174
|
+
body && !body.empty? ? JSON.parse(body) : nil
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def respond_to_missing?(_method_name, _include_private = false)
|
|
178
|
+
true
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Restate
|
|
5
|
+
# Global SDK configuration. Set via +Restate.configure+.
|
|
6
|
+
#
|
|
7
|
+
# @example
|
|
8
|
+
# Restate.configure do |c|
|
|
9
|
+
# c.ingress_url = "http://localhost:8080"
|
|
10
|
+
# c.admin_url = "http://localhost:9070"
|
|
11
|
+
# end
|
|
12
|
+
#
|
|
13
|
+
# # Then use the pre-configured client:
|
|
14
|
+
# Restate.client.service(Greeter).greet("World")
|
|
15
|
+
class Config
|
|
16
|
+
extend T::Sig
|
|
17
|
+
|
|
18
|
+
# Restate ingress URL (for invoking services).
|
|
19
|
+
sig { returns(String) }
|
|
20
|
+
attr_accessor :ingress_url
|
|
21
|
+
|
|
22
|
+
# Restate admin URL (for deployments, invocation management).
|
|
23
|
+
sig { returns(String) }
|
|
24
|
+
attr_accessor :admin_url
|
|
25
|
+
|
|
26
|
+
# Default headers sent with every ingress request.
|
|
27
|
+
sig { returns(T::Hash[String, String]) }
|
|
28
|
+
attr_accessor :ingress_headers
|
|
29
|
+
|
|
30
|
+
# Default headers sent with every admin request.
|
|
31
|
+
sig { returns(T::Hash[String, String]) }
|
|
32
|
+
attr_accessor :admin_headers
|
|
33
|
+
|
|
34
|
+
sig { void }
|
|
35
|
+
def initialize
|
|
36
|
+
@ingress_url = T.let('http://localhost:8080', String)
|
|
37
|
+
@admin_url = T.let('http://localhost:9070', String)
|
|
38
|
+
@ingress_headers = T.let({}, T::Hash[String, String])
|
|
39
|
+
@admin_headers = T.let({}, T::Hash[String, String])
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
data/lib/restate/handler.rb
CHANGED
|
@@ -41,19 +41,20 @@ module Restate
|
|
|
41
41
|
end
|
|
42
42
|
def invoke_handler(handler:, ctx:, in_buffer:, middleware: []) # rubocop:disable Metrics/AbcSize
|
|
43
43
|
call_handler = Kernel.proc do
|
|
44
|
-
if handler.arity ==
|
|
44
|
+
if handler.arity == 1
|
|
45
45
|
begin
|
|
46
46
|
in_arg = handler.handler_io.input_serde.deserialize(in_buffer)
|
|
47
47
|
rescue StandardError => e
|
|
48
48
|
Kernel.raise TerminalError, "Unable to parse input argument: #{e.message}"
|
|
49
49
|
end
|
|
50
|
-
handler.callable.call(
|
|
50
|
+
handler.callable.call(in_arg)
|
|
51
51
|
else
|
|
52
|
-
handler.callable.call
|
|
52
|
+
handler.callable.call
|
|
53
53
|
end
|
|
54
54
|
end
|
|
55
55
|
|
|
56
56
|
# Build the middleware chain so each middleware can use `yield` to call the next.
|
|
57
|
+
# Middleware still receives (handler, ctx) for low-level access.
|
|
57
58
|
chain = middleware.reverse.reduce(call_handler) do |nxt, mw|
|
|
58
59
|
Kernel.proc { mw.call(handler, ctx, &nxt) }
|
|
59
60
|
end
|
data/lib/restate/service.rb
CHANGED
|
@@ -30,6 +30,28 @@ module Restate
|
|
|
30
30
|
_register_handler(method_name, **T.unsafe({ kind: nil, **opts }))
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
+
# Returns a call proxy for fluent durable calls to this service.
|
|
34
|
+
#
|
|
35
|
+
# @example
|
|
36
|
+
# Greeter.call.greet("World").await
|
|
37
|
+
#
|
|
38
|
+
# @return [ServiceCallProxy]
|
|
39
|
+
def self.call
|
|
40
|
+
ServiceCallProxy.new(self, call_method: :service_call)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Returns a send proxy for fluent fire-and-forget sends to this service.
|
|
44
|
+
#
|
|
45
|
+
# @example
|
|
46
|
+
# Greeter.send!.greet("World")
|
|
47
|
+
# Greeter.send!(delay: 60).greet("World")
|
|
48
|
+
#
|
|
49
|
+
# @param delay [Numeric, nil] optional delay in seconds
|
|
50
|
+
# @return [ServiceSendProxy]
|
|
51
|
+
def self.send!(delay: nil)
|
|
52
|
+
ServiceSendProxy.new(self, send_method: :service_send, delay: delay)
|
|
53
|
+
end
|
|
54
|
+
|
|
33
55
|
def self._service_kind
|
|
34
56
|
'service'
|
|
35
57
|
end
|
data/lib/restate/service_dsl.rb
CHANGED
|
@@ -31,6 +31,70 @@ module Restate
|
|
|
31
31
|
subclass.instance_variable_set(:@_idempotency_retention, nil)
|
|
32
32
|
subclass.instance_variable_set(:@_ingress_private, nil)
|
|
33
33
|
subclass.instance_variable_set(:@_invocation_retry_policy, nil)
|
|
34
|
+
subclass.instance_variable_set(:@_state_declarations, {})
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Declare a durable state entry with auto-generated getter, setter, and clear methods.
|
|
38
|
+
# Only available on VirtualObject and Workflow.
|
|
39
|
+
#
|
|
40
|
+
# The generated methods delegate to the current Restate context via +Thread.current+
|
|
41
|
+
# (fiber-scoped in Ruby 3.0+), so they work correctly across concurrent invocations.
|
|
42
|
+
#
|
|
43
|
+
# @param name [Symbol] state key name
|
|
44
|
+
# @param default [Object, nil] default value returned when state is not set
|
|
45
|
+
# @param serde [Object] serializer/deserializer (defaults to JsonSerde)
|
|
46
|
+
#
|
|
47
|
+
# @example
|
|
48
|
+
# class Counter < Restate::VirtualObject
|
|
49
|
+
# state :count, default: 0
|
|
50
|
+
#
|
|
51
|
+
# handler def add(ctx, addend)
|
|
52
|
+
# self.count += addend # reads then writes via ctx.get/ctx.set
|
|
53
|
+
# end
|
|
54
|
+
#
|
|
55
|
+
# shared def get(ctx)
|
|
56
|
+
# count # reads via ctx.get, returns 0 if unset
|
|
57
|
+
# end
|
|
58
|
+
# end
|
|
59
|
+
def state(name, default: nil, serde: nil) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
|
60
|
+
unless T.unsafe(self).respond_to?(:_service_kind) && %w[object workflow].include?(T.unsafe(self)._service_kind)
|
|
61
|
+
Kernel.raise ArgumentError, 'state declarations are only available on VirtualObject and Workflow'
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
name = name.to_sym
|
|
65
|
+
@_state_declarations[name] = { default: default, serde: serde }
|
|
66
|
+
state_key = name.to_s
|
|
67
|
+
state_serde = serde
|
|
68
|
+
state_default = default
|
|
69
|
+
|
|
70
|
+
# Getter: reads from durable state, returns default if unset
|
|
71
|
+
T.unsafe(self).define_method(name) do
|
|
72
|
+
ctx = Thread.current[:restate_context]
|
|
73
|
+
Kernel.raise 'Not inside a Restate handler' unless ctx
|
|
74
|
+
|
|
75
|
+
val = state_serde ? ctx.get(state_key, serde: state_serde) : ctx.get(state_key)
|
|
76
|
+
val.nil? ? state_default : val
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Setter: writes to durable state
|
|
80
|
+
T.unsafe(self).define_method(:"#{name}=") do |value|
|
|
81
|
+
ctx = Thread.current[:restate_context]
|
|
82
|
+
Kernel.raise 'Not inside a Restate handler' unless ctx
|
|
83
|
+
|
|
84
|
+
if state_serde
|
|
85
|
+
ctx.set(state_key, value, serde: state_serde)
|
|
86
|
+
else
|
|
87
|
+
ctx.set(state_key, value)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Clear: removes the state entry
|
|
92
|
+
T.unsafe(self).define_method(:"clear_#{name}") do
|
|
93
|
+
ctx = Thread.current[:restate_context]
|
|
94
|
+
Kernel.raise 'Not inside a Restate handler' unless ctx
|
|
95
|
+
|
|
96
|
+
ctx.clear(state_key)
|
|
97
|
+
end
|
|
34
98
|
end
|
|
35
99
|
|
|
36
100
|
# Get or set the service name. Defaults to the unqualified class name.
|
|
@@ -210,8 +274,8 @@ module Restate
|
|
|
210
274
|
|
|
211
275
|
um = T.unsafe(self).instance_method(name)
|
|
212
276
|
arity = um.arity.abs
|
|
213
|
-
unless [
|
|
214
|
-
Kernel.raise ArgumentError, "handler '#{name}' must accept
|
|
277
|
+
unless [0, 1].include?(arity)
|
|
278
|
+
Kernel.raise ArgumentError, "handler '#{name}' must accept 0 or 1 parameters ([input]), got #{arity}"
|
|
215
279
|
end
|
|
216
280
|
|
|
217
281
|
bound = um.bind(instance)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Restate
|
|
5
|
+
# Proxy for fluent durable calls: +Service.call.handler(arg)+
|
|
6
|
+
#
|
|
7
|
+
# Returned by the +.call+ class method on service classes. Uses +method_missing+
|
|
8
|
+
# to forward handler invocations to the current Restate context.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# # Instead of: ctx.service_call(Greeter, :greet, "World")
|
|
12
|
+
# Greeter.call.greet("World")
|
|
13
|
+
#
|
|
14
|
+
# # Instead of: ctx.object_call(Counter, :add, "key", 5)
|
|
15
|
+
# Counter.call("key").add(5)
|
|
16
|
+
#
|
|
17
|
+
# @!visibility private
|
|
18
|
+
class ServiceCallProxy
|
|
19
|
+
extend T::Sig
|
|
20
|
+
|
|
21
|
+
sig { params(service_class: T.untyped, key: T.nilable(String), call_method: Symbol).void }
|
|
22
|
+
def initialize(service_class, key: nil, call_method: :service_call)
|
|
23
|
+
@service_class = service_class
|
|
24
|
+
@key = key
|
|
25
|
+
@call_method = call_method
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def method_missing(handler_name, arg = nil, **opts)
|
|
29
|
+
ctx = Restate.fetch_context!
|
|
30
|
+
if @key
|
|
31
|
+
ctx.public_send(@call_method, @service_class, handler_name, @key, arg, **opts)
|
|
32
|
+
else
|
|
33
|
+
ctx.public_send(@call_method, @service_class, handler_name, arg, **opts)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
38
|
+
(@service_class.respond_to?(:handlers) && T.unsafe(@service_class).handlers.key?(method_name.to_s)) || super
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Proxy for fluent fire-and-forget sends: +Service.send!.handler(arg)+
|
|
43
|
+
#
|
|
44
|
+
# Returned by the +.send!+ class method on service classes.
|
|
45
|
+
#
|
|
46
|
+
# @example
|
|
47
|
+
# # Instead of: ctx.service_send(Greeter, :greet, "World")
|
|
48
|
+
# Greeter.send!.greet("World")
|
|
49
|
+
#
|
|
50
|
+
# # Instead of: ctx.object_send(Counter, :add, "key", 5, delay: 60)
|
|
51
|
+
# Counter.send!("key", delay: 60).add(5)
|
|
52
|
+
#
|
|
53
|
+
# @!visibility private
|
|
54
|
+
class ServiceSendProxy
|
|
55
|
+
extend T::Sig
|
|
56
|
+
|
|
57
|
+
sig do
|
|
58
|
+
params(
|
|
59
|
+
service_class: T.untyped, key: T.nilable(String),
|
|
60
|
+
send_method: Symbol, delay: T.nilable(Numeric)
|
|
61
|
+
).void
|
|
62
|
+
end
|
|
63
|
+
def initialize(service_class, key: nil, send_method: :service_send, delay: nil)
|
|
64
|
+
@service_class = service_class
|
|
65
|
+
@key = key
|
|
66
|
+
@send_method = send_method
|
|
67
|
+
@delay = delay
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def method_missing(handler_name, arg = nil, **opts)
|
|
71
|
+
ctx = Restate.fetch_context!
|
|
72
|
+
opts[:delay] = @delay if @delay
|
|
73
|
+
if @key
|
|
74
|
+
ctx.public_send(@send_method, @service_class, handler_name, @key, arg, **opts)
|
|
75
|
+
else
|
|
76
|
+
ctx.public_send(@send_method, @service_class, handler_name, arg, **opts)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
81
|
+
(@service_class.respond_to?(:handlers) && T.unsafe(@service_class).handlers.key?(method_name.to_s)) || super
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
data/lib/restate/version.rb
CHANGED
|
@@ -49,6 +49,30 @@ module Restate
|
|
|
49
49
|
_register_handler(method_name, **T.unsafe({ kind: 'shared', **opts }))
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
+
# Returns a call proxy for fluent durable calls to this virtual object.
|
|
53
|
+
#
|
|
54
|
+
# @example
|
|
55
|
+
# Counter.call("my-key").add(5).await
|
|
56
|
+
#
|
|
57
|
+
# @param key [String] the object key
|
|
58
|
+
# @return [ServiceCallProxy]
|
|
59
|
+
def self.call(key)
|
|
60
|
+
ServiceCallProxy.new(self, key: key, call_method: :object_call)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Returns a send proxy for fluent fire-and-forget sends to this virtual object.
|
|
64
|
+
#
|
|
65
|
+
# @example
|
|
66
|
+
# Counter.send!("my-key").add(5)
|
|
67
|
+
# Counter.send!("my-key", delay: 60).add(5)
|
|
68
|
+
#
|
|
69
|
+
# @param key [String] the object key
|
|
70
|
+
# @param delay [Numeric, nil] optional delay in seconds
|
|
71
|
+
# @return [ServiceSendProxy]
|
|
72
|
+
def self.send!(key, delay: nil)
|
|
73
|
+
ServiceSendProxy.new(self, key: key, send_method: :object_send, delay: delay)
|
|
74
|
+
end
|
|
75
|
+
|
|
52
76
|
def self._service_kind
|
|
53
77
|
'object'
|
|
54
78
|
end
|
data/lib/restate/workflow.rb
CHANGED
|
@@ -49,6 +49,30 @@ module Restate
|
|
|
49
49
|
_register_handler(method_name, **T.unsafe({ kind: 'shared', **opts }))
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
+
# Returns a call proxy for fluent durable calls to this workflow.
|
|
53
|
+
#
|
|
54
|
+
# @example
|
|
55
|
+
# UserSignup.call("user42").run("user@example.com").await
|
|
56
|
+
#
|
|
57
|
+
# @param key [String] the workflow key
|
|
58
|
+
# @return [ServiceCallProxy]
|
|
59
|
+
def self.call(key)
|
|
60
|
+
ServiceCallProxy.new(self, key: key, call_method: :workflow_call)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Returns a send proxy for fluent fire-and-forget sends to this workflow.
|
|
64
|
+
#
|
|
65
|
+
# @example
|
|
66
|
+
# UserSignup.send!("user42").run("user@example.com")
|
|
67
|
+
# UserSignup.send!("user42", delay: 60).run("user@example.com")
|
|
68
|
+
#
|
|
69
|
+
# @param key [String] the workflow key
|
|
70
|
+
# @param delay [Numeric, nil] optional delay in seconds
|
|
71
|
+
# @return [ServiceSendProxy]
|
|
72
|
+
def self.send!(key, delay: nil)
|
|
73
|
+
ServiceSendProxy.new(self, key: key, send_method: :workflow_send, delay: delay)
|
|
74
|
+
end
|
|
75
|
+
|
|
52
76
|
def self._service_kind
|
|
53
77
|
'workflow'
|
|
54
78
|
end
|