julewire-core 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.
Files changed (139) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -0
  3. data/docs/advanced-configuration.md +2 -1
  4. data/docs/configuration.md +14 -7
  5. data/docs/context-and-propagation.md +26 -4
  6. data/docs/contracts.md +25 -50
  7. data/docs/extensions-and-api.md +87 -88
  8. data/docs/health-schema.md +12 -2
  9. data/docs/internals.md +24 -23
  10. data/docs/outputs-and-lifecycle.md +15 -8
  11. data/docs/records-and-data-policy.md +38 -7
  12. data/lib/julewire/core/cli/doctor.rb +20 -16
  13. data/lib/julewire/core/cli/line_helpers.rb +23 -12
  14. data/lib/julewire/core/cli/log_formats/core_json_decoder.rb +3 -3
  15. data/lib/julewire/core/cli/log_formats/record_decoder.rb +1 -1
  16. data/lib/julewire/core/cli/log_formats.rb +9 -9
  17. data/lib/julewire/core/cli/tail.rb +19 -14
  18. data/lib/julewire/core/cli/transcode.rb +4 -5
  19. data/lib/julewire/core/cli.rb +1 -1
  20. data/lib/julewire/core/configuration.rb +1 -1
  21. data/lib/julewire/core/context_store.rb +56 -87
  22. data/lib/julewire/core/destinations/chaos_output.rb +2 -2
  23. data/lib/julewire/core/destinations/collection.rb +8 -16
  24. data/lib/julewire/core/destinations/definition.rb +1 -9
  25. data/lib/julewire/core/destinations/destination.rb +16 -66
  26. data/lib/julewire/core/destinations/processor_handling.rb +78 -0
  27. data/lib/julewire/core/destinations/registry.rb +1 -1
  28. data/lib/julewire/core/destinations/sink.rb +0 -1
  29. data/lib/julewire/core/destinations/synchronized_output.rb +54 -17
  30. data/lib/julewire/core/destinations/tail_sampling.rb +18 -27
  31. data/lib/julewire/core/destinations/write_step.rb +5 -5
  32. data/lib/julewire/core/destinations.rb +0 -7
  33. data/lib/julewire/core/diagnostics/callback_notifier.rb +4 -2
  34. data/lib/julewire/core/diagnostics/doctor.rb +22 -15
  35. data/lib/julewire/core/diagnostics/failure_snapshot.rb +9 -3
  36. data/lib/julewire/core/diagnostics/health.rb +41 -57
  37. data/lib/julewire/core/diagnostics/integration_health_store.rb +11 -24
  38. data/lib/julewire/core/diagnostics/internal_records.rb +8 -12
  39. data/lib/julewire/core/diagnostics/invalid_severity_reporter.rb +16 -55
  40. data/lib/julewire/core/diagnostics/meta_observer.rb +37 -44
  41. data/lib/julewire/core/diagnostics/process_integration_health.rb +0 -5
  42. data/lib/julewire/core/diagnostics/tail/renderer.rb +1 -1
  43. data/lib/julewire/core/diagnostics/tail.rb +18 -22
  44. data/lib/julewire/core/execution/boundary.rb +2 -8
  45. data/lib/julewire/core/execution/handle.rb +14 -17
  46. data/lib/julewire/core/execution/lineage.rb +26 -62
  47. data/lib/julewire/core/execution/measurement_handle.rb +6 -10
  48. data/lib/julewire/core/execution/scope.rb +17 -22
  49. data/lib/julewire/core/execution/scope_fields.rb +4 -13
  50. data/lib/julewire/core/execution/scope_identity.rb +1 -9
  51. data/lib/julewire/core/execution/scope_snapshot.rb +18 -39
  52. data/lib/julewire/core/execution/summary_state.rb +16 -19
  53. data/lib/julewire/core/execution/view.rb +8 -17
  54. data/lib/julewire/core/facade_methods.rb +67 -65
  55. data/lib/julewire/core/fields/attribute_keys.rb +0 -2
  56. data/lib/julewire/core/fields/bags.rb +23 -27
  57. data/lib/julewire/core/fields/field_set.rb +52 -16
  58. data/lib/julewire/core/fields/field_stack.rb +69 -109
  59. data/lib/julewire/core/fields/internal/deletion.rb +6 -9
  60. data/lib/julewire/core/fields/internal.rb +35 -26
  61. data/lib/julewire/core/fields/lookup.rb +9 -13
  62. data/lib/julewire/core/fields/section_proxy.rb +1 -9
  63. data/lib/julewire/core/fields/stack_set.rb +9 -14
  64. data/lib/julewire/core/fields/static_labels.rb +1 -1
  65. data/lib/julewire/core/integration/configurable.rb +4 -3
  66. data/lib/julewire/core/integration/destination_health.rb +28 -7
  67. data/lib/julewire/core/integration/event_subscriber.rb +1 -1
  68. data/lib/julewire/core/integration/facade.rb +12 -15
  69. data/lib/julewire/core/integration/fork_hooks.rb +12 -36
  70. data/lib/julewire/core/integration/health.rb +0 -3
  71. data/lib/julewire/core/integration/ivar_state.rb +0 -5
  72. data/lib/julewire/core/integration/protocol.rb +20 -0
  73. data/lib/julewire/core/integration/settings.rb +4 -4
  74. data/lib/julewire/core/integration/subscriber_install.rb +2 -2
  75. data/lib/julewire/core/integration/subscription.rb +3 -2
  76. data/lib/julewire/core/integration/values.rb +0 -17
  77. data/lib/julewire/core/local_storage.rb +3 -19
  78. data/lib/julewire/core/processing/invalid_result_failure.rb +23 -0
  79. data/lib/julewire/core/processing/level_threshold.rb +7 -14
  80. data/lib/julewire/core/processing/match.rb +6 -4
  81. data/lib/julewire/core/processing/pipeline.rb +46 -40
  82. data/lib/julewire/core/processing/processor_chain.rb +22 -11
  83. data/lib/julewire/core/processing/processor_registry.rb +3 -4
  84. data/lib/julewire/core/processing/record_field_transform.rb +0 -4
  85. data/lib/julewire/core/processing/sampling.rb +1 -3
  86. data/lib/julewire/core/processing.rb +2 -2
  87. data/lib/julewire/core/propagation/carrier.rb +73 -19
  88. data/lib/julewire/core/propagation.rb +18 -4
  89. data/lib/julewire/core/records/build_input.rb +75 -0
  90. data/lib/julewire/core/records/console_formatter.rb +6 -6
  91. data/lib/julewire/core/records/deconstruct.rb +1 -1
  92. data/lib/julewire/core/records/display_message.rb +2 -4
  93. data/lib/julewire/core/records/draft.rb +127 -258
  94. data/lib/julewire/core/records/lazy_emit_input.rb +2 -12
  95. data/lib/julewire/core/records/metadata.rb +4 -3
  96. data/lib/julewire/core/records/public_projection.rb +2 -2
  97. data/lib/julewire/core/records/raw_input.rb +1 -1
  98. data/lib/julewire/core/records/record.rb +30 -18
  99. data/lib/julewire/core/records/severity.rb +7 -15
  100. data/lib/julewire/core/runtime.rb +87 -72
  101. data/lib/julewire/core/runtime_registry.rb +20 -15
  102. data/lib/julewire/core/runtime_state.rb +5 -3
  103. data/lib/julewire/core/scheduling/shared_scheduler.rb +28 -22
  104. data/lib/julewire/core/serialization/backtrace_limiter.rb +13 -9
  105. data/lib/julewire/core/serialization/bounded_transform.rb +3 -2
  106. data/lib/julewire/core/serialization/bounded_traversal.rb +72 -83
  107. data/lib/julewire/core/serialization/deep_compact_empty.rb +9 -11
  108. data/lib/julewire/core/serialization/deep_freeze.rb +45 -6
  109. data/lib/julewire/core/serialization/encoding_sanitizer.rb +2 -3
  110. data/lib/julewire/core/serialization/exception_shape.rb +7 -6
  111. data/lib/julewire/core/serialization/json_encoder.rb +10 -13
  112. data/lib/julewire/core/serialization/serializer.rb +22 -49
  113. data/lib/julewire/core/serialization/serializer_pool.rb +9 -0
  114. data/lib/julewire/core/serialization/text_encoder.rb +5 -5
  115. data/lib/julewire/core/serialization/truncation_metadata.rb +157 -0
  116. data/lib/julewire/core/serialization/value_copy.rb +183 -109
  117. data/lib/julewire/core/serialization/value_traversal.rb +10 -127
  118. data/lib/julewire/core/testing.rb +6 -56
  119. data/lib/julewire/core/validation.rb +2 -3
  120. data/lib/julewire/core/version.rb +1 -1
  121. data/lib/julewire/core.rb +2 -7
  122. metadata +7 -19
  123. data/lib/julewire/core/scheduling/deadline_scheduler.rb +0 -207
  124. data/lib/julewire/core/testing/chaos/catalog.rb +0 -72
  125. data/lib/julewire/core/testing/chaos/core_runtime.rb +0 -120
  126. data/lib/julewire/core/testing/chaos/destination.rb +0 -55
  127. data/lib/julewire/core/testing/chaos/emitter.rb +0 -20
  128. data/lib/julewire/core/testing/chaos/raising_output.rb +0 -42
  129. data/lib/julewire/core/testing/chaos.rb +0 -80
  130. data/lib/julewire/core/testing/contracts/component.rb +0 -162
  131. data/lib/julewire/core/testing/contracts/deadline_scheduler.rb +0 -59
  132. data/lib/julewire/core/testing/contracts/integration.rb +0 -166
  133. data/lib/julewire/core/testing/contracts/integration_fields.rb +0 -36
  134. data/lib/julewire/core/testing/contracts/record_draft.rb +0 -37
  135. data/lib/julewire/core/testing/contracts/runtime.rb +0 -178
  136. data/lib/julewire/core/testing/contracts/wire.rb +0 -60
  137. data/lib/julewire/core/testing/contracts.rb +0 -24
  138. data/lib/julewire/core/testing/coverage.rb +0 -58
  139. data/lib/julewire/core/testing/test_reports.rb +0 -78
@@ -6,8 +6,8 @@ module Julewire
6
6
  # Fiber-local context stack used by the runtime facade. Use Julewire.context
7
7
  # and Julewire.with_execution instead of reaching into this class directly.
8
8
  class ContextStore # rubocop:disable Metrics/ClassLength
9
- EMPTY_HASH = {}.freeze
10
- private_constant :EMPTY_HASH
9
+ PropagationOverlay = Data.define(:execution, :link_executions)
10
+ private_constant :PropagationOverlay
11
11
 
12
12
  class << self
13
13
  def current
@@ -26,10 +26,7 @@ module Julewire
26
26
  @scopes = []
27
27
  @ambient_fields = Fields::StackSet.new
28
28
  @execution_overlays = []
29
- @execution_lineage_overlays = []
30
29
  @propagation_execution_hash = nil
31
- @propagation_scope_snapshot = nil
32
- @linked_propagation_scope_snapshot = nil
33
30
  end
34
31
 
35
32
  def current_scope = @scopes.last
@@ -72,6 +69,7 @@ module Julewire
72
69
  current_field_hash(:neutral)
73
70
  end
74
71
 
72
+ # SectionProxy dispatches these dynamically from the public field readers.
75
73
  def context_value(key, default:)
76
74
  current_field_stack(:context).value_for(key, default: default)
77
75
  end
@@ -84,26 +82,24 @@ module Julewire
84
82
  current_field_stack(:attributes).value_for(key, default: default)
85
83
  end
86
84
 
87
- def add_context(fields = EMPTY_HASH, owned: false, **keyword_fields)
85
+ def add_context(fields = nil, owned: false, **keyword_fields)
88
86
  add_field(:context, field_input(fields, keyword_fields), owned: owned)
89
87
  end
90
88
 
91
- def add_carry(fields = EMPTY_HASH, owned: false, **keyword_fields)
89
+ def add_carry(fields = nil, owned: false, **keyword_fields)
92
90
  add_field(:carry, field_input(fields, keyword_fields), owned: owned)
93
91
  end
94
92
 
95
- def add_attributes(fields = EMPTY_HASH, owned: false, **keyword_fields)
93
+ def add_attributes(fields = nil, owned: false, **keyword_fields)
96
94
  add_field(:attributes, field_input(fields, keyword_fields), owned: owned)
97
95
  end
98
96
 
99
- def add_neutral(fields = EMPTY_HASH, owned: false, **keyword_fields)
97
+ def add_neutral(fields = nil, owned: false, **keyword_fields)
100
98
  add_field(:neutral, field_input(fields, keyword_fields), owned: owned)
101
99
  end
102
100
 
103
101
  def delete_carry(path)
104
102
  path = Fields::Internal.normalize_path(path)
105
- return if path.empty?
106
-
107
103
  if current_scope
108
104
  current_scope.delete_carry(path)
109
105
  else
@@ -111,19 +107,19 @@ module Julewire
111
107
  end
112
108
  end
113
109
 
114
- def with_context(fields = EMPTY_HASH, owned: false, **keyword_fields, &)
110
+ def with_context(fields = nil, owned: false, **keyword_fields, &)
115
111
  with_scope_or_ambient_overlay(:context, field_input(fields, keyword_fields), owned: owned, &)
116
112
  end
117
113
 
118
- def with_carry(fields = EMPTY_HASH, owned: false, **keyword_fields, &)
114
+ def with_carry(fields = nil, owned: false, **keyword_fields, &)
119
115
  with_scope_or_ambient_overlay(:carry, field_input(fields, keyword_fields), owned: owned, &)
120
116
  end
121
117
 
122
- def with_attributes(fields = EMPTY_HASH, owned: false, **keyword_fields, &)
118
+ def with_attributes(fields = nil, owned: false, **keyword_fields, &)
123
119
  with_scope_or_ambient_overlay(:attributes, field_input(fields, keyword_fields), owned: owned, &)
124
120
  end
125
121
 
126
- def with_neutral(fields = EMPTY_HASH, owned: false, **keyword_fields, &)
122
+ def with_neutral(fields = nil, owned: false, **keyword_fields, &)
127
123
  with_scope_or_ambient_overlay(:neutral, field_input(fields, keyword_fields), owned: owned, &)
128
124
  end
129
125
 
@@ -139,26 +135,21 @@ module Julewire
139
135
  end
140
136
  end
141
137
 
142
- def with_propagation(context: {}, carry: {}, execution: {}, link_executions: false, &)
143
- scope = current_scope
144
- execution = Fields::FieldSet.deep_symbolize_keys(execution)
145
- @execution_overlays.push(execution)
146
- @execution_lineage_overlays.push(link_executions ? Execution::Lineage.from_execution_hash(execution) : nil)
138
+ def with_propagation(context:, carry:, execution:, link_executions:, owned:, &)
139
+ execution = if owned
140
+ Serialization::DeepFreeze.validate_symbol_hash(execution)
141
+ else
142
+ Fields::FieldSet.deep_symbolize_keys(execution)
143
+ end
144
+ @execution_overlays.push(PropagationOverlay.new(execution: execution, link_executions: link_executions))
147
145
  invalidate_propagation_cache!
148
146
 
149
147
  begin
150
- if scope
151
- scope.with_carry(carry) do
152
- scope.with_context(context, &)
153
- end
154
- else
155
- @ambient_fields.with(:carry, carry) do
156
- @ambient_fields.with(:context, context, &)
157
- end
148
+ with_carry(carry, owned: owned) do
149
+ with_context(context, owned: owned, &)
158
150
  end
159
151
  ensure
160
152
  @execution_overlays.pop
161
- @execution_lineage_overlays.pop
162
153
  invalidate_propagation_cache!
163
154
  end
164
155
  end
@@ -209,12 +200,12 @@ module Julewire
209
200
  Execution::Scope.new(
210
201
  type: options.fetch(:type),
211
202
  id: options[:id],
212
- execution: merged_execution_hash(options.fetch(:execution, EMPTY_HASH)),
203
+ execution: merged_execution_hash(options.fetch(:execution, {}), owned: options.fetch(:owned, false)),
213
204
  execution_owned: true,
214
205
  context: fields.stack(:context),
215
206
  attributes: fields.stack(:attributes),
216
207
  neutral: fields.stack(:neutral),
217
- labels: options.fetch(:labels, EMPTY_HASH),
208
+ labels: options[:labels],
218
209
  carry: fields.stack(:carry),
219
210
  parent: parent_scope || linked_propagation_scope_snapshot,
220
211
  started_at: options[:started_at],
@@ -233,7 +224,7 @@ module Julewire
233
224
  end
234
225
 
235
226
  def add_scope_stack(stack_set, options, section:, key:)
236
- value = options.fetch(key, EMPTY_HASH)
227
+ value = options[key]
237
228
  if options.fetch(:owned, false)
238
229
  stack_set.add(section, value, owned: true)
239
230
  else
@@ -241,37 +232,26 @@ module Julewire
241
232
  end
242
233
  end
243
234
 
244
- def add_field(section, fields, owned: false)
235
+ def add_field(section, fields, owned:)
245
236
  scope = current_scope
246
237
  if scope
247
238
  scope.add_field(section, fields, owned: owned)
248
- elsif owned
249
- @ambient_fields.add(section, fields, owned: true)
250
239
  else
251
- @ambient_fields.add(section, fields)
240
+ @ambient_fields.add(section, fields, owned: owned)
252
241
  end
253
242
  end
254
243
 
255
244
  def field_input(fields, keyword_fields)
256
245
  return fields if keyword_fields.empty?
257
- return keyword_fields if empty_field_input?(fields)
258
-
259
- fields.is_a?(Hash) ? fields.merge(keyword_fields) : fields
260
- end
261
246
 
262
- def empty_field_input?(fields)
263
- fields.nil? || (fields.respond_to?(:empty?) && fields.empty?)
247
+ Fields::FieldSet.coerce(fields, keyword_fields, invalid: :wrap)
264
248
  end
265
249
 
266
- def with_scope_or_ambient_overlay(section, fields, owned: false, &)
250
+ def with_scope_or_ambient_overlay(section, fields, owned:, &)
267
251
  scope = current_scope
268
252
  return scope.with_field(section, fields, owned: owned, &) if scope
269
253
 
270
- if owned
271
- @ambient_fields.with(section, fields, owned: true, &)
272
- else
273
- @ambient_fields.with(section, fields, &)
274
- end
254
+ @ambient_fields.with(section, fields, owned: owned, &)
275
255
  end
276
256
 
277
257
  def current_field_stack(section)
@@ -292,32 +272,31 @@ module Julewire
292
272
  end
293
273
 
294
274
  def execution_hash
295
- return {} if @execution_overlays.empty?
296
-
297
- Fields::FieldSet.deep_dup(propagation_execution_hash)
275
+ Fields::FieldSet.deep_dup_owned(propagation_execution_hash)
298
276
  end
299
277
 
300
278
  def propagation_execution_hash
301
- @propagation_execution_hash ||= Fields::Internal.frozen_copy(@execution_overlays.reduce({}) do |memo, overlay|
302
- Fields::FieldSet.merge!(memo, overlay)
303
- end)
279
+ @propagation_execution_hash ||= Fields::Internal.frozen_owned_copy(
280
+ @execution_overlays.reduce({}) do |memo, overlay|
281
+ Fields::Internal.merge_owned!(memo, overlay.execution)
282
+ end
283
+ )
304
284
  end
305
285
 
306
286
  def propagation_scope_snapshot
307
287
  execution = propagation_execution_hash
308
288
  return if execution.empty?
309
289
 
310
- @propagation_scope_snapshot ||= Execution::ScopeSnapshot.new(execution: execution)
290
+ @propagation_scope_snapshot ||= Execution::ScopeSnapshot.new(execution: execution, owned: true)
311
291
  end
312
292
 
313
293
  def linked_propagation_scope_snapshot
314
- lineage = linked_propagation_lineage
315
- return unless lineage
294
+ return unless linked_propagation?
316
295
 
317
- execution = propagation_execution_hash
318
- return if execution.empty?
319
-
320
- @linked_propagation_scope_snapshot ||= Execution::ScopeSnapshot.new(execution: execution, lineage: lineage)
296
+ @linked_propagation_scope_snapshot ||= Execution::ScopeSnapshot.new(
297
+ execution: propagation_execution_hash,
298
+ owned: true
299
+ )
321
300
  end
322
301
 
323
302
  def invalidate_propagation_cache!
@@ -326,56 +305,46 @@ module Julewire
326
305
  @linked_propagation_scope_snapshot = nil
327
306
  end
328
307
 
329
- def linked_propagation_lineage
330
- (@execution_overlays.length - 1).downto(0) do |index|
331
- execution = @execution_overlays.fetch(index)
332
- next if execution.empty?
333
-
334
- return @execution_lineage_overlays.fetch(index)
335
- end
336
- nil
308
+ def linked_propagation?
309
+ @execution_overlays.reverse_each.find { |overlay| !overlay.execution.empty? }&.link_executions
337
310
  end
338
311
 
339
- def merged_execution_hash(execution)
340
- inherited = inherited_execution_hash
341
- return inherited unless execution.is_a?(Hash) && !execution.empty?
312
+ def merged_execution_hash(execution, owned:)
313
+ return Fields::FieldSet.merge!(inherited_execution_hash, execution) unless owned
342
314
 
343
- Fields::FieldSet.merge!(inherited, execution)
315
+ Serialization::DeepFreeze.validate_symbol_hash(execution)
316
+ Fields::Internal.merge_owned!(inherited_execution_hash, execution)
344
317
  end
345
318
 
346
319
  def inherited_execution_hash
347
320
  scope = current_scope
348
321
  return execution_hash unless scope
349
322
 
350
- inherited = scope.inheritable_execution_hash
351
- return inherited if @execution_overlays.empty?
352
-
353
- overlay = execution_hash
354
- return inherited if overlay.empty?
355
-
356
- Fields::FieldSet.merge!(inherited, overlay)
323
+ Fields::Internal.merge_owned!(scope.inheritable_execution_hash, execution_hash)
357
324
  end
358
325
 
359
326
  def finish_scope(scope, on_finish, on_finish_failure, active_exception: nil)
360
327
  return unless on_finish
361
328
 
362
- contain_finish_failure(on_finish_failure, active_exception) { scope.finish_owned unless scope.finished? }
363
- contain_finish_failure(on_finish_failure, active_exception) { on_finish.call(scope) }
329
+ contain_finish_failure(on_finish_failure, active_exception, phase: :summary_finish) do
330
+ scope.finish_owned unless scope.finished?
331
+ end
332
+ contain_finish_failure(on_finish_failure, active_exception, phase: :summary_emit) { on_finish.call(scope) }
364
333
  end
365
334
 
366
- def contain_finish_failure(on_finish_failure, active_exception)
335
+ def contain_finish_failure(on_finish_failure, active_exception, phase:)
367
336
  yield
368
337
  rescue StandardError => e
369
- report_finish_failure(on_finish_failure, e)
338
+ report_finish_failure(on_finish_failure, e, phase: phase)
370
339
  rescue SystemStackError => e
371
340
  # Preserve the app's active stack error during unwind.
372
341
  raise unless active_exception
373
342
 
374
- report_finish_failure(on_finish_failure, e)
343
+ report_finish_failure(on_finish_failure, e, phase: phase)
375
344
  end
376
345
 
377
- def report_finish_failure(on_finish_failure, error)
378
- on_finish_failure&.call(error)
346
+ def report_finish_failure(on_finish_failure, error, phase:)
347
+ on_finish_failure.call(error, phase: phase)
379
348
  rescue StandardError
380
349
  nil
381
350
  end
@@ -25,7 +25,7 @@ module Julewire
25
25
  when :raise then raise "julewire punk chaos output failure"
26
26
  when :reject then false
27
27
  when :sleep
28
- sleep(@sleep_seconds)
28
+ Kernel.sleep(@sleep_seconds)
29
29
  @output.write(value)
30
30
  end
31
31
  end
@@ -77,7 +77,7 @@ module Julewire
77
77
  end
78
78
 
79
79
  def trigger?
80
- @rate.positive? && @random.rand < @rate
80
+ @random.rand < @rate
81
81
  end
82
82
 
83
83
  def chaos_mode
@@ -6,7 +6,7 @@ module Julewire
6
6
  module Destinations
7
7
  class Collection
8
8
  def initialize(destinations, on_drop:, on_failure:)
9
- @destinations = destinations.freeze
9
+ @destinations = destinations.dup.freeze
10
10
  @on_drop = on_drop
11
11
  @on_failure = on_failure
12
12
  end
@@ -23,9 +23,9 @@ module Julewire
23
23
  private
24
24
 
25
25
  def validate_destinations(destinations)
26
- destinations.map do |destination|
26
+ destinations.each do |destination|
27
27
  Registry.validate!(destination)
28
- end.freeze
28
+ end
29
29
  end
30
30
  end
31
31
 
@@ -76,7 +76,7 @@ module Julewire
76
76
 
77
77
  lifecycle_destinations(skip_resource_identities).each do |destination|
78
78
  remaining_timeout = Scheduling::Deadline.remaining(deadline)
79
- if attempted && deadline && remaining_timeout <= 0
79
+ if attempted && deadline && remaining_timeout.zero?
80
80
  ok = false
81
81
  break
82
82
  end
@@ -96,7 +96,7 @@ module Julewire
96
96
  def lifecycle_destinations(skip_resource_identities)
97
97
  return @destinations unless skip_resource_identities
98
98
 
99
- @destinations.reject { skip_lifecycle_destination?(it, skip_resource_identities) }
99
+ @destinations.reject { skip_resource_identities.key?(resource_identity(it)) }
100
100
  end
101
101
 
102
102
  def call_destination_after_fork(destination)
@@ -108,13 +108,6 @@ module Julewire
108
108
  destination: destination_name(destination),
109
109
  phase: :destination_lifecycle
110
110
  )
111
- nil
112
- end
113
-
114
- def skip_lifecycle_destination?(destination, identities)
115
- return false unless identities
116
-
117
- identities.key?(resource_identity(destination))
118
111
  end
119
112
 
120
113
  def resource_identity(destination)
@@ -125,7 +118,7 @@ module Julewire
125
118
 
126
119
  def emit_to_destination(destination, record)
127
120
  result = destination.emit(record)
128
- record_drop(:destination_rejected, destination, record) if result == false
121
+ record_drop(:destination_rejected, metadata: destination_metadata(destination, record)) if result == false
129
122
  rescue StandardError => e
130
123
  metadata = destination_metadata(destination, record)
131
124
  notify_failure(
@@ -133,8 +126,7 @@ module Julewire
133
126
  **metadata,
134
127
  phase: :destination
135
128
  )
136
- record_drop(:destination_exception, destination, record, metadata: metadata)
137
- nil
129
+ record_drop(:destination_exception, metadata: metadata)
138
130
  end
139
131
 
140
132
  def destination_name(destination)
@@ -161,7 +153,7 @@ module Julewire
161
153
  @on_failure.call(error, **metadata)
162
154
  end
163
155
 
164
- def record_drop(reason, destination, record, metadata: destination_metadata(destination, record))
156
+ def record_drop(reason, metadata:)
165
157
  @on_drop.call(reason, phase: :destination, **metadata)
166
158
  end
167
159
 
@@ -43,7 +43,6 @@ module Julewire
43
43
  on_drop: resolve(:on_drop, defaults),
44
44
  on_failure: resolve(:on_failure, defaults),
45
45
  output: output,
46
- error_backtrace_lines: defaults.fetch(:error_backtrace_lines),
47
46
  processors: resolve(:processors, defaults)
48
47
  )
49
48
  end
@@ -95,17 +94,10 @@ module Julewire
95
94
  return defaults.fetch(key) if defaults.key?(key)
96
95
 
97
96
  case key
98
- when :close_output
99
- false
100
- when :encoder, :formatter, :on_drop, :on_failure
97
+ when :encoder, :formatter
101
98
  raise ArgumentError, "destination default #{key} is required"
102
99
  when :max_record_bytes
103
100
  DEFAULT_MAX_RECORD_BYTES
104
- when :processors
105
- []
106
- when :output
107
- # No inherited output exists; build raises the required-output error.
108
- nil
109
101
  end
110
102
  end
111
103
 
@@ -4,6 +4,8 @@ module Julewire
4
4
  module Core
5
5
  module Destinations
6
6
  class Destination
7
+ include ProcessorHandling
8
+
7
9
  COUNTER_KEYS = %i[
8
10
  callback_error
9
11
  encode_error
@@ -15,6 +17,7 @@ module Julewire
15
17
  output_rejected
16
18
  processor_dropped
17
19
  processor_error
20
+ processor_invalid
18
21
  received
19
22
  record_too_large
20
23
  ].freeze
@@ -30,8 +33,7 @@ module Julewire
30
33
  on_drop:,
31
34
  on_failure:,
32
35
  output:,
33
- error_backtrace_lines: Core::MAX_BACKTRACE_LINES,
34
- processors: []
36
+ processors: nil
35
37
  )
36
38
  @name = Destinations.normalize_name(name)
37
39
  @formatter = validate_callable(formatter, name: :formatter)
@@ -43,7 +45,7 @@ module Julewire
43
45
  raise ArgumentError, "destination #{@name.inspect} output is required" if output.nil?
44
46
 
45
47
  @output = Sink.wrap(output, close_output: close_output)
46
- @processor_chain = processor_chain(processors, error_backtrace_lines)
48
+ @processor_chain = processor_chain(processors)
47
49
  initialize_tracking
48
50
  @write_step = build_write_step
49
51
  end
@@ -57,10 +59,7 @@ module Julewire
57
59
  end
58
60
 
59
61
  def emit_processed_record(record, degradation_marker:)
60
- return unless @write_step.call(record) == :accepted
61
-
62
- clear_degradation_if_unchanged(degradation_marker)
63
- nil
62
+ clear_degradation_if_unchanged(degradation_marker) if @write_step.call(record)
64
63
  end
65
64
 
66
65
  def flush(timeout: nil)
@@ -73,7 +72,7 @@ module Julewire
73
72
 
74
73
  def after_fork!
75
74
  initialize_tracking
76
- @output.after_fork! if @output.respond_to?(:after_fork!)
75
+ @output.after_fork!
77
76
  self
78
77
  rescue StandardError => e
79
78
  notify_failure(
@@ -86,9 +85,7 @@ module Julewire
86
85
  end
87
86
 
88
87
  def resource_identity
89
- return @output.resource_identity if @output.respond_to?(:resource_identity)
90
-
91
- @output
88
+ @output.resource_identity
92
89
  end
93
90
 
94
91
  def health
@@ -146,7 +143,7 @@ module Julewire
146
143
 
147
144
  def record_step_metadata(metadata)
148
145
  record = metadata.delete(:record)
149
- metadata[:record_metadata] = Records::Metadata.call(record) if record
146
+ metadata[:record_metadata] = Records::Metadata.call(record)
150
147
  metadata
151
148
  end
152
149
 
@@ -166,7 +163,7 @@ module Julewire
166
163
  end
167
164
 
168
165
  def record_loss(reason, metadata)
169
- record_metadata = metadata.fetch(:record_metadata, {})
166
+ record_metadata = metadata.fetch(:record_metadata)
170
167
  @health.record_loss(
171
168
  reason: reason,
172
169
  counter: nil,
@@ -177,10 +174,6 @@ module Julewire
177
174
  )
178
175
  end
179
176
 
180
- def clear_degradation
181
- @health.clear_degradation
182
- end
183
-
184
177
  def clear_degradation_if_unchanged(marker)
185
178
  @health.clear_degradation_if_unchanged(marker)
186
179
  end
@@ -195,57 +188,16 @@ module Julewire
195
188
  callback
196
189
  end
197
190
 
198
- def processor_chain(processors, error_backtrace_lines)
199
- processors = processor_entries(processors)
200
- return if processors.empty?
201
-
202
- Processing::ProcessorChain.new(
203
- processors: processors,
204
- error_backtrace_lines: error_backtrace_lines,
205
- on_error: method(:record_processor_error)
206
- )
207
- end
208
-
209
- def processor_entries(value)
210
- case value
211
- when Processing::ProcessorRegistry
212
- value.to_a
213
- else
214
- Processing::ProcessorRegistry.new(Array(value)).to_a
215
- end
216
- end
217
-
218
- def process_record(record)
219
- return record unless @processor_chain
220
-
221
- processed = @processor_chain.call(Records::Draft.from_record(record, freeze_sections: false))
222
- if processed.equal?(Processing::ProcessorChain::DROP)
223
- increment_counter(:processor_dropped)
224
- nil
225
- elsif processed.is_a?(Processing::ProcessorChain::ErrorResult)
226
- processed.draft.to_record
227
- else
228
- processed.to_record
229
- end
230
- rescue StandardError => e
231
- notify_failure(e, phase: :destination_processor, record_metadata: Records::Metadata.call(record))
232
- nil
233
- end
234
-
235
- def record_processor_error(error, record_metadata)
236
- increment_counter(:processor_error)
237
- notify_failure(error, phase: :destination_processor, record_metadata: record_metadata)
238
- end
239
-
240
191
  def call_output_lifecycle(method_name, timeout:)
241
192
  Validation.validate_timeout!(timeout, name: :timeout)
242
193
  call_output_lifecycle_safely(method_name, timeout)
243
194
  end
244
195
 
245
- def call_output_lifecycle_safely(method_name, _timeout)
246
- # Collection owns the shared deadline; plain outputs expose no timeout API.
247
- result = @output.public_send(method_name)
248
- clear_degradation if method_name == :flush && result != false
196
+ def call_output_lifecycle_safely(method_name, timeout)
197
+ # Sink.wrap centralizes timeout-aware lifecycle dispatch for every output.
198
+ degradation_marker = @health.degradation_marker
199
+ result = @output.public_send(method_name, timeout: timeout)
200
+ clear_degradation_if_unchanged(degradation_marker) if method_name == :flush && result
249
201
  result
250
202
  rescue StandardError => e
251
203
  notify_failure(
@@ -258,9 +210,7 @@ module Julewire
258
210
  end
259
211
 
260
212
  def output_class_name
261
- return @output.output_class_name if @output.respond_to?(:output_class_name)
262
-
263
- @output.class.name
213
+ @output.output_class_name
264
214
  end
265
215
  end
266
216
  end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ module Destinations
6
+ # @api internal
7
+ module ProcessorHandling
8
+ private
9
+
10
+ def processor_chain(processors)
11
+ processors = processor_entries(processors)
12
+ return if processors.empty?
13
+
14
+ Processing::ProcessorChain.new(
15
+ processors: processors,
16
+ on_error: method(:record_processor_error),
17
+ on_invalid: method(:record_invalid_processor_result),
18
+ on_invalid_draft: method(:record_invalid_processor_draft)
19
+ )
20
+ end
21
+
22
+ def processor_entries(value)
23
+ case value
24
+ when Processing::ProcessorRegistry
25
+ value.to_a
26
+ else
27
+ Processing::ProcessorRegistry.new(Array(value)).to_a
28
+ end
29
+ end
30
+
31
+ def process_record(record)
32
+ return record unless @processor_chain
33
+
34
+ processed = @processor_chain.call(Records::Draft.from_record(record))
35
+ if processed.equal?(Processing::ProcessorChain::DROP)
36
+ drop_processed_record
37
+ elsif processed.instance_of?(Processing::ProcessorChain::ErrorResult)
38
+ processed.draft.to_record
39
+ else
40
+ processed.to_record
41
+ end
42
+ end
43
+
44
+ def drop_processed_record
45
+ increment_counter(:processor_dropped)
46
+ nil
47
+ end
48
+
49
+ def record_processor_error(error, record_metadata)
50
+ increment_counter(:processor_error)
51
+ notify_failure(error, phase: :destination_processor, record_metadata: record_metadata)
52
+ end
53
+
54
+ def record_invalid_processor_result(processor_name, result, record_metadata)
55
+ increment_counter(:processor_invalid)
56
+ error, metadata = Processing::InvalidResultFailure.build(
57
+ message: "destination processor returned unsupported result",
58
+ phase: :destination_processor_result,
59
+ processor_name: processor_name,
60
+ record_metadata: record_metadata,
61
+ result: result
62
+ )
63
+ notify_failure(error, **metadata)
64
+ end
65
+
66
+ def record_invalid_processor_draft(processor_name, error, record_metadata)
67
+ increment_counter(:processor_invalid)
68
+ notify_failure(
69
+ error,
70
+ phase: :destination_processor_data,
71
+ processor: processor_name,
72
+ record_metadata: record_metadata
73
+ )
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -49,7 +49,7 @@ module Julewire
49
49
  def build(defaults:)
50
50
  output_identities = {}.compare_by_identity
51
51
  @definitions.map do |definition|
52
- if definition.is_a?(Definition)
52
+ if definition.instance_of?(Definition)
53
53
  definition.build(defaults: defaults, output_identities: output_identities)
54
54
  else
55
55
  definition