restate-sdk 0.5.1-aarch64-linux → 0.7.0-aarch64-linux
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Cargo.lock +1 -1
- data/README.md +16 -4
- data/ext/restate_internal/Cargo.toml +1 -1
- data/lib/restate/3.2/restate_internal.so +0 -0
- data/lib/restate/3.3/restate_internal.so +0 -0
- data/lib/restate/3.4/restate_internal.so +0 -0
- data/lib/restate/4.0/restate_internal.so +0 -0
- data/lib/restate/client.rb +181 -0
- data/lib/restate/config.rb +42 -0
- data/lib/restate/endpoint.rb +68 -0
- data/lib/restate/handler.rb +24 -10
- data/lib/restate/server.rb +2 -1
- data/lib/restate/server_context.rb +8 -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/testing.rb +7 -2
- 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: 72ba1274d55d51d051485f01a9779d27b6d9935fdafc397f98d79a6471bd0314
|
|
4
|
+
data.tar.gz: a731a191f45085088e57fb54ca1210826ae2b8dfcf1a8789f187074b63685666
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f6a72f1412e67244f4abe808aa4a9c3ab7bf411018351a4909e85242fd1516bd48141964866959b59d72f3570df037c48018927cb13383ba9c555768a77834fd
|
|
7
|
+
data.tar.gz: 467150dd5dba8e0adeb5dfddec4770c6a8976846c13b79b8865e954b9dc54c7f714bd3d89d95f267c4a9f6bcacd7de629eb5877693663757c9eac98d70909ca9
|
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
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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/endpoint.rb
CHANGED
|
@@ -15,11 +15,15 @@ module Restate
|
|
|
15
15
|
sig { returns(T.nilable(String)) }
|
|
16
16
|
attr_accessor :protocol
|
|
17
17
|
|
|
18
|
+
sig { returns(T::Array[T.untyped]) }
|
|
19
|
+
attr_reader :middleware
|
|
20
|
+
|
|
18
21
|
sig { void }
|
|
19
22
|
def initialize
|
|
20
23
|
@services = T.let({}, T::Hash[String, T.untyped])
|
|
21
24
|
@protocol = T.let(nil, T.nilable(String))
|
|
22
25
|
@identity_keys = T.let([], T::Array[String])
|
|
26
|
+
@middleware = T.let([], T::Array[T.untyped])
|
|
23
27
|
end
|
|
24
28
|
|
|
25
29
|
# Bind one or more services to this endpoint.
|
|
@@ -59,6 +63,70 @@ module Restate
|
|
|
59
63
|
self
|
|
60
64
|
end
|
|
61
65
|
|
|
66
|
+
# Add handler-level middleware.
|
|
67
|
+
#
|
|
68
|
+
# Middleware wraps every handler invocation with access to the handler metadata
|
|
69
|
+
# and context. Use it for tracing, metrics, logging, error reporting, etc.
|
|
70
|
+
#
|
|
71
|
+
# A middleware is a class whose instances respond to +call(handler, ctx)+.
|
|
72
|
+
# Use +yield+ inside +call+ to invoke the next middleware or the handler.
|
|
73
|
+
# The return value of +yield+ is the handler's return value.
|
|
74
|
+
#
|
|
75
|
+
# This follows the same pattern as {https://github.com/sidekiq/sidekiq/wiki/Middleware Sidekiq middleware}.
|
|
76
|
+
#
|
|
77
|
+
# @example OpenTelemetry tracing
|
|
78
|
+
# class OpenTelemetryMiddleware
|
|
79
|
+
# def call(handler, ctx)
|
|
80
|
+
# tracer.in_span(handler.name, attributes: {
|
|
81
|
+
# 'restate.service' => handler.service_tag.name,
|
|
82
|
+
# 'restate.invocation_id' => ctx.request.id
|
|
83
|
+
# }) do
|
|
84
|
+
# yield
|
|
85
|
+
# end
|
|
86
|
+
# end
|
|
87
|
+
# end
|
|
88
|
+
# endpoint.use(OpenTelemetryMiddleware)
|
|
89
|
+
#
|
|
90
|
+
# @example Metrics
|
|
91
|
+
# class MetricsMiddleware
|
|
92
|
+
# def call(handler, ctx)
|
|
93
|
+
# start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
94
|
+
# result = yield
|
|
95
|
+
# duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
|
|
96
|
+
# StatsD.timing("restate.handler.#{handler.name}", duration)
|
|
97
|
+
# result
|
|
98
|
+
# end
|
|
99
|
+
# end
|
|
100
|
+
# endpoint.use(MetricsMiddleware)
|
|
101
|
+
#
|
|
102
|
+
# @example Middleware with configuration
|
|
103
|
+
# class AuthMiddleware
|
|
104
|
+
# def initialize(api_key:)
|
|
105
|
+
# @api_key = api_key
|
|
106
|
+
# end
|
|
107
|
+
#
|
|
108
|
+
# def call(handler, ctx)
|
|
109
|
+
# raise Restate::TerminalError.new('unauthorized', status_code: 401) unless valid?(ctx)
|
|
110
|
+
# yield
|
|
111
|
+
# end
|
|
112
|
+
# end
|
|
113
|
+
# endpoint.use(AuthMiddleware, api_key: 'secret')
|
|
114
|
+
#
|
|
115
|
+
# @param klass [Class] middleware class (will be instantiated by the SDK)
|
|
116
|
+
# @param args [Array] positional arguments for the middleware constructor
|
|
117
|
+
# @param kwargs [Hash] keyword arguments for the middleware constructor
|
|
118
|
+
# @return [self]
|
|
119
|
+
sig { params(klass: T.untyped, args: T.untyped, kwargs: T.untyped).returns(T.self_type) }
|
|
120
|
+
def use(klass, *args, **kwargs)
|
|
121
|
+
instance = if kwargs.empty?
|
|
122
|
+
klass.new(*args)
|
|
123
|
+
else
|
|
124
|
+
klass.new(*args, **kwargs)
|
|
125
|
+
end
|
|
126
|
+
@middleware << instance
|
|
127
|
+
self
|
|
128
|
+
end
|
|
129
|
+
|
|
62
130
|
# Build and return the Rack-compatible application.
|
|
63
131
|
sig { returns(T.untyped) }
|
|
64
132
|
def app
|
data/lib/restate/handler.rb
CHANGED
|
@@ -33,19 +33,33 @@ module Restate
|
|
|
33
33
|
|
|
34
34
|
# Invoke a handler with the context and raw input bytes.
|
|
35
35
|
# The context is passed as the first argument to every handler.
|
|
36
|
+
# Middleware (if any) wraps the handler call.
|
|
36
37
|
# Returns raw output bytes.
|
|
37
|
-
sig
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
sig do
|
|
39
|
+
params(handler: T.untyped, ctx: T.untyped, in_buffer: String,
|
|
40
|
+
middleware: T::Array[T.untyped]).returns(String)
|
|
41
|
+
end
|
|
42
|
+
def invoke_handler(handler:, ctx:, in_buffer:, middleware: []) # rubocop:disable Metrics/AbcSize
|
|
43
|
+
call_handler = Kernel.proc do
|
|
44
|
+
if handler.arity == 1
|
|
45
|
+
begin
|
|
46
|
+
in_arg = handler.handler_io.input_serde.deserialize(in_buffer)
|
|
47
|
+
rescue StandardError => e
|
|
48
|
+
Kernel.raise TerminalError, "Unable to parse input argument: #{e.message}"
|
|
49
|
+
end
|
|
50
|
+
handler.callable.call(in_arg)
|
|
51
|
+
else
|
|
52
|
+
handler.callable.call
|
|
44
53
|
end
|
|
45
|
-
out_arg = handler.callable.call(ctx, in_arg)
|
|
46
|
-
else
|
|
47
|
-
out_arg = handler.callable.call(ctx)
|
|
48
54
|
end
|
|
55
|
+
|
|
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.
|
|
58
|
+
chain = middleware.reverse.reduce(call_handler) do |nxt, mw|
|
|
59
|
+
Kernel.proc { mw.call(handler, ctx, &nxt) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
out_arg = chain.call
|
|
49
63
|
handler.handler_io.output_serde.serialize(out_arg)
|
|
50
64
|
end
|
|
51
65
|
end
|
data/lib/restate/server.rb
CHANGED
|
@@ -204,7 +204,8 @@ module Restate
|
|
|
204
204
|
handler: handler,
|
|
205
205
|
invocation: invocation,
|
|
206
206
|
send_output: send_output,
|
|
207
|
-
input_queue: input_queue
|
|
207
|
+
input_queue: input_queue,
|
|
208
|
+
middleware: @endpoint.middleware
|
|
208
209
|
)
|
|
209
210
|
|
|
210
211
|
# Spawn the handler as an async task so the response body can stream
|
|
@@ -28,8 +28,11 @@ module Restate
|
|
|
28
28
|
sig { returns(T.untyped) }
|
|
29
29
|
attr_reader :invocation
|
|
30
30
|
|
|
31
|
-
sig
|
|
32
|
-
|
|
31
|
+
sig do
|
|
32
|
+
params(vm: VMWrapper, handler: T.untyped, invocation: T.untyped, send_output: T.untyped,
|
|
33
|
+
input_queue: Async::Queue, middleware: T::Array[T.untyped]).void
|
|
34
|
+
end
|
|
35
|
+
def initialize(vm:, handler:, invocation:, send_output:, input_queue:, middleware: [])
|
|
33
36
|
@vm = T.let(vm, VMWrapper)
|
|
34
37
|
@handler = T.let(handler, T.untyped)
|
|
35
38
|
@invocation = T.let(invocation, T.untyped)
|
|
@@ -37,6 +40,7 @@ module Restate
|
|
|
37
40
|
@input_queue = T.let(input_queue, Async::Queue)
|
|
38
41
|
@run_coros_to_execute = T.let({}, T::Hash[Integer, T.untyped])
|
|
39
42
|
@attempt_finished_event = T.let(AttemptFinishedEvent.new, AttemptFinishedEvent)
|
|
43
|
+
@middleware = T.let(middleware, T::Array[T.untyped])
|
|
40
44
|
end
|
|
41
45
|
|
|
42
46
|
# ── Main entry point ──
|
|
@@ -48,7 +52,8 @@ module Restate
|
|
|
48
52
|
Thread.current[:restate_service_kind] = @handler.service_tag.kind
|
|
49
53
|
Thread.current[:restate_handler_kind] = @handler.kind
|
|
50
54
|
in_buffer = @invocation.input_buffer
|
|
51
|
-
out_buffer = Restate.invoke_handler(handler: @handler, ctx: self, in_buffer: in_buffer
|
|
55
|
+
out_buffer = Restate.invoke_handler(handler: @handler, ctx: self, in_buffer: in_buffer,
|
|
56
|
+
middleware: @middleware)
|
|
52
57
|
@vm.sys_write_output_success(out_buffer.b)
|
|
53
58
|
@vm.sys_end
|
|
54
59
|
rescue TerminalError => e
|
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
|