julewire-ractor 1.0.1 → 1.1.1
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 +16 -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
|
@@ -6,6 +6,37 @@ 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
|
|
@@ -18,8 +49,8 @@ module Julewire
|
|
|
18
49
|
].freeze
|
|
19
50
|
DEFAULT_MAX_QUEUE = 1024
|
|
20
51
|
DEFAULT_REQUEST_TIMEOUT = 1
|
|
21
|
-
|
|
22
|
-
private_constant :COUNTER_KEYS
|
|
52
|
+
WORKER_STOP_MESSAGE = { command: :close_worker }.freeze
|
|
53
|
+
private_constant :COUNTER_KEYS, :QueueSlots, :WORKER_STOP_MESSAGE
|
|
23
54
|
|
|
24
55
|
attr_reader :name
|
|
25
56
|
|
|
@@ -42,6 +73,8 @@ module Julewire
|
|
|
42
73
|
Core::Validation.validate_byte_limit!(max_record_bytes, name: :max_record_bytes)
|
|
43
74
|
Core::Validation.validate_non_negative_integer!(max_queue, name: :max_queue)
|
|
44
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
|
+
|
|
45
78
|
Core::Validation.validate_callable!(on_drop, name: :on_drop, allow_nil: true)
|
|
46
79
|
Core::Validation.validate_callable!(on_failure, name: :on_failure, allow_nil: true)
|
|
47
80
|
@output = output
|
|
@@ -51,7 +84,6 @@ module Julewire
|
|
|
51
84
|
@request_timeout = request_timeout
|
|
52
85
|
@on_drop = on_drop
|
|
53
86
|
@on_failure = on_failure
|
|
54
|
-
@scheduler = Core::Scheduling::DeadlineScheduler.new(thread_name: TIMEOUT_THREAD_NAME, idle: :exit)
|
|
55
87
|
initialize_tracking
|
|
56
88
|
start_worker
|
|
57
89
|
end
|
|
@@ -59,10 +91,11 @@ module Julewire
|
|
|
59
91
|
def emit(record)
|
|
60
92
|
increment(:received)
|
|
61
93
|
return drop(:closed_dropped, record) if closed?
|
|
62
|
-
return drop(:queue_full_dropped, record) unless
|
|
94
|
+
return drop(:queue_full_dropped, record) unless @queue_slots.reserve
|
|
63
95
|
|
|
64
96
|
begin
|
|
65
|
-
@
|
|
97
|
+
degradation_marker = @health.degradation_marker
|
|
98
|
+
@port.send({ command: :emit, degradation_marker: degradation_marker, record: record })
|
|
66
99
|
increment(:queued)
|
|
67
100
|
rescue StandardError => e
|
|
68
101
|
release_slot
|
|
@@ -73,21 +106,22 @@ module Julewire
|
|
|
73
106
|
end
|
|
74
107
|
|
|
75
108
|
def flush(timeout: nil)
|
|
76
|
-
|
|
77
|
-
request(:flush, timeout: timeout)
|
|
109
|
+
@health.recover_if_successful { request(:flush, timeout: lifecycle_timeout(timeout)) }
|
|
78
110
|
end
|
|
79
111
|
|
|
80
112
|
def close(timeout: nil)
|
|
81
|
-
timeout =
|
|
113
|
+
timeout = lifecycle_timeout(timeout)
|
|
82
114
|
@closed.set(true)
|
|
83
|
-
result = request(:close, timeout: timeout)
|
|
84
|
-
close_ports
|
|
115
|
+
result = request(:close, timeout: timeout, allow_closed: true)
|
|
116
|
+
close_ports(timeout: timeout)
|
|
85
117
|
result
|
|
86
118
|
end
|
|
87
119
|
|
|
88
120
|
def after_fork!
|
|
89
|
-
close_ports
|
|
90
|
-
|
|
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
|
+
|
|
91
125
|
initialize_tracking
|
|
92
126
|
start_worker
|
|
93
127
|
self
|
|
@@ -100,11 +134,17 @@ module Julewire
|
|
|
100
134
|
|
|
101
135
|
def health
|
|
102
136
|
worker = request(:health, timeout: @request_timeout)
|
|
103
|
-
|
|
104
|
-
|
|
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
|
|
105
145
|
|
|
106
146
|
@health.snapshot(
|
|
107
|
-
in_flight: @
|
|
147
|
+
in_flight: @queue_slots.value,
|
|
108
148
|
max_queue: @max_queue,
|
|
109
149
|
status: status_for(worker),
|
|
110
150
|
worker: worker
|
|
@@ -119,22 +159,28 @@ module Julewire
|
|
|
119
159
|
end
|
|
120
160
|
|
|
121
161
|
def initialize_tracking
|
|
122
|
-
@
|
|
162
|
+
@process_id = Process.pid
|
|
163
|
+
@scheduler = ReplyTimeoutScheduler.new(timeout_value: false)
|
|
164
|
+
@closed = Concurrent::AtomicReference.new
|
|
123
165
|
@health = Core::Integration::DestinationHealth.new(counter_keys: COUNTER_KEYS, failure_counter: nil)
|
|
124
|
-
@
|
|
166
|
+
@queue_slots = QueueSlots.new(max_queue: @max_queue)
|
|
125
167
|
@worker_health = Concurrent::AtomicReference.new
|
|
126
168
|
end
|
|
127
169
|
|
|
128
170
|
def start_worker
|
|
129
171
|
@ack_port = ::Ractor::Port.new
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
|
133
180
|
@ack_thread = start_ack_thread
|
|
134
|
-
rescue
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
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}"
|
|
138
184
|
end
|
|
139
185
|
|
|
140
186
|
def spawn_worker(setup_port)
|
|
@@ -145,7 +191,7 @@ module Julewire
|
|
|
145
191
|
max_record_bytes = @max_record_bytes
|
|
146
192
|
close_output = @close_output
|
|
147
193
|
|
|
148
|
-
# :
|
|
194
|
+
# simplecov:disable
|
|
149
195
|
::Ractor.new(setup_port, ack_port, formatter, encoder, output, max_record_bytes, close_output,
|
|
150
196
|
name: "julewire-ractor-destination") do |worker_port, worker_ack_port, worker_formatter,
|
|
151
197
|
worker_encoder, worker_output, worker_max_record_bytes,
|
|
@@ -162,21 +208,19 @@ module Julewire
|
|
|
162
208
|
close_output: worker_close_output
|
|
163
209
|
)
|
|
164
210
|
end
|
|
165
|
-
# :
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
def receive_worker_port(setup_port)
|
|
169
|
-
selected, value = ::Ractor.select(setup_port, @worker)
|
|
170
|
-
return value if selected.equal?(setup_port) && value.is_a?(::Ractor::Port)
|
|
171
|
-
|
|
172
|
-
raise ArgumentError, "ractor destination worker did not start"
|
|
211
|
+
# simplecov:enable
|
|
173
212
|
end
|
|
174
213
|
|
|
175
214
|
def start_ack_thread
|
|
215
|
+
ack_port = @ack_port
|
|
176
216
|
thread = Thread.new do
|
|
177
217
|
loop do
|
|
178
|
-
message =
|
|
179
|
-
|
|
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
|
|
180
224
|
|
|
181
225
|
handle_ack(message)
|
|
182
226
|
end
|
|
@@ -184,73 +228,86 @@ module Julewire
|
|
|
184
228
|
record_failure(e, phase: :ack)
|
|
185
229
|
end
|
|
186
230
|
thread.name = "julewire-ractor-destination-ack"
|
|
187
|
-
thread.report_on_exception = true
|
|
188
231
|
thread
|
|
189
232
|
end
|
|
190
233
|
|
|
191
234
|
def handle_ack(message)
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
decrement_in_flight
|
|
195
|
-
case message[:status]
|
|
235
|
+
release_slot
|
|
236
|
+
case message.fetch(:status)
|
|
196
237
|
when :accepted
|
|
197
238
|
increment(:worker_accepted)
|
|
239
|
+
@health.clear_degradation_if_unchanged(message.fetch(:degradation_marker))
|
|
198
240
|
when :dropped
|
|
199
241
|
increment(:worker_dropped)
|
|
242
|
+
else
|
|
243
|
+
raise ArgumentError, "unknown ractor destination acknowledgement status: #{message.fetch(:status).inspect}"
|
|
200
244
|
end
|
|
201
245
|
end
|
|
202
246
|
|
|
203
|
-
def request(command, timeout:)
|
|
204
|
-
return false if closed? &&
|
|
247
|
+
def request(command, timeout:, allow_closed: false)
|
|
248
|
+
return false if closed? && !allow_closed
|
|
205
249
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
250
|
+
PortLifecycle.with_port do |reply|
|
|
251
|
+
@port.send({ command: command, reply: reply })
|
|
252
|
+
wait_for_reply(reply, timeout, command)
|
|
253
|
+
end
|
|
209
254
|
rescue StandardError => e
|
|
210
255
|
record_failure(e, phase: :request, command: command)
|
|
211
256
|
false
|
|
212
|
-
ensure
|
|
213
|
-
PortLifecycle.close(reply) if reply
|
|
214
257
|
end
|
|
215
258
|
|
|
216
|
-
def wait_for_reply(reply, timeout)
|
|
217
|
-
|
|
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)
|
|
218
263
|
|
|
219
|
-
|
|
220
|
-
token = @scheduler.schedule(timeout) do
|
|
221
|
-
timeout_port.send(:timeout)
|
|
222
|
-
rescue StandardError
|
|
223
|
-
nil
|
|
264
|
+
raise Core::Error, "ractor destination worker stopped before #{command} replied"
|
|
224
265
|
end
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
selected.equal?(timeout_port) ? false : response
|
|
228
|
-
ensure
|
|
229
|
-
@scheduler.cancel(token) if defined?(token)
|
|
230
|
-
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"
|
|
231
268
|
end
|
|
232
269
|
|
|
233
|
-
def
|
|
234
|
-
return true unless @max_queue.positive?
|
|
270
|
+
def closed? = @closed.get
|
|
235
271
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
|
242
294
|
end
|
|
295
|
+
|
|
296
|
+
worker_stopped
|
|
243
297
|
end
|
|
244
298
|
|
|
245
|
-
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
|
|
246
307
|
|
|
247
|
-
def
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
nil
|
|
251
|
-
ensure
|
|
252
|
-
PortLifecycle.close(@port) if @port
|
|
253
|
-
PortLifecycle.close(@ack_port) if @ack_port
|
|
308
|
+
def lifecycle_timeout(timeout)
|
|
309
|
+
Core::Validation.validate_timeout!(timeout, name: :timeout)
|
|
310
|
+
timeout || @request_timeout
|
|
254
311
|
end
|
|
255
312
|
|
|
256
313
|
def drop(reason, record)
|
|
@@ -264,39 +321,23 @@ module Julewire
|
|
|
264
321
|
end
|
|
265
322
|
|
|
266
323
|
def record_loss(reason, record)
|
|
267
|
-
metadata = Core::Records::Metadata.call(record)
|
|
268
324
|
@health.record_loss(
|
|
269
325
|
reason: reason,
|
|
270
|
-
event:
|
|
271
|
-
severity:
|
|
272
|
-
source:
|
|
326
|
+
event: record.event,
|
|
327
|
+
severity: record.severity,
|
|
328
|
+
source: record.source
|
|
273
329
|
)
|
|
274
330
|
end
|
|
275
331
|
|
|
276
332
|
def record_failure(error, **metadata)
|
|
277
|
-
@health.record_failure(error,
|
|
333
|
+
@health.record_failure(error, **metadata)
|
|
278
334
|
Core::Diagnostics::CallbackNotifier.call(@on_failure, error, { destination: name }.merge(metadata))
|
|
279
|
-
rescue StandardError
|
|
280
|
-
nil
|
|
281
335
|
end
|
|
282
336
|
|
|
283
337
|
def release_slot
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
current = @in_flight.value
|
|
288
|
-
if current <= 0
|
|
289
|
-
# Late or duplicate ACKs can arrive after teardown/reset; keep them visible
|
|
290
|
-
# without treating the ignored underflow as an operator-facing defect.
|
|
291
|
-
increment(:slot_underflow_ignored)
|
|
292
|
-
return
|
|
293
|
-
end
|
|
294
|
-
return if @in_flight.compare_and_set(current, current - 1)
|
|
295
|
-
end
|
|
296
|
-
end
|
|
297
|
-
|
|
298
|
-
def decrement_in_flight
|
|
299
|
-
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
|
|
300
341
|
end
|
|
301
342
|
|
|
302
343
|
def increment(key)
|
|
@@ -305,10 +346,8 @@ module Julewire
|
|
|
305
346
|
|
|
306
347
|
def status_for(worker)
|
|
307
348
|
return :closed if closed?
|
|
308
|
-
return :degraded if @health.degraded?
|
|
309
|
-
return :degraded if worker.is_a?(Hash) && worker[:status] == :degraded
|
|
310
349
|
|
|
311
|
-
:
|
|
350
|
+
:degraded if worker&.fetch(:status) == :degraded
|
|
312
351
|
end
|
|
313
352
|
end
|
|
314
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
|