restate-sdk 0.9.0-aarch64-linux → 0.11.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 +9 -17
- data/ext/restate_internal/Cargo.toml +1 -1
- data/ext/restate_internal/src/lib.rs +52 -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 +27 -0
- data/lib/restate/context.rb +12 -0
- data/lib/restate/introspection.rb +127 -0
- data/lib/restate/middleware/deadlock_detection.rb +216 -0
- data/lib/restate/railtie.rb +17 -0
- data/lib/restate/server_context.rb +41 -15
- data/lib/restate/version.rb +1 -1
- data/lib/restate/vm.rb +13 -0
- data/lib/restate.rb +19 -0
- data/sig/restate.rbs +9 -0
- metadata +6 -4
- data/lib/restate/3.2/restate_internal.so +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e63ec315f5d51fb381c2881bcf59887c4b87759ea862768eb7c19ca1679e126c
|
|
4
|
+
data.tar.gz: 5e08e3adde55ed23702eb07a4ee39af293aba626f6ade11d1b2397b24eedaa5a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fa16f197ba900b69c230c79e29babaa31044c83366a3586387f58f2870b2730129559fa9ed44cea27fb33118e689472f2755dace09f6190f501a434da5294268
|
|
7
|
+
data.tar.gz: 7b93888914e35d8d4e1cde6d1232030dae6be7d34fef9075afb2c31fdaea71e01ca0e99cfc1d6714758a84152ce08f384e60ea40236ccb682c67bd518360ecd1
|
data/Cargo.lock
CHANGED
|
@@ -31,16 +31,14 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
|
|
31
31
|
|
|
32
32
|
[[package]]
|
|
33
33
|
name = "bindgen"
|
|
34
|
-
version = "0.
|
|
34
|
+
version = "0.72.1"
|
|
35
35
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
36
|
-
checksum = "
|
|
36
|
+
checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
|
|
37
37
|
dependencies = [
|
|
38
38
|
"bitflags",
|
|
39
39
|
"cexpr",
|
|
40
40
|
"clang-sys",
|
|
41
41
|
"itertools 0.12.1",
|
|
42
|
-
"lazy_static",
|
|
43
|
-
"lazycell",
|
|
44
42
|
"proc-macro2",
|
|
45
43
|
"quote",
|
|
46
44
|
"regex",
|
|
@@ -278,12 +276,6 @@ version = "1.5.0"
|
|
|
278
276
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
279
277
|
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
|
280
278
|
|
|
281
|
-
[[package]]
|
|
282
|
-
name = "lazycell"
|
|
283
|
-
version = "1.3.0"
|
|
284
|
-
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
285
|
-
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
|
|
286
|
-
|
|
287
279
|
[[package]]
|
|
288
280
|
name = "libc"
|
|
289
281
|
version = "0.2.183"
|
|
@@ -489,18 +481,18 @@ dependencies = [
|
|
|
489
481
|
|
|
490
482
|
[[package]]
|
|
491
483
|
name = "rb-sys"
|
|
492
|
-
version = "0.9.
|
|
484
|
+
version = "0.9.128"
|
|
493
485
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
494
|
-
checksum = "
|
|
486
|
+
checksum = "45ca28513560e56cfb79a62b1fce363c73af170a182024ce880c77ee9429920a"
|
|
495
487
|
dependencies = [
|
|
496
488
|
"rb-sys-build",
|
|
497
489
|
]
|
|
498
490
|
|
|
499
491
|
[[package]]
|
|
500
492
|
name = "rb-sys-build"
|
|
501
|
-
version = "0.9.
|
|
493
|
+
version = "0.9.128"
|
|
502
494
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
503
|
-
checksum = "
|
|
495
|
+
checksum = "ce04b2c55eff3a21aaa623fcc655d94373238e72cac6b3e1a3641ff31649f99a"
|
|
504
496
|
dependencies = [
|
|
505
497
|
"bindgen",
|
|
506
498
|
"lazy_static",
|
|
@@ -569,7 +561,7 @@ dependencies = [
|
|
|
569
561
|
|
|
570
562
|
[[package]]
|
|
571
563
|
name = "restate_internal"
|
|
572
|
-
version = "0.
|
|
564
|
+
version = "0.11.0"
|
|
573
565
|
dependencies = [
|
|
574
566
|
"magnus",
|
|
575
567
|
"rb-sys",
|
|
@@ -593,9 +585,9 @@ dependencies = [
|
|
|
593
585
|
|
|
594
586
|
[[package]]
|
|
595
587
|
name = "rustc-hash"
|
|
596
|
-
version = "
|
|
588
|
+
version = "2.1.2"
|
|
597
589
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
598
|
-
checksum = "
|
|
590
|
+
checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
|
|
599
591
|
|
|
600
592
|
[[package]]
|
|
601
593
|
name = "rustversion"
|
|
@@ -794,6 +794,49 @@ impl RbVM {
|
|
|
794
794
|
.map_err(core_error_to_magnus)
|
|
795
795
|
}
|
|
796
796
|
|
|
797
|
+
// ── Signals ──
|
|
798
|
+
|
|
799
|
+
fn sys_signal(&self, signal_name: String) -> Result<u32, Error> {
|
|
800
|
+
self.vm
|
|
801
|
+
.borrow_mut()
|
|
802
|
+
.create_signal_handle(signal_name)
|
|
803
|
+
.map(Into::into)
|
|
804
|
+
.map_err(core_error_to_magnus)
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
fn sys_complete_signal_success(
|
|
808
|
+
&self,
|
|
809
|
+
target_invocation_id: String,
|
|
810
|
+
signal_name: String,
|
|
811
|
+
buffer: RString,
|
|
812
|
+
) -> Result<(), Error> {
|
|
813
|
+
let bytes: Vec<u8> = unsafe { buffer.as_slice().to_vec() };
|
|
814
|
+
self.vm
|
|
815
|
+
.borrow_mut()
|
|
816
|
+
.sys_complete_signal(
|
|
817
|
+
target_invocation_id,
|
|
818
|
+
signal_name,
|
|
819
|
+
NonEmptyValue::Success(bytes.into()),
|
|
820
|
+
)
|
|
821
|
+
.map_err(core_error_to_magnus)
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
fn sys_complete_signal_failure(
|
|
825
|
+
&self,
|
|
826
|
+
target_invocation_id: String,
|
|
827
|
+
signal_name: String,
|
|
828
|
+
failure: &RbFailure,
|
|
829
|
+
) -> Result<(), Error> {
|
|
830
|
+
self.vm
|
|
831
|
+
.borrow_mut()
|
|
832
|
+
.sys_complete_signal(
|
|
833
|
+
target_invocation_id,
|
|
834
|
+
signal_name,
|
|
835
|
+
NonEmptyValue::Failure(failure.clone().into()),
|
|
836
|
+
)
|
|
837
|
+
.map_err(core_error_to_magnus)
|
|
838
|
+
}
|
|
839
|
+
|
|
797
840
|
// ── Cancel invocation ──
|
|
798
841
|
|
|
799
842
|
fn sys_cancel_invocation(&self, target_invocation_id: String) -> Result<(), Error> {
|
|
@@ -1074,6 +1117,15 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
1074
1117
|
"sys_cancel_invocation",
|
|
1075
1118
|
method!(RbVM::sys_cancel_invocation, 1),
|
|
1076
1119
|
)?;
|
|
1120
|
+
vm_class.define_method("sys_signal", method!(RbVM::sys_signal, 1))?;
|
|
1121
|
+
vm_class.define_method(
|
|
1122
|
+
"sys_complete_signal_success",
|
|
1123
|
+
method!(RbVM::sys_complete_signal_success, 3),
|
|
1124
|
+
)?;
|
|
1125
|
+
vm_class.define_method(
|
|
1126
|
+
"sys_complete_signal_failure",
|
|
1127
|
+
method!(RbVM::sys_complete_signal_failure, 3),
|
|
1128
|
+
)?;
|
|
1077
1129
|
|
|
1078
1130
|
// IdentityVerifier
|
|
1079
1131
|
let iv_class = internal.define_class("IdentityVerifier", ruby.class_object())?;
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
data/lib/restate/client.rb
CHANGED
|
@@ -81,6 +81,33 @@ module Restate
|
|
|
81
81
|
post_admin("/restate/invocations/#{invocation_id}/kill", nil)
|
|
82
82
|
end
|
|
83
83
|
|
|
84
|
+
# ── Introspection queries ──
|
|
85
|
+
|
|
86
|
+
# Execute a SQL query against Restate's introspection API (DataFusion).
|
|
87
|
+
# The admin API exposes system tables (sys_invocation, sys_journal, state,
|
|
88
|
+
# etc.) that can be queried with standard SQL.
|
|
89
|
+
#
|
|
90
|
+
# @param sql [String] a SQL query string
|
|
91
|
+
# @return [Array<Hash>] rows returned by the query
|
|
92
|
+
#
|
|
93
|
+
# @example
|
|
94
|
+
# client.execute_query("SELECT id, status FROM sys_invocation LIMIT 10")
|
|
95
|
+
def execute_query(sql) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
|
96
|
+
uri = URI("#{@admin_url}/query")
|
|
97
|
+
request = Net::HTTP::Post.new(uri)
|
|
98
|
+
request['Content-Type'] = 'application/json'
|
|
99
|
+
request['Accept'] = 'application/json'
|
|
100
|
+
@admin_headers.each { |k, v| request[k] = v }
|
|
101
|
+
request.body = JSON.generate({ query: sql })
|
|
102
|
+
response = Net::HTTP.start(uri.hostname, uri.port, # steep:ignore ArgumentTypeMismatch
|
|
103
|
+
use_ssl: uri.scheme == 'https',
|
|
104
|
+
open_timeout: 5,
|
|
105
|
+
read_timeout: 30) { |http| http.request(request) }
|
|
106
|
+
Kernel.raise "Restate query error: #{response.code} #{response.body}" unless response.is_a?(Net::HTTPSuccess)
|
|
107
|
+
body = response.body
|
|
108
|
+
body && !body.empty? ? (JSON.parse(body)['rows'] || []) : []
|
|
109
|
+
end
|
|
110
|
+
|
|
84
111
|
private
|
|
85
112
|
|
|
86
113
|
def resolve_name(service)
|
data/lib/restate/context.rb
CHANGED
|
@@ -135,6 +135,18 @@ module Restate
|
|
|
135
135
|
# Reject an awakeable with a terminal failure.
|
|
136
136
|
def reject_awakeable(awakeable_id, message, code: 500); end
|
|
137
137
|
|
|
138
|
+
# Wait for a named signal addressed to this invocation.
|
|
139
|
+
# Returns a DurableFuture that resolves once another invocation calls
|
|
140
|
+
# +resolve_signal+ or +reject_signal+ with the same name targeting this
|
|
141
|
+
# invocation's id.
|
|
142
|
+
def signal(name, serde: JsonSerde); end
|
|
143
|
+
|
|
144
|
+
# Send a success value to a named signal on another invocation.
|
|
145
|
+
def resolve_signal(invocation_id, name, payload, serde: JsonSerde); end
|
|
146
|
+
|
|
147
|
+
# Send a terminal failure to a named signal on another invocation.
|
|
148
|
+
def reject_signal(invocation_id, name, message, code: 500); end
|
|
149
|
+
|
|
138
150
|
# Request cancellation of another invocation.
|
|
139
151
|
def cancel_invocation(invocation_id); end
|
|
140
152
|
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# typed: false
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'active_record'
|
|
5
|
+
|
|
6
|
+
module Restate
|
|
7
|
+
# Arel table references for Restate's SQL introspection tables.
|
|
8
|
+
#
|
|
9
|
+
# Restate exposes a DataFusion-powered SQL endpoint at +/query+ on the admin
|
|
10
|
+
# API. These tables let you build queries using Arel's composable, type-safe
|
|
11
|
+
# predicate API — the same one ActiveRecord uses under the hood.
|
|
12
|
+
#
|
|
13
|
+
# @example Query running invocations for a service
|
|
14
|
+
# i = Restate::Sys::Invocation
|
|
15
|
+
# query = i.project(i[:id], i[:status], i[:created_at])
|
|
16
|
+
# .where(i[:target_service_name].eq("MyService"))
|
|
17
|
+
# .where(i[:status].eq("running"))
|
|
18
|
+
# .order(i[:created_at].desc)
|
|
19
|
+
# .take(50)
|
|
20
|
+
# Restate.query(query)
|
|
21
|
+
#
|
|
22
|
+
# @example Join invocations with their journal input
|
|
23
|
+
# i = Restate::Sys::Invocation
|
|
24
|
+
# j = Restate::Sys::Journal
|
|
25
|
+
# query = i.project(i[:id], i[:target], i[:status], j[:entry_json])
|
|
26
|
+
# .join(j, Arel::Nodes::OuterJoin)
|
|
27
|
+
# .on(j[:id].eq(i[:id]).and(j[:index].eq(0)))
|
|
28
|
+
# .where(i[:target_service_name].eq("CrawlPipeline"))
|
|
29
|
+
# .order(i[:created_at].desc)
|
|
30
|
+
# .take(20)
|
|
31
|
+
# Restate.query(query)
|
|
32
|
+
#
|
|
33
|
+
# @example Query virtual object state
|
|
34
|
+
# s = Restate::Sys::State
|
|
35
|
+
# query = s.project(s[:service_name], s[:service_key], s[:key], s[:value_utf8])
|
|
36
|
+
# .where(s[:service_name].eq("Counter"))
|
|
37
|
+
# Restate.query(query)
|
|
38
|
+
module Sys
|
|
39
|
+
Invocation = Arel::Table.new(:sys_invocation)
|
|
40
|
+
Journal = Arel::Table.new(:sys_journal)
|
|
41
|
+
JournalEvents = Arel::Table.new(:sys_journal_events)
|
|
42
|
+
Inbox = Arel::Table.new(:sys_inbox)
|
|
43
|
+
KeyedStatus = Arel::Table.new(:sys_keyed_service_status)
|
|
44
|
+
Service = Arel::Table.new(:sys_service)
|
|
45
|
+
Deployment = Arel::Table.new(:sys_deployment)
|
|
46
|
+
Idempotency = Arel::Table.new(:sys_idempotency)
|
|
47
|
+
Promise = Arel::Table.new(:sys_promise)
|
|
48
|
+
State = Arel::Table.new(:state)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Minimal quoting adapter for Arel's ToSql visitor. Emits ANSI SQL
|
|
52
|
+
# (double-quoted identifiers, single-quoted strings) which DataFusion expects.
|
|
53
|
+
# Allows Arel query generation without an ActiveRecord database connection.
|
|
54
|
+
#
|
|
55
|
+
# @!visibility private
|
|
56
|
+
class DataFusionQuoting
|
|
57
|
+
def quote_table_name(name)
|
|
58
|
+
"\"#{name}\""
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def quote_column_name(name)
|
|
62
|
+
"\"#{name}\""
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def quote(value)
|
|
66
|
+
case value
|
|
67
|
+
when String then "'#{value.gsub("'", "''")}'"
|
|
68
|
+
when nil then 'NULL'
|
|
69
|
+
when true then 'TRUE'
|
|
70
|
+
when false then 'FALSE'
|
|
71
|
+
else value.to_s
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def schema_cache
|
|
76
|
+
self
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def columns_hash(_table)
|
|
80
|
+
{}
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def data_source_exists?(_table)
|
|
84
|
+
true
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Arel visitor that generates ANSI SQL compatible with DataFusion.
|
|
89
|
+
# Uses DataFusionQuoting for identifier/value quoting without requiring
|
|
90
|
+
# a live database connection.
|
|
91
|
+
#
|
|
92
|
+
# @!visibility private
|
|
93
|
+
class DataFusionVisitor < Arel::Visitors::ToSql
|
|
94
|
+
def initialize
|
|
95
|
+
super(DataFusionQuoting.new)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
class << self
|
|
100
|
+
# Convert an Arel AST to a SQL string without requiring an AR connection.
|
|
101
|
+
# Uses a standalone visitor that emits ANSI SQL (DataFusion-compatible).
|
|
102
|
+
#
|
|
103
|
+
# @param arel [Arel::SelectManager] an Arel query
|
|
104
|
+
# @return [String] SQL string
|
|
105
|
+
def arel_to_sql(arel)
|
|
106
|
+
collector = Arel::Collectors::SQLString.new
|
|
107
|
+
DataFusionVisitor.new.accept(arel.ast, collector).value
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Execute an Arel query or raw SQL string against the Restate admin
|
|
111
|
+
# introspection API. Returns an array of row hashes.
|
|
112
|
+
#
|
|
113
|
+
# @param arel_or_sql [Arel::SelectManager, String] an Arel query or raw SQL
|
|
114
|
+
# @return [Array<Hash>] rows returned by Restate
|
|
115
|
+
#
|
|
116
|
+
# @example With Arel
|
|
117
|
+
# i = Restate::Sys::Invocation
|
|
118
|
+
# Restate.query(i.project(Arel.star).take(10))
|
|
119
|
+
#
|
|
120
|
+
# @example With raw SQL
|
|
121
|
+
# Restate.query("SELECT id, status FROM sys_invocation LIMIT 10")
|
|
122
|
+
def query(arel_or_sql)
|
|
123
|
+
sql = arel_or_sql.respond_to?(:ast) ? arel_to_sql(arel_or_sql) : arel_or_sql.to_s
|
|
124
|
+
client.execute_query(sql)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# typed: false
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'base64'
|
|
5
|
+
require 'set'
|
|
6
|
+
|
|
7
|
+
module Restate
|
|
8
|
+
module Middleware
|
|
9
|
+
# Detects VirtualObject deadlocks caused by re-entrant calls to a VO whose
|
|
10
|
+
# exclusive handler is still running higher up the call chain.
|
|
11
|
+
#
|
|
12
|
+
# == The problem
|
|
13
|
+
#
|
|
14
|
+
# Restate VirtualObjects serialize exclusive handler access per key. If handler A
|
|
15
|
+
# on VO key "x" calls handler B on the same VO key "x", the call will block
|
|
16
|
+
# forever — the key is already locked by A. This is a deadlock.
|
|
17
|
+
#
|
|
18
|
+
# == How it works
|
|
19
|
+
#
|
|
20
|
+
# This middleware tracks which VO keys are held by the current call chain and
|
|
21
|
+
# propagates that information via a header on every outbound call.
|
|
22
|
+
#
|
|
23
|
+
# === Inbound side
|
|
24
|
+
#
|
|
25
|
+
# 1. Reads the held-locks header from the incoming request.
|
|
26
|
+
# 2. If the current handler is an exclusive VO handler targeting a key already
|
|
27
|
+
# in the set → deadlock. Raises a {DeadlockError} immediately.
|
|
28
|
+
# 3. If this handler is an exclusive VO handler, appends its lock to the set
|
|
29
|
+
# so further downstream calls propagate it.
|
|
30
|
+
#
|
|
31
|
+
# === Outbound side
|
|
32
|
+
#
|
|
33
|
+
# Injects the held-locks header into every outbound service call. When
|
|
34
|
+
# handler metadata is available (the target service class is known), only
|
|
35
|
+
# raises for exclusive handlers — shared handler calls are safe. Falls
|
|
36
|
+
# back to raising for any same-service call when metadata is unavailable
|
|
37
|
+
# (e.g., calling by string name to an external service).
|
|
38
|
+
#
|
|
39
|
+
# == Wire format
|
|
40
|
+
#
|
|
41
|
+
# Lock entries are encoded as +base64url(service).base64url(key)+ and
|
|
42
|
+
# separated by commas. Base64url encoding ensures arbitrary service names
|
|
43
|
+
# and keys (including those containing +.+, +,+, or non-ASCII characters)
|
|
44
|
+
# are handled correctly.
|
|
45
|
+
#
|
|
46
|
+
# == Journal determinism
|
|
47
|
+
#
|
|
48
|
+
# The held-locks header is deterministic across replays: its value depends only
|
|
49
|
+
# on the execution path, which Restate's journal guarantees is identical on
|
|
50
|
+
# every replay.
|
|
51
|
+
#
|
|
52
|
+
# == Usage
|
|
53
|
+
#
|
|
54
|
+
# endpoint = Restate.endpoint(MyVirtualObject)
|
|
55
|
+
# endpoint.use(Restate::Middleware::DeadlockDetection::Inbound)
|
|
56
|
+
# endpoint.use_outbound(Restate::Middleware::DeadlockDetection::Outbound)
|
|
57
|
+
#
|
|
58
|
+
module DeadlockDetection
|
|
59
|
+
HEADER = 'x-restate-held-locks'
|
|
60
|
+
ENTRY_SEPARATOR = ','
|
|
61
|
+
FIELD_SEPARATOR = '.'
|
|
62
|
+
DEADLOCK_STATUS_CODE = 409
|
|
63
|
+
|
|
64
|
+
THREAD_KEY = :restate_held_exclusive_locks
|
|
65
|
+
|
|
66
|
+
class << self
|
|
67
|
+
# Returns the current set of held exclusive locks for this fiber.
|
|
68
|
+
# Each entry is a two-element array: [service_name, key].
|
|
69
|
+
#
|
|
70
|
+
# @return [Set<Array<String>>]
|
|
71
|
+
def held_locks
|
|
72
|
+
Thread.current[THREAD_KEY] || Set.new
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# @param locks [Set<Array<String>>]
|
|
76
|
+
def held_locks=(locks)
|
|
77
|
+
Thread.current[THREAD_KEY] = locks
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Encodes a [service, key] pair into a wire-safe string.
|
|
81
|
+
def encode_lock(service, key)
|
|
82
|
+
b64_svc = Base64.urlsafe_encode64(service, padding: false)
|
|
83
|
+
b64_key = Base64.urlsafe_encode64(key, padding: false)
|
|
84
|
+
"#{b64_svc}#{FIELD_SEPARATOR}#{b64_key}"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Decodes a wire-format lock string into [service, key].
|
|
88
|
+
# Returns nil if the format is invalid.
|
|
89
|
+
def decode_lock(encoded)
|
|
90
|
+
parts = encoded.split(FIELD_SEPARATOR, 2)
|
|
91
|
+
return nil unless parts.length == 2
|
|
92
|
+
|
|
93
|
+
svc = Base64.urlsafe_decode64(parts[0]).force_encoding('UTF-8')
|
|
94
|
+
key = Base64.urlsafe_decode64(parts[1]).force_encoding('UTF-8')
|
|
95
|
+
[svc, key]
|
|
96
|
+
rescue ArgumentError
|
|
97
|
+
nil
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Serializes a set of [service, key] lock pairs into a header value.
|
|
101
|
+
def encode_header(locks)
|
|
102
|
+
locks.map { |svc, key| encode_lock(svc, key) }.join(ENTRY_SEPARATOR)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Deserializes a header value into a Set of [service, key] pairs.
|
|
106
|
+
def decode_header(raw)
|
|
107
|
+
return Set.new if raw.nil? || raw.to_s.empty?
|
|
108
|
+
|
|
109
|
+
entries = raw.to_s.split(ENTRY_SEPARATOR).filter_map do |entry|
|
|
110
|
+
decode_lock(entry.strip)
|
|
111
|
+
end
|
|
112
|
+
Set.new(entries)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Error raised when a deadlock is detected.
|
|
117
|
+
#
|
|
118
|
+
# Uses status code 409 (Conflict) to signal that retrying won't help.
|
|
119
|
+
class DeadlockError < Restate::TerminalError
|
|
120
|
+
def initialize(message)
|
|
121
|
+
super(message, status_code: DEADLOCK_STATUS_CODE)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Inbound middleware that checks for and tracks VO locks.
|
|
126
|
+
#
|
|
127
|
+
# Register with: +endpoint.use(Restate::Middleware::DeadlockDetection::Inbound)+
|
|
128
|
+
#
|
|
129
|
+
# @example
|
|
130
|
+
# endpoint = Restate.endpoint(MyVirtualObject)
|
|
131
|
+
# endpoint.use(Restate::Middleware::DeadlockDetection::Inbound)
|
|
132
|
+
class Inbound
|
|
133
|
+
def call(handler, ctx)
|
|
134
|
+
previous = DeadlockDetection.held_locks
|
|
135
|
+
incoming = parse_locks(ctx)
|
|
136
|
+
check_and_track_lock!(handler, ctx, incoming)
|
|
137
|
+
DeadlockDetection.held_locks = incoming
|
|
138
|
+
yield
|
|
139
|
+
ensure
|
|
140
|
+
DeadlockDetection.held_locks = previous
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
private
|
|
144
|
+
|
|
145
|
+
def check_and_track_lock!(handler, ctx, incoming)
|
|
146
|
+
return unless handler.service_tag.kind == 'object'
|
|
147
|
+
return unless handler.kind == 'exclusive'
|
|
148
|
+
|
|
149
|
+
key = ctx.respond_to?(:key) ? ctx.key : nil
|
|
150
|
+
return unless key
|
|
151
|
+
|
|
152
|
+
svc = handler.service_tag.name
|
|
153
|
+
lock = [svc, key]
|
|
154
|
+
raise_deadlock!(svc, handler.name, key, incoming) if incoming.include?(lock)
|
|
155
|
+
incoming << lock
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def raise_deadlock!(svc, handler_name, key, locks)
|
|
159
|
+
held = locks.map { |s, k| "#{s}:#{k}" }.join(', ')
|
|
160
|
+
msg = "Deadlock detected: #{svc}##{handler_name} on key '#{key}' " \
|
|
161
|
+
'called while an exclusive handler holds the same VO key. ' \
|
|
162
|
+
"Held locks: #{held}. " \
|
|
163
|
+
'This call will never complete.'
|
|
164
|
+
Kernel.raise DeadlockError, msg
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def parse_locks(ctx)
|
|
168
|
+
headers = ctx.request.headers
|
|
169
|
+
raw = headers.is_a?(Hash) ? headers[HEADER] : nil
|
|
170
|
+
DeadlockDetection.decode_header(raw)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Outbound middleware that propagates held locks via headers.
|
|
175
|
+
#
|
|
176
|
+
# When handler metadata is available (via Thread.current[:restate_outbound_handler_meta]),
|
|
177
|
+
# shared handler calls are allowed through — only exclusive handlers can deadlock.
|
|
178
|
+
# When metadata is unavailable (external service called by string name), falls
|
|
179
|
+
# back to raising for any same-service call.
|
|
180
|
+
#
|
|
181
|
+
# Register with: +endpoint.use_outbound(Restate::Middleware::DeadlockDetection::Outbound)+
|
|
182
|
+
#
|
|
183
|
+
# @example
|
|
184
|
+
# endpoint = Restate.endpoint(MyVirtualObject)
|
|
185
|
+
# endpoint.use_outbound(Restate::Middleware::DeadlockDetection::Outbound)
|
|
186
|
+
class Outbound
|
|
187
|
+
def call(service, handler, headers)
|
|
188
|
+
locks = DeadlockDetection.held_locks
|
|
189
|
+
propagate_and_check!(service, handler, headers, locks) if locks.any?
|
|
190
|
+
yield
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
private
|
|
194
|
+
|
|
195
|
+
def propagate_and_check!(service, handler, headers, locks)
|
|
196
|
+
headers[HEADER] = DeadlockDetection.encode_header(locks)
|
|
197
|
+
|
|
198
|
+
held_lock = locks.find { |svc, _key| svc == service }
|
|
199
|
+
return unless held_lock
|
|
200
|
+
|
|
201
|
+
return if target_shared?
|
|
202
|
+
|
|
203
|
+
msg = "Deadlock detected: outbound call to #{service}##{handler} " \
|
|
204
|
+
"while exclusive lock held on #{held_lock[0]}:#{held_lock[1]}. " \
|
|
205
|
+
'This call will block forever.'
|
|
206
|
+
Kernel.raise DeadlockError, msg
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def target_shared?
|
|
210
|
+
meta = Thread.current[:restate_outbound_handler_meta]
|
|
211
|
+
meta&.kind == 'shared'
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# typed: false
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Restate
|
|
5
|
+
# Rails integration for the Restate SDK. Automatically loads the
|
|
6
|
+
# introspection module which provides Arel-based query support for
|
|
7
|
+
# Restate's SQL introspection API (powered by DataFusion).
|
|
8
|
+
#
|
|
9
|
+
# When Rails is present, +Restate::Sys+ table constants become available
|
|
10
|
+
# for building type-safe, composable queries against system tables like
|
|
11
|
+
# +sys_invocation+, +sys_journal+, and +state+.
|
|
12
|
+
class Railtie < Rails::Railtie
|
|
13
|
+
initializer 'restate.introspection' do
|
|
14
|
+
require_relative 'introspection'
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -206,7 +206,7 @@ module Restate
|
|
|
206
206
|
in_serde = resolve_serde(input_serde, handler_meta, :input_serde)
|
|
207
207
|
out_serde = resolve_serde(output_serde, handler_meta, :output_serde)
|
|
208
208
|
parameter = in_serde.serialize(arg)
|
|
209
|
-
with_outbound_middleware(svc_name, handler_name, headers) do |hdrs|
|
|
209
|
+
with_outbound_middleware(svc_name, handler_name, headers, handler_meta: handler_meta) do |hdrs|
|
|
210
210
|
call_handle = @vm.sys_call(
|
|
211
211
|
service: svc_name, handler: handler_name, parameter: parameter,
|
|
212
212
|
key: key, idempotency_key: idempotency_key, headers: hdrs
|
|
@@ -223,7 +223,7 @@ module Restate
|
|
|
223
223
|
in_serde = resolve_serde(input_serde, handler_meta, :input_serde)
|
|
224
224
|
parameter = in_serde.serialize(arg)
|
|
225
225
|
delay_ms = delay ? (delay * 1000).to_i : nil
|
|
226
|
-
with_outbound_middleware(svc_name, handler_name, headers) do |hdrs|
|
|
226
|
+
with_outbound_middleware(svc_name, handler_name, headers, handler_meta: handler_meta) do |hdrs|
|
|
227
227
|
invocation_id_handle = @vm.sys_send(
|
|
228
228
|
service: svc_name, handler: handler_name, parameter: parameter,
|
|
229
229
|
key: key, delay: delay_ms, idempotency_key: idempotency_key, headers: hdrs
|
|
@@ -239,7 +239,7 @@ module Restate
|
|
|
239
239
|
in_serde = resolve_serde(input_serde, handler_meta, :input_serde)
|
|
240
240
|
out_serde = resolve_serde(output_serde, handler_meta, :output_serde)
|
|
241
241
|
parameter = in_serde.serialize(arg)
|
|
242
|
-
with_outbound_middleware(svc_name, handler_name, headers) do |hdrs|
|
|
242
|
+
with_outbound_middleware(svc_name, handler_name, headers, handler_meta: handler_meta) do |hdrs|
|
|
243
243
|
call_handle = @vm.sys_call(
|
|
244
244
|
service: svc_name, handler: handler_name, parameter: parameter,
|
|
245
245
|
key: key, idempotency_key: idempotency_key, headers: hdrs
|
|
@@ -256,7 +256,7 @@ module Restate
|
|
|
256
256
|
in_serde = resolve_serde(input_serde, handler_meta, :input_serde)
|
|
257
257
|
parameter = in_serde.serialize(arg)
|
|
258
258
|
delay_ms = delay ? (delay * 1000).to_i : nil
|
|
259
|
-
with_outbound_middleware(svc_name, handler_name, headers) do |hdrs|
|
|
259
|
+
with_outbound_middleware(svc_name, handler_name, headers, handler_meta: handler_meta) do |hdrs|
|
|
260
260
|
invocation_id_handle = @vm.sys_send(
|
|
261
261
|
service: svc_name, handler: handler_name, parameter: parameter,
|
|
262
262
|
key: key, delay: delay_ms, idempotency_key: idempotency_key, headers: hdrs
|
|
@@ -298,6 +298,25 @@ module Restate
|
|
|
298
298
|
@vm.sys_complete_awakeable_failure(awakeable_id, failure)
|
|
299
299
|
end
|
|
300
300
|
|
|
301
|
+
# ── Signals ──
|
|
302
|
+
|
|
303
|
+
# Wait for a named signal addressed to this invocation. Returns a DurableFuture.
|
|
304
|
+
def signal(name, serde: JsonSerde)
|
|
305
|
+
handle = @vm.sys_signal(name)
|
|
306
|
+
DurableFuture.new(self, handle, serde: serde)
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# Send a success value to a named signal on another invocation.
|
|
310
|
+
def resolve_signal(invocation_id, name, payload, serde: JsonSerde)
|
|
311
|
+
@vm.sys_complete_signal_success(invocation_id, name, serde.serialize(payload))
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# Send a terminal failure to a named signal on another invocation.
|
|
315
|
+
def reject_signal(invocation_id, name, message, code: 500)
|
|
316
|
+
failure = Failure.new(code: code, message: message)
|
|
317
|
+
@vm.sys_complete_signal_failure(invocation_id, name, failure)
|
|
318
|
+
end
|
|
319
|
+
|
|
301
320
|
# ── Promises (Workflow API) ──
|
|
302
321
|
|
|
303
322
|
# Gets a durable promise value, blocking until resolved.
|
|
@@ -472,18 +491,25 @@ module Restate
|
|
|
472
491
|
# Runs outbound middleware chain (Sidekiq client middleware pattern).
|
|
473
492
|
# Each middleware gets +call(service, handler, headers)+ and must +yield+
|
|
474
493
|
# to continue the chain. The block at the end performs the actual VM call.
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
494
|
+
#
|
|
495
|
+
# The optional +handler_meta+ (a Handler struct from resolve_call_target)
|
|
496
|
+
# is exposed via Thread.current[:restate_outbound_handler_meta] so that
|
|
497
|
+
# middleware can inspect the target handler's kind without changing the
|
|
498
|
+
# middleware interface.
|
|
499
|
+
def with_outbound_middleware(service, handler, headers, handler_meta: nil, &action)
|
|
500
|
+
return action.call(headers) if @outbound_middleware.empty?
|
|
501
|
+
|
|
502
|
+
h = headers || {}
|
|
503
|
+
previous_meta = Thread.current[:restate_outbound_handler_meta]
|
|
504
|
+
Thread.current[:restate_outbound_handler_meta] = handler_meta
|
|
505
|
+
chain = ->(hdrs) { action.call(hdrs) }
|
|
506
|
+
@outbound_middleware.reverse_each do |mw|
|
|
507
|
+
prev = chain
|
|
508
|
+
chain = ->(hdrs) { mw.call(service, handler, hdrs) { prev.call(hdrs) } }
|
|
486
509
|
end
|
|
510
|
+
chain.call(h)
|
|
511
|
+
ensure
|
|
512
|
+
Thread.current[:restate_outbound_handler_meta] = previous_meta
|
|
487
513
|
end
|
|
488
514
|
|
|
489
515
|
# ── Call target resolution ──
|
data/lib/restate/version.rb
CHANGED
data/lib/restate/vm.rb
CHANGED
|
@@ -223,6 +223,19 @@ module Restate
|
|
|
223
223
|
@vm.sys_cancel_invocation(invocation_id)
|
|
224
224
|
end
|
|
225
225
|
|
|
226
|
+
def sys_signal(name)
|
|
227
|
+
@vm.sys_signal(name)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def sys_complete_signal_success(invocation_id, name, value)
|
|
231
|
+
@vm.sys_complete_signal_success(invocation_id, name, value)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def sys_complete_signal_failure(invocation_id, name, failure)
|
|
235
|
+
native_failure = Internal::Failure.new(failure.code, failure.message, nil)
|
|
236
|
+
@vm.sys_complete_signal_failure(invocation_id, name, native_failure)
|
|
237
|
+
end
|
|
238
|
+
|
|
226
239
|
private
|
|
227
240
|
|
|
228
241
|
def map_do_progress(result)
|
data/lib/restate.rb
CHANGED
|
@@ -18,6 +18,8 @@ require_relative 'restate/endpoint'
|
|
|
18
18
|
require_relative 'restate/service_proxy'
|
|
19
19
|
require_relative 'restate/config'
|
|
20
20
|
require_relative 'restate/client'
|
|
21
|
+
require_relative 'restate/middleware/deadlock_detection'
|
|
22
|
+
require_relative 'restate/railtie' if defined?(Rails::Railtie)
|
|
21
23
|
|
|
22
24
|
# Restate Ruby SDK — build resilient applications with durable execution.
|
|
23
25
|
#
|
|
@@ -240,6 +242,23 @@ module Restate # rubocop:disable Metrics/ModuleLength
|
|
|
240
242
|
fetch_context!.reject_awakeable(awakeable_id, message, code: code)
|
|
241
243
|
end
|
|
242
244
|
|
|
245
|
+
# ── Signals ──
|
|
246
|
+
|
|
247
|
+
# Wait for a named signal addressed to this invocation. Returns a DurableFuture.
|
|
248
|
+
def signal(name, serde: JsonSerde)
|
|
249
|
+
fetch_context!.signal(name, serde: serde)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Send a success value to a named signal on another invocation.
|
|
253
|
+
def resolve_signal(invocation_id, name, payload, serde: JsonSerde)
|
|
254
|
+
fetch_context!.resolve_signal(invocation_id, name, payload, serde: serde)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Send a terminal failure to a named signal on another invocation.
|
|
258
|
+
def reject_signal(invocation_id, name, message, code: 500)
|
|
259
|
+
fetch_context!.reject_signal(invocation_id, name, message, code: code)
|
|
260
|
+
end
|
|
261
|
+
|
|
243
262
|
# ── Promises (Workflow only) ──
|
|
244
263
|
|
|
245
264
|
# Get a durable promise value, blocking until resolved.
|
data/sig/restate.rbs
CHANGED
|
@@ -8,6 +8,8 @@ module Restate
|
|
|
8
8
|
def self.configure: () { (Config) -> void } -> void
|
|
9
9
|
def self.config: () -> Config
|
|
10
10
|
def self.client: () -> Client
|
|
11
|
+
def self.query: (untyped arel_or_sql) -> Array[Hash[String, untyped]]
|
|
12
|
+
def self.arel_to_sql: (untyped arel) -> String
|
|
11
13
|
|
|
12
14
|
# ── Durable execution ──
|
|
13
15
|
|
|
@@ -42,6 +44,12 @@ module Restate
|
|
|
42
44
|
def self.resolve_awakeable: (String awakeable_id, untyped payload, ?serde: untyped) -> void
|
|
43
45
|
def self.reject_awakeable: (String awakeable_id, String message, ?code: Integer) -> void
|
|
44
46
|
|
|
47
|
+
# ── Signals ──
|
|
48
|
+
|
|
49
|
+
def self.signal: (String name, ?serde: untyped) -> DurableFuture
|
|
50
|
+
def self.resolve_signal: (String invocation_id, String name, untyped payload, ?serde: untyped) -> void
|
|
51
|
+
def self.reject_signal: (String invocation_id, String name, String message, ?code: Integer) -> void
|
|
52
|
+
|
|
45
53
|
# ── Promises ──
|
|
46
54
|
|
|
47
55
|
def self.promise: (String name, ?serde: untyped) -> untyped
|
|
@@ -118,6 +126,7 @@ module Restate
|
|
|
118
126
|
def reject_awakeable: (String awakeable_id, String message, ?code: Integer) -> void
|
|
119
127
|
def cancel_invocation: (String invocation_id) -> void
|
|
120
128
|
def kill_invocation: (String invocation_id) -> void
|
|
129
|
+
def execute_query: (String sql) -> Array[Hash[String, untyped]]
|
|
121
130
|
|
|
122
131
|
private
|
|
123
132
|
|
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.11.0
|
|
5
5
|
platform: aarch64-linux
|
|
6
6
|
authors:
|
|
7
7
|
- Restate Developers
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-05-12 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: async
|
|
@@ -54,7 +54,6 @@ files:
|
|
|
54
54
|
- ext/restate_internal/extconf.rb
|
|
55
55
|
- ext/restate_internal/src/lib.rs
|
|
56
56
|
- lib/restate.rb
|
|
57
|
-
- lib/restate/3.2/restate_internal.so
|
|
58
57
|
- lib/restate/3.3/restate_internal.so
|
|
59
58
|
- lib/restate/3.4/restate_internal.so
|
|
60
59
|
- lib/restate/4.0/restate_internal.so
|
|
@@ -66,6 +65,9 @@ files:
|
|
|
66
65
|
- lib/restate/endpoint.rb
|
|
67
66
|
- lib/restate/errors.rb
|
|
68
67
|
- lib/restate/handler.rb
|
|
68
|
+
- lib/restate/introspection.rb
|
|
69
|
+
- lib/restate/middleware/deadlock_detection.rb
|
|
70
|
+
- lib/restate/railtie.rb
|
|
69
71
|
- lib/restate/serde.rb
|
|
70
72
|
- lib/restate/server.rb
|
|
71
73
|
- lib/restate/server_context.rb
|
|
@@ -91,7 +93,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
91
93
|
requirements:
|
|
92
94
|
- - ">="
|
|
93
95
|
- !ruby/object:Gem::Version
|
|
94
|
-
version: '3.
|
|
96
|
+
version: '3.3'
|
|
95
97
|
- - "<"
|
|
96
98
|
- !ruby/object:Gem::Version
|
|
97
99
|
version: 4.1.dev
|
|
Binary file
|