julewire-ractor 1.0.1 → 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 +12 -0
- data/docs/bridge.md +23 -4
- 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 +28 -12
- data/lib/julewire/ractor/child_stats.rb +14 -18
- data/lib/julewire/ractor/destination.rb +141 -102
- 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 -14
- 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,17 @@
|
|
|
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
|
+
|
|
3
15
|
## 1.0.1 - 2026-06-25
|
|
4
16
|
|
|
5
17
|
- Reserve ractor destination queue slots before send and roll them back on
|
data/docs/bridge.md
CHANGED
|
@@ -29,6 +29,12 @@ 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.
|
|
@@ -106,15 +112,28 @@ synchronously. The destination then sends each immutable record to a worker
|
|
|
106
112
|
ractor. The worker owns formatter, encoder, byte-limit checks, and output
|
|
107
113
|
writes.
|
|
108
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
|
+
|
|
109
123
|
The destination has a bounded parent-side in-flight queue. When `max_queue` is
|
|
110
124
|
full, new records are dropped and counted in destination health. `flush` sends a
|
|
111
125
|
request to the worker and waits for all earlier records to finish. `close`
|
|
112
126
|
flushes or closes the worker-owned output and stops the worker.
|
|
113
127
|
|
|
114
|
-
Unlike direct core destinations, ractor-backed destinations
|
|
115
|
-
`flush(timeout: nil)` and `close(timeout: nil)`
|
|
116
|
-
instead of an unbounded wait. Worker ractors
|
|
117
|
-
|
|
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.
|
|
118
137
|
|
|
119
138
|
Formatter, encoder, and output must be ractor-copyable or shareable. Avoid
|
|
120
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
|
|
|
@@ -117,8 +123,14 @@ module Julewire
|
|
|
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
|