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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1e2eb5b1db06208811ae8417ecd1239ae9490c99f758a6bee9ed976e7039dec1
4
- data.tar.gz: 4fe4be111904001ec7425be6298c2c4d8591164280d38f609a85e2da467d3ebe
3
+ metadata.gz: e63ec315f5d51fb381c2881bcf59887c4b87759ea862768eb7c19ca1679e126c
4
+ data.tar.gz: 5e08e3adde55ed23702eb07a4ee39af293aba626f6ade11d1b2397b24eedaa5a
5
5
  SHA512:
6
- metadata.gz: ad9226b9ae4786d06bb7bd47fcd26ec7c3e75de511b854a78d6f99eb6f8cbd27053ac196bd6a83e70ce7a729da536f8204ce4b0cb0862c44c66cbcf14c355238
7
- data.tar.gz: c2de41ab47f9324a9721e21dad91f661268884676c9c276893dc1bb5fe2debba2d74a85a5e894f78455c5eb49037cd66cc97f43803f5f78b07b4576c97483943
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.69.5"
34
+ version = "0.72.1"
35
35
  source = "registry+https://github.com/rust-lang/crates.io-index"
36
- checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088"
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.124"
484
+ version = "0.9.128"
493
485
  source = "registry+https://github.com/rust-lang/crates.io-index"
494
- checksum = "c85c4188462601e2aa1469def389c17228566f82ea72f137ed096f21591bc489"
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.124"
493
+ version = "0.9.128"
502
494
  source = "registry+https://github.com/rust-lang/crates.io-index"
503
- checksum = "568068db4102230882e6d4ae8de6632e224ca75fe5970f6e026a04e91ed635d3"
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.9.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 = "1.1.0"
588
+ version = "2.1.2"
597
589
  source = "registry+https://github.com/rust-lang/crates.io-index"
598
- checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
590
+ checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
599
591
 
600
592
  [[package]]
601
593
  name = "rustversion"
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "restate_internal"
3
- version = "0.9.0"
3
+ version = "0.11.0"
4
4
  edition = "2021"
5
5
  publish = false
6
6
 
@@ -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
@@ -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)
@@ -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
- def with_outbound_middleware(service, handler, headers, &action)
476
- if @outbound_middleware.empty?
477
- action.call(headers)
478
- else
479
- h = headers || {}
480
- chain = ->(hdrs) { action.call(hdrs) }
481
- @outbound_middleware.reverse_each do |mw|
482
- prev = chain
483
- chain = ->(hdrs) { mw.call(service, handler, hdrs) { prev.call(hdrs) } }
484
- end
485
- chain.call(h)
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 ──
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Restate
5
- VERSION = '0.9.0'
5
+ VERSION = '0.11.0'
6
6
  end
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.9.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-03-23 00:00:00.000000000 Z
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.2'
96
+ version: '3.3'
95
97
  - - "<"
96
98
  - !ruby/object:Gem::Version
97
99
  version: 4.1.dev
Binary file