julewire-ractor 1.0.0 → 1.1.0
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/CHANGELOG.md +20 -0
- data/docs/bridge.md +26 -6
- data/julewire-ractor.gemspec +1 -1
- data/lib/julewire/ractor/bridge/bridge_thread.rb +2 -2
- data/lib/julewire/ractor/bridge/stats.rb +2 -1
- data/lib/julewire/ractor/bridge.rb +29 -13
- data/lib/julewire/ractor/child_stats.rb +14 -18
- data/lib/julewire/ractor/destination.rb +152 -87
- data/lib/julewire/ractor/destination_worker.rb +40 -45
- data/lib/julewire/ractor/fanout.rb +28 -21
- data/lib/julewire/ractor/port_lifecycle.rb +7 -1
- data/lib/julewire/ractor/remote_payload.rb +9 -11
- data/lib/julewire/ractor/remote_runtime.rb +42 -33
- data/lib/julewire/ractor/remote_serializer.rb +27 -0
- data/lib/julewire/ractor/remote_summary_record.rb +1 -1
- data/lib/julewire/ractor/reply_timeout_scheduler.rb +15 -12
- data/lib/julewire/ractor/version.rb +1 -1
- data/lib/julewire/ractor/worker_handshake.rb +28 -0
- data/lib/julewire/ractor.rb +2 -7
- metadata +6 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a0b295ac53d6eea75e9313e6274e039cf88f0c6b9d990b3152035f7cd193b3cf
|
|
4
|
+
data.tar.gz: d3992f7083c8b612d59d8104dd46d76f58c94db851c7c4a8cfa090de45de8618
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 819d50753851ad531bb413f593173b6c4b6cd08fda9f5a1bfdfcd3f72d6c9ef12aebe980cbdde9bed2f9f7234865a39162b3e488b8ef05544f3703403900a65a
|
|
7
|
+
data.tar.gz: 6880dca6536397902cd131d0f9f2243336bde72df17ddbcbd89918f11429795d0eba5924acf58c651a3edcad341718bd2ccb154bf26f2313a85dcdfb3e415008
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
## Unreleased
|
|
2
2
|
|
|
3
|
+
## 1.1.0 - 2026-07-20
|
|
4
|
+
|
|
5
|
+
- Require finite request timeouts and bound destination worker startup and
|
|
6
|
+
shutdown so lifecycle operations cannot wait forever.
|
|
7
|
+
- Harden queue accounting, fanout validation, worker stats, and Ractor-safe
|
|
8
|
+
reply-timeout cleanup.
|
|
9
|
+
- Discard inherited Ractor handles before rebuilding destination workers in a
|
|
10
|
+
forked child process.
|
|
11
|
+
- Normalize application emit input before strict bridge serialization while
|
|
12
|
+
keeping integration-owned emits on a non-normalizing Symbol-key path.
|
|
13
|
+
- Require julewire-core 1.1.0.
|
|
14
|
+
|
|
15
|
+
## 1.0.1 - 2026-06-25
|
|
16
|
+
|
|
17
|
+
- Reserve ractor destination queue slots before send and roll them back on
|
|
18
|
+
send failure.
|
|
19
|
+
- Count impossible queue-slot over-release events for destination health
|
|
20
|
+
debugging.
|
|
21
|
+
- Require julewire-core 1.0.1.
|
|
22
|
+
|
|
3
23
|
## 1.0.0 - 2026-06-21
|
|
4
24
|
|
|
5
25
|
- Initial release: Ruby 4 ractor bridge, child-runtime forwarding, remote
|
data/docs/bridge.md
CHANGED
|
@@ -29,11 +29,18 @@ Values are serialized before crossing the ractor boundary. Parent-side
|
|
|
29
29
|
processors see log-safe scalar values rather than the child object's original
|
|
30
30
|
identity or class.
|
|
31
31
|
|
|
32
|
+
The child public facade canonicalizes application input, including nested
|
|
33
|
+
String keys and unknown top-level payload fields, before strict bridge
|
|
34
|
+
serialization. Integration facade emits use a separate owned path: every key
|
|
35
|
+
must already be a Symbol and unknown record fields are rejected. The serializer
|
|
36
|
+
never decides which contract applies and never normalizes bridge data.
|
|
37
|
+
|
|
32
38
|
The serialized envelope is a ractor concern. Core keeps only the narrow
|
|
33
39
|
`emit_envelope` hook that accepts already-extracted input, context, carry, and
|
|
34
40
|
a scope snapshot. Payload parsing and scope reconstruction stay in this gem.
|
|
35
|
-
That hook is parent-runtime SPI; the
|
|
36
|
-
facade emit methods instead of a
|
|
41
|
+
That hook is parent-runtime SPI; the bridge marks ractor wire data as owned, and
|
|
42
|
+
the child `RemoteRuntime` exposes the normal facade emit methods instead of a
|
|
43
|
+
detached-envelope API.
|
|
37
44
|
|
|
38
45
|
## Available in Child
|
|
39
46
|
|
|
@@ -105,15 +112,28 @@ synchronously. The destination then sends each immutable record to a worker
|
|
|
105
112
|
ractor. The worker owns formatter, encoder, byte-limit checks, and output
|
|
106
113
|
writes.
|
|
107
114
|
|
|
115
|
+
All hashes exchanged between Julewire gems, the parent bridge, and worker
|
|
116
|
+
ractors use Symbol keys at every depth. This is an internal protocol, not an
|
|
117
|
+
input-normalization boundary: String keys and malformed or unknown commands are
|
|
118
|
+
protocol errors. They are raised and recorded at the owning bridge/worker
|
|
119
|
+
lifecycle boundary instead of being converted, ignored, or replaced with empty
|
|
120
|
+
values. String-key normalization remains limited to public application input
|
|
121
|
+
and decoded propagation carriers.
|
|
122
|
+
|
|
108
123
|
The destination has a bounded parent-side in-flight queue. When `max_queue` is
|
|
109
124
|
full, new records are dropped and counted in destination health. `flush` sends a
|
|
110
125
|
request to the worker and waits for all earlier records to finish. `close`
|
|
111
126
|
flushes or closes the worker-owned output and stops the worker.
|
|
112
127
|
|
|
113
|
-
Unlike direct core destinations, ractor-backed destinations
|
|
114
|
-
`flush(timeout: nil)` and `close(timeout: nil)`
|
|
115
|
-
instead of an unbounded wait. Worker ractors
|
|
116
|
-
|
|
128
|
+
Unlike direct core destinations, ractor-backed destinations require a finite
|
|
129
|
+
configured request timeout. `flush(timeout: nil)` and `close(timeout: nil)` use
|
|
130
|
+
that configured timeout instead of creating an unbounded wait. Worker ractors
|
|
131
|
+
can die or stop replying; the parent observes worker termination as well as the
|
|
132
|
+
reply and must not park forever while draining diagnostics.
|
|
133
|
+
|
|
134
|
+
Worker startup has a fixed one-second bound, independent of the request timeout.
|
|
135
|
+
A worker that does not hand back its command port raises
|
|
136
|
+
`Julewire::Core::Error` instead of blocking construction.
|
|
117
137
|
|
|
118
138
|
Formatter, encoder, and output must be ractor-copyable or shareable. Avoid
|
|
119
139
|
singleton-method/proc-backed output objects; plain class instances with
|
data/julewire-ractor.gemspec
CHANGED
|
@@ -27,7 +27,7 @@ module Julewire
|
|
|
27
27
|
rescue StandardError => e
|
|
28
28
|
bridge_error = e
|
|
29
29
|
warn_bridge_stopped(e)
|
|
30
|
-
|
|
30
|
+
PortLifecycle.close(port)
|
|
31
31
|
break
|
|
32
32
|
end
|
|
33
33
|
ensure
|
|
@@ -35,7 +35,7 @@ module Julewire
|
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
def close_message?(message)
|
|
38
|
-
message.
|
|
38
|
+
message.instance_of?(Hash) && message[:command] == :close
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
def monitor_message?(message)
|
|
@@ -40,6 +40,7 @@ module Julewire
|
|
|
40
40
|
experimental: true,
|
|
41
41
|
failure_count: @failure_count.value,
|
|
42
42
|
last_error_class: @last_error.get&.fetch(:class),
|
|
43
|
+
last_error_message: @last_error.get&.fetch(:message),
|
|
43
44
|
messages: @message_count.value,
|
|
44
45
|
started_threads: @started_threads.value,
|
|
45
46
|
stopped_threads: @stopped_threads.value
|
|
@@ -66,7 +67,7 @@ module Julewire
|
|
|
66
67
|
|
|
67
68
|
def record_failure(error)
|
|
68
69
|
@failure_count.increment
|
|
69
|
-
@last_error.set({ class: error.class.name })
|
|
70
|
+
@last_error.set({ class: error.class.name, message: error.message })
|
|
70
71
|
end
|
|
71
72
|
end
|
|
72
73
|
end
|
|
@@ -17,14 +17,14 @@ module Julewire
|
|
|
17
17
|
|
|
18
18
|
def enabled? = ENABLED.get
|
|
19
19
|
|
|
20
|
-
def
|
|
20
|
+
def start(args:, name:, runtime:, &)
|
|
21
21
|
unless enabled?
|
|
22
22
|
raise Core::Error, "Julewire.ractor is experimental; call Julewire.enable_experimental_ractor! first"
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
RuntimeValidation.validate!(runtime)
|
|
26
26
|
|
|
27
|
-
envelope = Core::Propagation.
|
|
27
|
+
envelope = Core::Propagation.capture_local
|
|
28
28
|
body = ::Ractor.shareable_proc(&)
|
|
29
29
|
port = ::Ractor::Port.new
|
|
30
30
|
ractor = spawn_ractor(
|
|
@@ -58,19 +58,23 @@ module Julewire
|
|
|
58
58
|
def monitor_ractor(ractor)
|
|
59
59
|
return unless ractor
|
|
60
60
|
|
|
61
|
-
::Ractor::Port.new
|
|
61
|
+
monitor_port = ::Ractor::Port.new
|
|
62
|
+
return unless monitor_port.instance_of?(::Ractor::Port)
|
|
63
|
+
|
|
64
|
+
ractor.monitor(monitor_port)
|
|
65
|
+
monitor_port
|
|
62
66
|
rescue StandardError
|
|
63
67
|
nil
|
|
64
68
|
end
|
|
65
69
|
|
|
66
70
|
def spawn_ractor(args:, name:, port:, envelope:, body:, emit_non_standard_exception_summaries:)
|
|
67
|
-
# :
|
|
71
|
+
# simplecov:disable
|
|
68
72
|
::Ractor.new(port, envelope, body, emit_non_standard_exception_summaries, *args, name: name) do
|
|
69
73
|
|bridge_port, captured_envelope, callable, emit_non_standard_summaries, *call_args|
|
|
70
|
-
|
|
74
|
+
Core::RuntimeLocator.current = RemoteRuntime.new(
|
|
71
75
|
port: bridge_port, emit_non_standard_exception_summaries: emit_non_standard_summaries
|
|
72
76
|
)
|
|
73
|
-
|
|
77
|
+
Core::Propagation.restore(captured_envelope, owned: true) do
|
|
74
78
|
callable.call(*call_args)
|
|
75
79
|
end
|
|
76
80
|
ensure
|
|
@@ -81,12 +85,10 @@ module Julewire
|
|
|
81
85
|
nil
|
|
82
86
|
end
|
|
83
87
|
end
|
|
84
|
-
# :
|
|
88
|
+
# simplecov:enable
|
|
85
89
|
end
|
|
86
90
|
|
|
87
91
|
def handle_message(runtime, message)
|
|
88
|
-
return unless message.is_a?(Hash)
|
|
89
|
-
|
|
90
92
|
response = dispatch(runtime, message)
|
|
91
93
|
reply_to(message, response)
|
|
92
94
|
rescue StandardError => e
|
|
@@ -95,7 +97,9 @@ module Julewire
|
|
|
95
97
|
end
|
|
96
98
|
|
|
97
99
|
def dispatch(runtime, message)
|
|
98
|
-
|
|
100
|
+
validate_message!(message)
|
|
101
|
+
command = message.fetch(:command)
|
|
102
|
+
case command
|
|
99
103
|
when :emit
|
|
100
104
|
dispatch_emit(runtime, message, enforce_level: true)
|
|
101
105
|
when :emit_without_level
|
|
@@ -105,7 +109,9 @@ module Julewire
|
|
|
105
109
|
RemoteSummaryRecord.new(RemotePayload.hash_value(message, :payload))
|
|
106
110
|
)
|
|
107
111
|
when :flush
|
|
108
|
-
runtime.flush(timeout: message.
|
|
112
|
+
runtime.flush(timeout: message.fetch(:payload).fetch(:timeout))
|
|
113
|
+
else
|
|
114
|
+
raise ArgumentError, "unknown ractor bridge command: #{command.inspect}"
|
|
109
115
|
end
|
|
110
116
|
end
|
|
111
117
|
|
|
@@ -113,12 +119,18 @@ module Julewire
|
|
|
113
119
|
payload = RemotePayload.hash_value(message, :payload)
|
|
114
120
|
arguments = RemotePayload.extract(payload)
|
|
115
121
|
arguments[:enforce_level] = false unless enforce_level
|
|
116
|
-
runtime.emit_envelope(**arguments)
|
|
122
|
+
runtime.emit_envelope(**arguments, owned: true)
|
|
117
123
|
end
|
|
118
124
|
|
|
119
125
|
def reply_to(message, response)
|
|
126
|
+
return unless message.is_a?(Hash)
|
|
127
|
+
|
|
120
128
|
reply = message[:reply]
|
|
121
|
-
|
|
129
|
+
return unless reply
|
|
130
|
+
|
|
131
|
+
raise TypeError, "ractor bridge reply must be a Ractor::Port" unless reply_port?(reply)
|
|
132
|
+
|
|
133
|
+
reply.send(response)
|
|
122
134
|
rescue StandardError => e
|
|
123
135
|
Stats.message_failed(e)
|
|
124
136
|
nil
|
|
@@ -127,6 +139,10 @@ module Julewire
|
|
|
127
139
|
def reply_port?(reply)
|
|
128
140
|
reply.is_a?(::Ractor::Port)
|
|
129
141
|
end
|
|
142
|
+
|
|
143
|
+
def validate_message!(message)
|
|
144
|
+
Core::Integration::Protocol.validate_symbol_hash(message)
|
|
145
|
+
end
|
|
130
146
|
end
|
|
131
147
|
end
|
|
132
148
|
end
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "concurrent/atomic/atomic_fixnum"
|
|
4
|
+
require "concurrent/atomic/atomic_reference"
|
|
5
|
+
|
|
3
6
|
module Julewire
|
|
4
7
|
module Ractor
|
|
5
8
|
class ChildStats
|
|
@@ -13,8 +16,8 @@ module Julewire
|
|
|
13
16
|
private_constant :COUNTER_KEYS
|
|
14
17
|
|
|
15
18
|
def initialize
|
|
16
|
-
@
|
|
17
|
-
@
|
|
19
|
+
@counters = COUNTER_KEYS.to_h { [it, Concurrent::AtomicFixnum.new] }
|
|
20
|
+
@last_error_class = Concurrent::AtomicReference.new
|
|
18
21
|
end
|
|
19
22
|
|
|
20
23
|
def message_sent = increment(:messages_sent)
|
|
@@ -32,34 +35,27 @@ module Julewire
|
|
|
32
35
|
def request_timed_out = increment(:requests_timed_out)
|
|
33
36
|
|
|
34
37
|
def reset!
|
|
35
|
-
@
|
|
36
|
-
|
|
37
|
-
@last_error_class = nil
|
|
38
|
-
end
|
|
39
|
-
nil
|
|
38
|
+
@counters.each_value { it.value = 0 }
|
|
39
|
+
@last_error_class.set(nil)
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
def to_h
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}.compact.freeze
|
|
48
|
-
end
|
|
43
|
+
{
|
|
44
|
+
counts: @counters.transform_values(&:value).freeze,
|
|
45
|
+
last_error_class: @last_error_class.get
|
|
46
|
+
}.compact.freeze
|
|
49
47
|
end
|
|
50
48
|
|
|
51
49
|
private
|
|
52
50
|
|
|
53
51
|
def increment(key)
|
|
54
|
-
@
|
|
52
|
+
@counters.fetch(key).increment
|
|
55
53
|
nil
|
|
56
54
|
end
|
|
57
55
|
|
|
58
56
|
def record_error(key, error)
|
|
59
|
-
@
|
|
60
|
-
|
|
61
|
-
@last_error_class = error.class.name
|
|
62
|
-
end
|
|
57
|
+
@counters.fetch(key).increment
|
|
58
|
+
@last_error_class.set(error.class.name)
|
|
63
59
|
nil
|
|
64
60
|
end
|
|
65
61
|
end
|