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
|
@@ -6,19 +6,51 @@ require "concurrent/atomic/atomic_reference"
|
|
|
6
6
|
module Julewire
|
|
7
7
|
module Ractor
|
|
8
8
|
class Destination # rubocop:disable Metrics/ClassLength -- Owns parent queue, worker lifecycle, and health.
|
|
9
|
+
class QueueSlots
|
|
10
|
+
def initialize(max_queue:)
|
|
11
|
+
@max_queue = max_queue
|
|
12
|
+
@in_flight = Concurrent::AtomicFixnum.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def value = @in_flight.value
|
|
16
|
+
|
|
17
|
+
def reserve
|
|
18
|
+
return true unless @max_queue.positive?
|
|
19
|
+
|
|
20
|
+
result = {}
|
|
21
|
+
@in_flight.update do |current|
|
|
22
|
+
result[:reserved] = current < @max_queue
|
|
23
|
+
result.fetch(:reserved) ? current + 1 : current
|
|
24
|
+
end
|
|
25
|
+
result.fetch(:reserved)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def release
|
|
29
|
+
return false unless @max_queue.positive?
|
|
30
|
+
|
|
31
|
+
result = {}
|
|
32
|
+
@in_flight.update do |current|
|
|
33
|
+
result[:underflow] = current.zero?
|
|
34
|
+
current.positive? ? current - 1 : current
|
|
35
|
+
end
|
|
36
|
+
result.fetch(:underflow)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
9
40
|
COUNTER_KEYS = %i[
|
|
10
41
|
closed_dropped
|
|
11
42
|
queue_full_dropped
|
|
12
43
|
queued
|
|
13
44
|
received
|
|
14
45
|
send_error
|
|
46
|
+
slot_underflow_ignored
|
|
15
47
|
worker_accepted
|
|
16
48
|
worker_dropped
|
|
17
49
|
].freeze
|
|
18
50
|
DEFAULT_MAX_QUEUE = 1024
|
|
19
51
|
DEFAULT_REQUEST_TIMEOUT = 1
|
|
20
|
-
|
|
21
|
-
private_constant :COUNTER_KEYS
|
|
52
|
+
WORKER_STOP_MESSAGE = { command: :close_worker }.freeze
|
|
53
|
+
private_constant :COUNTER_KEYS, :QueueSlots, :WORKER_STOP_MESSAGE
|
|
22
54
|
|
|
23
55
|
attr_reader :name
|
|
24
56
|
|
|
@@ -41,6 +73,8 @@ module Julewire
|
|
|
41
73
|
Core::Validation.validate_byte_limit!(max_record_bytes, name: :max_record_bytes)
|
|
42
74
|
Core::Validation.validate_non_negative_integer!(max_queue, name: :max_queue)
|
|
43
75
|
Core::Validation.validate_timeout!(request_timeout, name: :request_timeout)
|
|
76
|
+
raise ArgumentError, "request_timeout must be a non-negative finite Numeric" if request_timeout.nil?
|
|
77
|
+
|
|
44
78
|
Core::Validation.validate_callable!(on_drop, name: :on_drop, allow_nil: true)
|
|
45
79
|
Core::Validation.validate_callable!(on_failure, name: :on_failure, allow_nil: true)
|
|
46
80
|
@output = output
|
|
@@ -50,7 +84,6 @@ module Julewire
|
|
|
50
84
|
@request_timeout = request_timeout
|
|
51
85
|
@on_drop = on_drop
|
|
52
86
|
@on_failure = on_failure
|
|
53
|
-
@scheduler = Core::Scheduling::DeadlineScheduler.new(thread_name: TIMEOUT_THREAD_NAME, idle: :exit)
|
|
54
87
|
initialize_tracking
|
|
55
88
|
start_worker
|
|
56
89
|
end
|
|
@@ -58,33 +91,37 @@ module Julewire
|
|
|
58
91
|
def emit(record)
|
|
59
92
|
increment(:received)
|
|
60
93
|
return drop(:closed_dropped, record) if closed?
|
|
61
|
-
return drop(:queue_full_dropped, record)
|
|
94
|
+
return drop(:queue_full_dropped, record) unless @queue_slots.reserve
|
|
62
95
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
96
|
+
begin
|
|
97
|
+
degradation_marker = @health.degradation_marker
|
|
98
|
+
@port.send({ command: :emit, degradation_marker: degradation_marker, record: record })
|
|
99
|
+
increment(:queued)
|
|
100
|
+
rescue StandardError => e
|
|
101
|
+
release_slot
|
|
102
|
+
record_failure(e, phase: :ractor_send)
|
|
103
|
+
drop(:send_error, record)
|
|
104
|
+
end
|
|
66
105
|
nil
|
|
67
|
-
rescue StandardError => e
|
|
68
|
-
record_failure(e, phase: :ractor_send)
|
|
69
|
-
drop(:send_error, record)
|
|
70
106
|
end
|
|
71
107
|
|
|
72
108
|
def flush(timeout: nil)
|
|
73
|
-
|
|
74
|
-
request(:flush, timeout: timeout)
|
|
109
|
+
@health.recover_if_successful { request(:flush, timeout: lifecycle_timeout(timeout)) }
|
|
75
110
|
end
|
|
76
111
|
|
|
77
112
|
def close(timeout: nil)
|
|
78
|
-
timeout =
|
|
113
|
+
timeout = lifecycle_timeout(timeout)
|
|
79
114
|
@closed.set(true)
|
|
80
|
-
result = request(:close, timeout: timeout)
|
|
81
|
-
close_ports
|
|
115
|
+
result = request(:close, timeout: timeout, allow_closed: true)
|
|
116
|
+
close_ports(timeout: timeout)
|
|
82
117
|
result
|
|
83
118
|
end
|
|
84
119
|
|
|
85
120
|
def after_fork!
|
|
86
|
-
close_ports
|
|
87
|
-
|
|
121
|
+
if (@process_id - Process.pid).zero? && !close_ports(timeout: @request_timeout)
|
|
122
|
+
raise Core::Error, "ractor destination worker did not stop within #{@request_timeout} seconds"
|
|
123
|
+
end
|
|
124
|
+
|
|
88
125
|
initialize_tracking
|
|
89
126
|
start_worker
|
|
90
127
|
self
|
|
@@ -97,11 +134,17 @@ module Julewire
|
|
|
97
134
|
|
|
98
135
|
def health
|
|
99
136
|
worker = request(:health, timeout: @request_timeout)
|
|
100
|
-
|
|
101
|
-
|
|
137
|
+
if worker.equal?(false)
|
|
138
|
+
worker = @worker_health.get
|
|
139
|
+
else
|
|
140
|
+
raise TypeError, "ractor destination health must be a Hash" unless worker.instance_of?(Hash)
|
|
141
|
+
|
|
142
|
+
Core::Integration::Protocol.validate_symbol_keys(worker)
|
|
143
|
+
@worker_health.set(worker)
|
|
144
|
+
end
|
|
102
145
|
|
|
103
146
|
@health.snapshot(
|
|
104
|
-
in_flight: @
|
|
147
|
+
in_flight: @queue_slots.value,
|
|
105
148
|
max_queue: @max_queue,
|
|
106
149
|
status: status_for(worker),
|
|
107
150
|
worker: worker
|
|
@@ -116,22 +159,28 @@ module Julewire
|
|
|
116
159
|
end
|
|
117
160
|
|
|
118
161
|
def initialize_tracking
|
|
119
|
-
@
|
|
162
|
+
@process_id = Process.pid
|
|
163
|
+
@scheduler = ReplyTimeoutScheduler.new(timeout_value: false)
|
|
164
|
+
@closed = Concurrent::AtomicReference.new
|
|
120
165
|
@health = Core::Integration::DestinationHealth.new(counter_keys: COUNTER_KEYS, failure_counter: nil)
|
|
121
|
-
@
|
|
166
|
+
@queue_slots = QueueSlots.new(max_queue: @max_queue)
|
|
122
167
|
@worker_health = Concurrent::AtomicReference.new
|
|
123
168
|
end
|
|
124
169
|
|
|
125
170
|
def start_worker
|
|
126
171
|
@ack_port = ::Ractor::Port.new
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
172
|
+
PortLifecycle.with_port do |setup_port|
|
|
173
|
+
@worker = spawn_worker(setup_port)
|
|
174
|
+
@port = WorkerHandshake.receive(
|
|
175
|
+
setup_port: setup_port,
|
|
176
|
+
worker: @worker,
|
|
177
|
+
scheduler: @scheduler
|
|
178
|
+
)
|
|
179
|
+
end
|
|
130
180
|
@ack_thread = start_ack_thread
|
|
131
|
-
rescue
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
PortLifecycle.close(setup_port) if defined?(setup_port) && setup_port
|
|
181
|
+
rescue TypeError, ::Ractor::Error => e
|
|
182
|
+
@port = @worker = @ack_port = @ack_thread = nil
|
|
183
|
+
raise ArgumentError, "ractor destination collaborators must be ractor-copyable or shareable: #{e}"
|
|
135
184
|
end
|
|
136
185
|
|
|
137
186
|
def spawn_worker(setup_port)
|
|
@@ -142,7 +191,7 @@ module Julewire
|
|
|
142
191
|
max_record_bytes = @max_record_bytes
|
|
143
192
|
close_output = @close_output
|
|
144
193
|
|
|
145
|
-
# :
|
|
194
|
+
# simplecov:disable
|
|
146
195
|
::Ractor.new(setup_port, ack_port, formatter, encoder, output, max_record_bytes, close_output,
|
|
147
196
|
name: "julewire-ractor-destination") do |worker_port, worker_ack_port, worker_formatter,
|
|
148
197
|
worker_encoder, worker_output, worker_max_record_bytes,
|
|
@@ -159,21 +208,19 @@ module Julewire
|
|
|
159
208
|
close_output: worker_close_output
|
|
160
209
|
)
|
|
161
210
|
end
|
|
162
|
-
# :
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
def receive_worker_port(setup_port)
|
|
166
|
-
selected, value = ::Ractor.select(setup_port, @worker)
|
|
167
|
-
return value if selected.equal?(setup_port) && value.is_a?(::Ractor::Port)
|
|
168
|
-
|
|
169
|
-
raise ArgumentError, "ractor destination worker did not start"
|
|
211
|
+
# simplecov:enable
|
|
170
212
|
end
|
|
171
213
|
|
|
172
214
|
def start_ack_thread
|
|
215
|
+
ack_port = @ack_port
|
|
173
216
|
thread = Thread.new do
|
|
174
217
|
loop do
|
|
175
|
-
message =
|
|
176
|
-
|
|
218
|
+
message = ack_port.receive
|
|
219
|
+
Core::Integration::Protocol.validate_symbol_hash(message)
|
|
220
|
+
event = message.fetch(:event)
|
|
221
|
+
unless event == :ack
|
|
222
|
+
raise ArgumentError, "unknown ractor destination acknowledgement event: #{event.inspect}"
|
|
223
|
+
end
|
|
177
224
|
|
|
178
225
|
handle_ack(message)
|
|
179
226
|
end
|
|
@@ -181,65 +228,86 @@ module Julewire
|
|
|
181
228
|
record_failure(e, phase: :ack)
|
|
182
229
|
end
|
|
183
230
|
thread.name = "julewire-ractor-destination-ack"
|
|
184
|
-
thread.report_on_exception = true
|
|
185
231
|
thread
|
|
186
232
|
end
|
|
187
233
|
|
|
188
234
|
def handle_ack(message)
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
decrement_in_flight
|
|
192
|
-
case message[:status]
|
|
235
|
+
release_slot
|
|
236
|
+
case message.fetch(:status)
|
|
193
237
|
when :accepted
|
|
194
238
|
increment(:worker_accepted)
|
|
239
|
+
@health.clear_degradation_if_unchanged(message.fetch(:degradation_marker))
|
|
195
240
|
when :dropped
|
|
196
241
|
increment(:worker_dropped)
|
|
242
|
+
else
|
|
243
|
+
raise ArgumentError, "unknown ractor destination acknowledgement status: #{message.fetch(:status).inspect}"
|
|
197
244
|
end
|
|
198
245
|
end
|
|
199
246
|
|
|
200
|
-
def request(command, timeout:)
|
|
201
|
-
return false if closed? &&
|
|
247
|
+
def request(command, timeout:, allow_closed: false)
|
|
248
|
+
return false if closed? && !allow_closed
|
|
202
249
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
250
|
+
PortLifecycle.with_port do |reply|
|
|
251
|
+
@port.send({ command: command, reply: reply })
|
|
252
|
+
wait_for_reply(reply, timeout, command)
|
|
253
|
+
end
|
|
206
254
|
rescue StandardError => e
|
|
207
255
|
record_failure(e, phase: :request, command: command)
|
|
208
256
|
false
|
|
209
|
-
ensure
|
|
210
|
-
PortLifecycle.close(reply) if reply
|
|
211
257
|
end
|
|
212
258
|
|
|
213
|
-
def wait_for_reply(reply, timeout)
|
|
214
|
-
|
|
259
|
+
def wait_for_reply(reply, timeout, command)
|
|
260
|
+
@scheduler.with_timeout(reply, timeout: timeout) do
|
|
261
|
+
selected, value = ::Ractor.select(reply, @worker)
|
|
262
|
+
return value if selected.equal?(reply)
|
|
215
263
|
|
|
216
|
-
|
|
217
|
-
token = @scheduler.schedule(timeout) do
|
|
218
|
-
timeout_port.send(:timeout)
|
|
219
|
-
rescue StandardError
|
|
220
|
-
nil
|
|
264
|
+
raise Core::Error, "ractor destination worker stopped before #{command} replied"
|
|
221
265
|
end
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
selected.equal?(timeout_port) ? false : response
|
|
225
|
-
ensure
|
|
226
|
-
@scheduler.cancel(token) if defined?(token)
|
|
227
|
-
PortLifecycle.close(timeout_port) if defined?(timeout_port) && timeout_port
|
|
266
|
+
rescue ::Ractor::RemoteError
|
|
267
|
+
raise Core::Error, "ractor destination worker stopped before #{command} replied"
|
|
228
268
|
end
|
|
229
269
|
|
|
230
|
-
def
|
|
231
|
-
|
|
270
|
+
def closed? = @closed.get
|
|
271
|
+
|
|
272
|
+
def close_ports(timeout:)
|
|
273
|
+
worker_stopped = true
|
|
274
|
+
begin
|
|
275
|
+
if @worker
|
|
276
|
+
begin
|
|
277
|
+
@port.send(WORKER_STOP_MESSAGE)
|
|
278
|
+
rescue ::Ractor::ClosedError
|
|
279
|
+
# A stopped worker no longer accepts commands. Still collect it
|
|
280
|
+
# below so an abnormal exit remains observable.
|
|
281
|
+
end
|
|
282
|
+
worker_stopped = wait_for_worker(timeout)
|
|
283
|
+
end
|
|
284
|
+
rescue ::Ractor::RemoteError => e
|
|
285
|
+
record_failure(e, phase: :worker_stop)
|
|
286
|
+
worker_stopped = true
|
|
287
|
+
ensure
|
|
288
|
+
@ack_thread&.kill
|
|
289
|
+
@ack_thread&.join
|
|
290
|
+
PortLifecycle.close(@ack_port)
|
|
291
|
+
@port = @worker = nil if worker_stopped
|
|
292
|
+
@ack_port = nil
|
|
293
|
+
@ack_thread = nil
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
worker_stopped
|
|
232
297
|
end
|
|
233
298
|
|
|
234
|
-
def
|
|
299
|
+
def wait_for_worker(timeout)
|
|
300
|
+
PortLifecycle.with_port do |timeout_port|
|
|
301
|
+
@scheduler.with_timeout(timeout_port, timeout: timeout) do
|
|
302
|
+
selected, = ::Ractor.select(@worker, timeout_port)
|
|
303
|
+
selected.equal?(@worker)
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
end
|
|
235
307
|
|
|
236
|
-
def
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
nil
|
|
240
|
-
ensure
|
|
241
|
-
PortLifecycle.close(@port) if @port
|
|
242
|
-
PortLifecycle.close(@ack_port) if @ack_port
|
|
308
|
+
def lifecycle_timeout(timeout)
|
|
309
|
+
Core::Validation.validate_timeout!(timeout, name: :timeout)
|
|
310
|
+
timeout || @request_timeout
|
|
243
311
|
end
|
|
244
312
|
|
|
245
313
|
def drop(reason, record)
|
|
@@ -253,24 +321,23 @@ module Julewire
|
|
|
253
321
|
end
|
|
254
322
|
|
|
255
323
|
def record_loss(reason, record)
|
|
256
|
-
metadata = Core::Records::Metadata.call(record)
|
|
257
324
|
@health.record_loss(
|
|
258
325
|
reason: reason,
|
|
259
|
-
event:
|
|
260
|
-
severity:
|
|
261
|
-
source:
|
|
326
|
+
event: record.event,
|
|
327
|
+
severity: record.severity,
|
|
328
|
+
source: record.source
|
|
262
329
|
)
|
|
263
330
|
end
|
|
264
331
|
|
|
265
332
|
def record_failure(error, **metadata)
|
|
266
|
-
@health.record_failure(error,
|
|
333
|
+
@health.record_failure(error, **metadata)
|
|
267
334
|
Core::Diagnostics::CallbackNotifier.call(@on_failure, error, { destination: name }.merge(metadata))
|
|
268
|
-
rescue StandardError
|
|
269
|
-
nil
|
|
270
335
|
end
|
|
271
336
|
|
|
272
|
-
def
|
|
273
|
-
|
|
337
|
+
def release_slot
|
|
338
|
+
# Late or duplicate ACKs can arrive after teardown/reset; keep them visible
|
|
339
|
+
# without treating the ignored underflow as an operator-facing defect.
|
|
340
|
+
increment(:slot_underflow_ignored) if @queue_slots.release
|
|
274
341
|
end
|
|
275
342
|
|
|
276
343
|
def increment(key)
|
|
@@ -279,10 +346,8 @@ module Julewire
|
|
|
279
346
|
|
|
280
347
|
def status_for(worker)
|
|
281
348
|
return :closed if closed?
|
|
282
|
-
return :degraded if @health.degraded?
|
|
283
|
-
return :degraded if worker.is_a?(Hash) && worker[:status] == :degraded
|
|
284
349
|
|
|
285
|
-
:
|
|
350
|
+
:degraded if worker&.fetch(:status) == :degraded
|
|
286
351
|
end
|
|
287
352
|
end
|
|
288
353
|
end
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# :nocov:
|
|
4
3
|
module Julewire
|
|
5
4
|
module Ractor
|
|
6
5
|
class DestinationWorker
|
|
@@ -47,27 +46,24 @@ module Julewire
|
|
|
47
46
|
@ack_port = ack_port
|
|
48
47
|
loop do
|
|
49
48
|
message = command_port.receive
|
|
50
|
-
|
|
49
|
+
current_command = command(message)
|
|
50
|
+
break if current_command == :close_worker
|
|
51
51
|
|
|
52
|
-
break if dispatch(message) == :close
|
|
52
|
+
break if dispatch(message, current_command) == :close
|
|
53
53
|
end
|
|
54
54
|
ensure
|
|
55
55
|
close_output
|
|
56
|
-
ack(:closed)
|
|
57
56
|
end
|
|
58
57
|
|
|
59
58
|
private
|
|
60
59
|
|
|
61
|
-
def
|
|
62
|
-
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def dispatch(message)
|
|
66
|
-
return unless message.is_a?(Hash)
|
|
67
|
-
|
|
68
|
-
case message[:command]
|
|
60
|
+
def dispatch(message, current_command)
|
|
61
|
+
case current_command
|
|
69
62
|
when :emit
|
|
70
|
-
emit(
|
|
63
|
+
emit(
|
|
64
|
+
message.fetch(:record),
|
|
65
|
+
parent_degradation_marker: message.fetch(:degradation_marker)
|
|
66
|
+
)
|
|
71
67
|
when :flush
|
|
72
68
|
reply_to(message, call_output_lifecycle(:flush))
|
|
73
69
|
when :close
|
|
@@ -75,51 +71,47 @@ module Julewire
|
|
|
75
71
|
:close
|
|
76
72
|
when :health
|
|
77
73
|
reply_to(message, health)
|
|
74
|
+
else
|
|
75
|
+
raise ArgumentError, "unknown ractor destination command: #{current_command.inspect}"
|
|
78
76
|
end
|
|
79
|
-
rescue StandardError => e
|
|
80
|
-
record_failure(e, phase: :dispatch)
|
|
81
|
-
reply_to(message, false)
|
|
82
77
|
end
|
|
83
78
|
|
|
84
|
-
def
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
79
|
+
def command(message)
|
|
80
|
+
Core::Integration::Protocol.validate_symbol_hash(message)
|
|
81
|
+
message.fetch(:command)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def emit(record, parent_degradation_marker:)
|
|
85
|
+
accepted = @health.recover_if_successful { @write_step.call(record) }
|
|
86
|
+
ack(accepted ? :accepted : :dropped, degradation_marker: parent_degradation_marker)
|
|
89
87
|
end
|
|
90
88
|
|
|
91
89
|
def call_output_lifecycle(method_name)
|
|
92
|
-
return
|
|
93
|
-
return true unless @output.respond_to?(method_name)
|
|
90
|
+
return close_lifecycle_succeeds? if method_name == :close
|
|
94
91
|
|
|
95
|
-
@
|
|
92
|
+
@health.recover_if_successful do
|
|
93
|
+
!@output.respond_to?(method_name) || @output.public_send(method_name) != false
|
|
94
|
+
end
|
|
96
95
|
rescue StandardError => e
|
|
97
96
|
record_failure(e, phase: :output_lifecycle, action: method_name)
|
|
98
97
|
false
|
|
99
98
|
end
|
|
100
99
|
|
|
101
|
-
def
|
|
102
|
-
return true if
|
|
100
|
+
def close_lifecycle_succeeds?
|
|
101
|
+
return true if @output.respond_to?(:closed?) && @output.closed?
|
|
103
102
|
return @output.close != false if @close_output && @output.respond_to?(:close)
|
|
104
103
|
return @output.flush != false if @output.respond_to?(:flush)
|
|
105
104
|
|
|
106
105
|
true
|
|
107
|
-
rescue StandardError => e
|
|
108
|
-
record_failure(e, phase: :output_lifecycle, action: :close)
|
|
109
|
-
false
|
|
110
106
|
end
|
|
111
107
|
|
|
112
108
|
def close_output
|
|
113
|
-
return if
|
|
109
|
+
return if @output.respond_to?(:closed?) && @output.closed?
|
|
114
110
|
return unless @close_output && @output.respond_to?(:close)
|
|
115
111
|
|
|
116
112
|
@output.close
|
|
117
|
-
rescue StandardError
|
|
118
|
-
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
def output_closed?
|
|
122
|
-
@output.respond_to?(:closed?) ? @output.closed? : false
|
|
113
|
+
rescue StandardError
|
|
114
|
+
nil
|
|
123
115
|
end
|
|
124
116
|
|
|
125
117
|
def health
|
|
@@ -131,7 +123,7 @@ module Julewire
|
|
|
131
123
|
end
|
|
132
124
|
|
|
133
125
|
def record_failure(error, **metadata)
|
|
134
|
-
@health.record_failure(error,
|
|
126
|
+
@health.record_failure(error, **metadata)
|
|
135
127
|
end
|
|
136
128
|
|
|
137
129
|
def record_loss(reason, **metadata)
|
|
@@ -139,7 +131,7 @@ module Julewire
|
|
|
139
131
|
end
|
|
140
132
|
|
|
141
133
|
def record_write_step_failure(error, metadata)
|
|
142
|
-
record_failure(error, **
|
|
134
|
+
record_failure(error, **metadata)
|
|
143
135
|
end
|
|
144
136
|
|
|
145
137
|
def record_write_step_loss(reason, metadata)
|
|
@@ -156,15 +148,19 @@ module Julewire
|
|
|
156
148
|
metadata.except(:record)
|
|
157
149
|
end
|
|
158
150
|
|
|
159
|
-
def ack(status)
|
|
160
|
-
@ack_port.send({ event: :ack, status: status })
|
|
161
|
-
rescue StandardError
|
|
162
|
-
nil
|
|
151
|
+
def ack(status, degradation_marker:)
|
|
152
|
+
@ack_port.send({ degradation_marker: degradation_marker, event: :ack, status: status })
|
|
163
153
|
end
|
|
164
154
|
|
|
165
155
|
def reply_to(message, response)
|
|
166
|
-
reply = message
|
|
167
|
-
reply
|
|
156
|
+
reply = message.fetch(:reply)
|
|
157
|
+
raise TypeError, "ractor destination reply must be a Ractor::Port" unless reply.is_a?(::Ractor::Port)
|
|
158
|
+
|
|
159
|
+
send_reply(reply, response)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def send_reply(reply, response)
|
|
163
|
+
reply.send(response)
|
|
168
164
|
rescue StandardError => e
|
|
169
165
|
record_failure(e, phase: :reply)
|
|
170
166
|
end
|
|
@@ -173,4 +169,3 @@ module Julewire
|
|
|
173
169
|
private_constant :DestinationWorker
|
|
174
170
|
end
|
|
175
171
|
end
|
|
176
|
-
# :nocov:
|
|
@@ -7,7 +7,9 @@ module Julewire
|
|
|
7
7
|
|
|
8
8
|
def initialize(destinations:, name: :ractor_fanout, on_failure: nil)
|
|
9
9
|
@name = Core::Destinations.normalize_name(name)
|
|
10
|
-
|
|
10
|
+
raise ArgumentError, "destinations must be an Array" unless destinations.instance_of?(Array)
|
|
11
|
+
|
|
12
|
+
@destinations = destinations.map { normalize_destination(it) }
|
|
11
13
|
raise ArgumentError, "destinations must not be empty" if @destinations.empty?
|
|
12
14
|
|
|
13
15
|
Core::Validation.validate_callable!(on_failure, name: :on_failure, allow_nil: true)
|
|
@@ -16,7 +18,9 @@ module Julewire
|
|
|
16
18
|
end
|
|
17
19
|
|
|
18
20
|
def emit(record)
|
|
19
|
-
@
|
|
21
|
+
@health.recover_if_successful do
|
|
22
|
+
@destinations.each { emit_to_destination(it, record) }
|
|
23
|
+
end
|
|
20
24
|
nil
|
|
21
25
|
end
|
|
22
26
|
|
|
@@ -29,10 +33,12 @@ module Julewire
|
|
|
29
33
|
end
|
|
30
34
|
|
|
31
35
|
def after_fork!
|
|
32
|
-
@
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
@health.recover_if_successful do
|
|
37
|
+
@destinations.each do |destination|
|
|
38
|
+
destination.after_fork! if destination.respond_to?(:after_fork!)
|
|
39
|
+
rescue StandardError => e
|
|
40
|
+
record_failure(e, action: :after_fork, destination: destination.name)
|
|
41
|
+
end
|
|
36
42
|
end
|
|
37
43
|
self
|
|
38
44
|
end
|
|
@@ -61,14 +67,16 @@ module Julewire
|
|
|
61
67
|
end
|
|
62
68
|
|
|
63
69
|
def call_lifecycle(method_name, timeout:)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
+
@health.recover_if_successful do
|
|
71
|
+
ok = true
|
|
72
|
+
@destinations.each do |destination|
|
|
73
|
+
ok = false if destination.public_send(method_name, timeout: timeout) == false
|
|
74
|
+
rescue StandardError => e
|
|
75
|
+
record_failure(e, action: method_name, destination: destination.name)
|
|
76
|
+
ok = false
|
|
77
|
+
end
|
|
78
|
+
ok
|
|
70
79
|
end
|
|
71
|
-
ok
|
|
72
80
|
end
|
|
73
81
|
|
|
74
82
|
def destination_health(destination)
|
|
@@ -78,17 +86,16 @@ module Julewire
|
|
|
78
86
|
end
|
|
79
87
|
|
|
80
88
|
def health_status(destinations)
|
|
81
|
-
|
|
82
|
-
return :degraded if destinations.any? { |_name, health| health[:status] == :degraded || health[:phase] }
|
|
83
|
-
|
|
84
|
-
:ok
|
|
89
|
+
:degraded if destinations.any? { |_name, health| health[:status] == :degraded || health[:phase] }
|
|
85
90
|
end
|
|
86
91
|
|
|
87
92
|
def record_failure(error, **metadata)
|
|
88
|
-
@health.record_failure(error,
|
|
89
|
-
@on_failure
|
|
90
|
-
|
|
91
|
-
|
|
93
|
+
@health.record_failure(error, phase: :ractor_fanout, **metadata)
|
|
94
|
+
callback_result = Core::Diagnostics::CallbackNotifier.call(@on_failure, error,
|
|
95
|
+
metadata.merge(phase: :ractor_fanout))
|
|
96
|
+
return unless Core::Diagnostics::CallbackNotifier.failure?(callback_result)
|
|
97
|
+
|
|
98
|
+
@health.record_callback_failure(callback_result)
|
|
92
99
|
end
|
|
93
100
|
end
|
|
94
101
|
end
|
|
@@ -4,11 +4,17 @@ module Julewire
|
|
|
4
4
|
module Ractor
|
|
5
5
|
module PortLifecycle
|
|
6
6
|
class << self
|
|
7
|
+
def with_port(port = ::Ractor::Port.new)
|
|
8
|
+
yield port
|
|
9
|
+
ensure
|
|
10
|
+
close(port)
|
|
11
|
+
end
|
|
12
|
+
|
|
7
13
|
def close(port)
|
|
8
14
|
return unless port.respond_to?(:close)
|
|
9
|
-
return if port.respond_to?(:closed?) && port.closed?
|
|
10
15
|
|
|
11
16
|
port.close
|
|
17
|
+
nil
|
|
12
18
|
rescue StandardError
|
|
13
19
|
nil
|
|
14
20
|
end
|