Module: Restate::Middleware::DeadlockDetection
- Defined in:
- lib/restate/middleware/deadlock_detection.rb
Overview
Detects VirtualObject deadlocks caused by re-entrant calls to a VO whose exclusive handler is still running higher up the call chain.
The problem
Restate VirtualObjects serialize exclusive handler access per key. If handler A on VO key “x” calls handler B on the same VO key “x”, the call will block forever — the key is already locked by A. This is a deadlock.
How it works
This middleware tracks which VO keys are held by the current call chain and propagates that information via a header on every outbound call.
Inbound side
-
Reads the held-locks header from the incoming request.
-
If the current handler is an exclusive VO handler targeting a key already in the set → deadlock. Raises a DeadlockError immediately.
-
If this handler is an exclusive VO handler, appends its lock to the set so further downstream calls propagate it.
Outbound side
Injects the held-locks header into every outbound service call. When handler metadata is available (the target service class is known), only raises for exclusive handlers — shared handler calls are safe. Falls back to raising for any same-service call when metadata is unavailable (e.g., calling by string name to an external service).
Wire format
Lock entries are encoded as base64url(service).base64url(key) and separated by commas. Base64url encoding ensures arbitrary service names and keys (including those containing ., ,, or non-ASCII characters) are handled correctly.
Journal determinism
The held-locks header is deterministic across replays: its value depends only on the execution path, which Restate’s journal guarantees is identical on every replay.
Usage
endpoint = Restate.endpoint(MyVirtualObject)
endpoint.use(Restate::Middleware::DeadlockDetection::Inbound)
endpoint.use_outbound(Restate::Middleware::DeadlockDetection::Outbound)
Defined Under Namespace
Classes: DeadlockError, Inbound, Outbound
Constant Summary collapse
- HEADER =
'x-restate-held-locks'- ENTRY_SEPARATOR =
','- FIELD_SEPARATOR =
'.'- DEADLOCK_STATUS_CODE =
409- THREAD_KEY =
:restate_held_exclusive_locks
Class Method Summary collapse
-
.decode_header(raw) ⇒ Object
Deserializes a header value into a Set of [service, key] pairs.
-
.decode_lock(encoded) ⇒ Object
Decodes a wire-format lock string into [service, key].
-
.encode_header(locks) ⇒ Object
Serializes a set of [service, key] lock pairs into a header value.
-
.encode_lock(service, key) ⇒ Object
Encodes a [service, key] pair into a wire-safe string.
-
.held_locks ⇒ Set<Array<String>>
Returns the current set of held exclusive locks for this fiber.
- .held_locks=(locks) ⇒ Object
Class Method Details
.decode_header(raw) ⇒ Object
Deserializes a header value into a Set of [service, key] pairs.
106 107 108 109 110 111 112 113 |
# File 'lib/restate/middleware/deadlock_detection.rb', line 106 def decode_header(raw) return Set.new if raw.nil? || raw.to_s.empty? entries = raw.to_s.split(ENTRY_SEPARATOR).filter_map do |entry| decode_lock(entry.strip) end Set.new(entries) end |
.decode_lock(encoded) ⇒ Object
Decodes a wire-format lock string into [service, key]. Returns nil if the format is invalid.
89 90 91 92 93 94 95 96 97 98 |
# File 'lib/restate/middleware/deadlock_detection.rb', line 89 def decode_lock(encoded) parts = encoded.split(FIELD_SEPARATOR, 2) return nil unless parts.length == 2 svc = Base64.urlsafe_decode64(parts[0]).force_encoding('UTF-8') key = Base64.urlsafe_decode64(parts[1]).force_encoding('UTF-8') [svc, key] rescue ArgumentError nil end |
.encode_header(locks) ⇒ Object
Serializes a set of [service, key] lock pairs into a header value.
101 102 103 |
# File 'lib/restate/middleware/deadlock_detection.rb', line 101 def encode_header(locks) locks.map { |svc, key| encode_lock(svc, key) }.join(ENTRY_SEPARATOR) end |
.encode_lock(service, key) ⇒ Object
Encodes a [service, key] pair into a wire-safe string.
81 82 83 84 85 |
# File 'lib/restate/middleware/deadlock_detection.rb', line 81 def encode_lock(service, key) b64_svc = Base64.urlsafe_encode64(service, padding: false) b64_key = Base64.urlsafe_encode64(key, padding: false) "#{b64_svc}#{FIELD_SEPARATOR}#{b64_key}" end |
.held_locks ⇒ Set<Array<String>>
Returns the current set of held exclusive locks for this fiber. Each entry is a two-element array: [service_name, key].
71 72 73 |
# File 'lib/restate/middleware/deadlock_detection.rb', line 71 def held_locks Thread.current[THREAD_KEY] || Set.new end |
.held_locks=(locks) ⇒ Object
76 77 78 |
# File 'lib/restate/middleware/deadlock_detection.rb', line 76 def held_locks=(locks) Thread.current[THREAD_KEY] = locks end |