restate-sdk 0.10.0-aarch64-linux → 0.12.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: 80a62b147724910e3329f4263e61a290ccc55e6f0c097ecc0fad4ece63ef2407
4
- data.tar.gz: 33661eccbb66c39e1ca6c57ea735e4ef74fa79a712e05136c7d0624720484ba9
3
+ metadata.gz: 2cd0022b788463ddb227407d085949183238eb47f8ade847cab75b78890b0200
4
+ data.tar.gz: f606a03c882e764eef66bb83c87dd0eb74c11032665c4b9d48a348638b1ffbc6
5
5
  SHA512:
6
- metadata.gz: e0b1fec80f09851613ccb2c4c04ec677566f845b47e51ae5a248d001e4fa9400ed096b574f475551ac3c29e9ca63775a27be799e9876dc2c49a5c4a45e06a466
7
- data.tar.gz: 4bbacd32b9999dfdec05918bf7eddb191a2e472b20c977dbab6b898c2bf8b2a6e8600649b2a6bc86fd8072368deab819e96e7b64c642dd2d17434be6084845d2
6
+ metadata.gz: 6e7b4fbe8225f2a6e06b6a5427738e917c7520aa27260cbead09c3dccdacbdf02899fb5e5c27bceff20eeed02dddd026afedbcf5138c4be13f386525d807f00d
7
+ data.tar.gz: d756190c93786ffce73f70a0ed109b3547a26ee68dd4c58d55c5c51726fee2adc579fd5ab14a119499b27c41e7a860bbb0547af45638d96bf55c1e013d8c722e
data/Cargo.lock CHANGED
@@ -561,7 +561,7 @@ dependencies = [
561
561
 
562
562
  [[package]]
563
563
  name = "restate_internal"
564
- version = "0.10.0"
564
+ version = "0.12.0"
565
565
  dependencies = [
566
566
  "magnus",
567
567
  "rb-sys",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "restate_internal"
3
- version = "0.10.0"
3
+ version = "0.12.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
@@ -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
 
@@ -133,8 +133,8 @@ module Restate
133
133
 
134
134
  # Build and return the Rack-compatible application.
135
135
  def app
136
- require_relative 'server'
137
- Server.new(self)
136
+ require_relative 'server/handler'
137
+ Server::Handler.new(self)
138
138
  end
139
139
 
140
140
  private
@@ -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