julewire-core 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.
Files changed (137) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -0
  3. data/docs/configuration.md +14 -7
  4. data/docs/context-and-propagation.md +20 -0
  5. data/docs/contracts.md +23 -49
  6. data/docs/extensions-and-api.md +78 -82
  7. data/docs/health-schema.md +12 -2
  8. data/docs/internals.md +21 -20
  9. data/docs/outputs-and-lifecycle.md +10 -6
  10. data/docs/records-and-data-policy.md +36 -5
  11. data/lib/julewire/core/cli/doctor.rb +20 -16
  12. data/lib/julewire/core/cli/line_helpers.rb +23 -12
  13. data/lib/julewire/core/cli/log_formats/core_json_decoder.rb +3 -3
  14. data/lib/julewire/core/cli/log_formats.rb +9 -9
  15. data/lib/julewire/core/cli/tail.rb +19 -14
  16. data/lib/julewire/core/cli/transcode.rb +4 -5
  17. data/lib/julewire/core/cli.rb +1 -1
  18. data/lib/julewire/core/configuration.rb +1 -1
  19. data/lib/julewire/core/context_store.rb +51 -87
  20. data/lib/julewire/core/destinations/chaos_output.rb +2 -2
  21. data/lib/julewire/core/destinations/collection.rb +8 -16
  22. data/lib/julewire/core/destinations/definition.rb +1 -9
  23. data/lib/julewire/core/destinations/destination.rb +13 -63
  24. data/lib/julewire/core/destinations/processor_handling.rb +78 -0
  25. data/lib/julewire/core/destinations/registry.rb +1 -1
  26. data/lib/julewire/core/destinations/sink.rb +0 -1
  27. data/lib/julewire/core/destinations/synchronized_output.rb +14 -13
  28. data/lib/julewire/core/destinations/tail_sampling.rb +18 -27
  29. data/lib/julewire/core/destinations/write_step.rb +5 -5
  30. data/lib/julewire/core/destinations.rb +0 -7
  31. data/lib/julewire/core/diagnostics/callback_notifier.rb +4 -2
  32. data/lib/julewire/core/diagnostics/doctor.rb +16 -9
  33. data/lib/julewire/core/diagnostics/failure_snapshot.rb +6 -2
  34. data/lib/julewire/core/diagnostics/health.rb +41 -57
  35. data/lib/julewire/core/diagnostics/integration_health_store.rb +11 -24
  36. data/lib/julewire/core/diagnostics/internal_records.rb +8 -12
  37. data/lib/julewire/core/diagnostics/invalid_severity_reporter.rb +16 -55
  38. data/lib/julewire/core/diagnostics/meta_observer.rb +37 -44
  39. data/lib/julewire/core/diagnostics/process_integration_health.rb +0 -5
  40. data/lib/julewire/core/diagnostics/tail/renderer.rb +1 -1
  41. data/lib/julewire/core/diagnostics/tail.rb +18 -22
  42. data/lib/julewire/core/execution/boundary.rb +2 -8
  43. data/lib/julewire/core/execution/handle.rb +12 -17
  44. data/lib/julewire/core/execution/lineage.rb +16 -56
  45. data/lib/julewire/core/execution/measurement_handle.rb +6 -10
  46. data/lib/julewire/core/execution/scope.rb +15 -20
  47. data/lib/julewire/core/execution/scope_fields.rb +3 -12
  48. data/lib/julewire/core/execution/scope_identity.rb +1 -9
  49. data/lib/julewire/core/execution/scope_snapshot.rb +18 -39
  50. data/lib/julewire/core/execution/summary_state.rb +16 -19
  51. data/lib/julewire/core/execution/view.rb +8 -17
  52. data/lib/julewire/core/facade_methods.rb +67 -65
  53. data/lib/julewire/core/fields/attribute_keys.rb +0 -2
  54. data/lib/julewire/core/fields/bags.rb +23 -27
  55. data/lib/julewire/core/fields/field_set.rb +29 -19
  56. data/lib/julewire/core/fields/field_stack.rb +68 -134
  57. data/lib/julewire/core/fields/internal/deletion.rb +6 -9
  58. data/lib/julewire/core/fields/internal.rb +19 -38
  59. data/lib/julewire/core/fields/lookup.rb +9 -13
  60. data/lib/julewire/core/fields/section_proxy.rb +1 -9
  61. data/lib/julewire/core/fields/stack_set.rb +7 -12
  62. data/lib/julewire/core/fields/static_labels.rb +1 -1
  63. data/lib/julewire/core/integration/configurable.rb +4 -3
  64. data/lib/julewire/core/integration/destination_health.rb +17 -7
  65. data/lib/julewire/core/integration/event_subscriber.rb +1 -1
  66. data/lib/julewire/core/integration/facade.rb +12 -15
  67. data/lib/julewire/core/integration/fork_hooks.rb +12 -36
  68. data/lib/julewire/core/integration/health.rb +0 -3
  69. data/lib/julewire/core/integration/ivar_state.rb +0 -5
  70. data/lib/julewire/core/integration/protocol.rb +20 -0
  71. data/lib/julewire/core/integration/settings.rb +4 -4
  72. data/lib/julewire/core/integration/subscriber_install.rb +2 -2
  73. data/lib/julewire/core/integration/subscription.rb +3 -2
  74. data/lib/julewire/core/integration/values.rb +0 -17
  75. data/lib/julewire/core/local_storage.rb +3 -19
  76. data/lib/julewire/core/processing/invalid_result_failure.rb +23 -0
  77. data/lib/julewire/core/processing/level_threshold.rb +7 -14
  78. data/lib/julewire/core/processing/match.rb +6 -4
  79. data/lib/julewire/core/processing/pipeline.rb +46 -40
  80. data/lib/julewire/core/processing/processor_chain.rb +22 -11
  81. data/lib/julewire/core/processing/processor_registry.rb +3 -4
  82. data/lib/julewire/core/processing/record_field_transform.rb +0 -4
  83. data/lib/julewire/core/processing/sampling.rb +1 -3
  84. data/lib/julewire/core/processing.rb +2 -2
  85. data/lib/julewire/core/propagation/carrier.rb +14 -14
  86. data/lib/julewire/core/propagation.rb +15 -2
  87. data/lib/julewire/core/records/build_input.rb +75 -0
  88. data/lib/julewire/core/records/console_formatter.rb +6 -6
  89. data/lib/julewire/core/records/deconstruct.rb +1 -1
  90. data/lib/julewire/core/records/display_message.rb +2 -4
  91. data/lib/julewire/core/records/draft.rb +125 -262
  92. data/lib/julewire/core/records/lazy_emit_input.rb +2 -12
  93. data/lib/julewire/core/records/metadata.rb +4 -3
  94. data/lib/julewire/core/records/public_projection.rb +2 -2
  95. data/lib/julewire/core/records/raw_input.rb +1 -1
  96. data/lib/julewire/core/records/record.rb +19 -37
  97. data/lib/julewire/core/records/severity.rb +7 -15
  98. data/lib/julewire/core/runtime.rb +67 -71
  99. data/lib/julewire/core/runtime_registry.rb +20 -15
  100. data/lib/julewire/core/runtime_state.rb +5 -3
  101. data/lib/julewire/core/scheduling/shared_scheduler.rb +28 -22
  102. data/lib/julewire/core/serialization/backtrace_limiter.rb +13 -9
  103. data/lib/julewire/core/serialization/bounded_transform.rb +2 -12
  104. data/lib/julewire/core/serialization/bounded_traversal.rb +49 -66
  105. data/lib/julewire/core/serialization/deep_compact_empty.rb +9 -11
  106. data/lib/julewire/core/serialization/deep_freeze.rb +45 -6
  107. data/lib/julewire/core/serialization/encoding_sanitizer.rb +2 -3
  108. data/lib/julewire/core/serialization/exception_shape.rb +7 -6
  109. data/lib/julewire/core/serialization/json_encoder.rb +10 -13
  110. data/lib/julewire/core/serialization/serializer.rb +23 -50
  111. data/lib/julewire/core/serialization/serializer_pool.rb +9 -0
  112. data/lib/julewire/core/serialization/text_encoder.rb +5 -5
  113. data/lib/julewire/core/serialization/truncation_metadata.rb +63 -48
  114. data/lib/julewire/core/serialization/value_copy.rb +60 -227
  115. data/lib/julewire/core/serialization/value_traversal.rb +10 -127
  116. data/lib/julewire/core/testing.rb +6 -56
  117. data/lib/julewire/core/validation.rb +2 -3
  118. data/lib/julewire/core/version.rb +1 -1
  119. data/lib/julewire/core.rb +2 -7
  120. metadata +6 -19
  121. data/lib/julewire/core/scheduling/deadline_scheduler.rb +0 -207
  122. data/lib/julewire/core/testing/chaos/catalog.rb +0 -72
  123. data/lib/julewire/core/testing/chaos/core_runtime.rb +0 -120
  124. data/lib/julewire/core/testing/chaos/destination.rb +0 -55
  125. data/lib/julewire/core/testing/chaos/emitter.rb +0 -20
  126. data/lib/julewire/core/testing/chaos/raising_output.rb +0 -42
  127. data/lib/julewire/core/testing/chaos.rb +0 -80
  128. data/lib/julewire/core/testing/contracts/component.rb +0 -162
  129. data/lib/julewire/core/testing/contracts/deadline_scheduler.rb +0 -59
  130. data/lib/julewire/core/testing/contracts/integration.rb +0 -166
  131. data/lib/julewire/core/testing/contracts/integration_fields.rb +0 -36
  132. data/lib/julewire/core/testing/contracts/record_draft.rb +0 -37
  133. data/lib/julewire/core/testing/contracts/runtime.rb +0 -178
  134. data/lib/julewire/core/testing/contracts/wire.rb +0 -60
  135. data/lib/julewire/core/testing/contracts.rb +0 -24
  136. data/lib/julewire/core/testing/coverage.rb +0 -58
  137. 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
@@ -85,26 +82,24 @@ module Julewire
85
82
  current_field_stack(:attributes).value_for(key, default: default)
86
83
  end
87
84
 
88
- def add_context(fields = EMPTY_HASH, owned: false, **keyword_fields)
85
+ def add_context(fields = nil, owned: false, **keyword_fields)
89
86
  add_field(:context, field_input(fields, keyword_fields), owned: owned)
90
87
  end
91
88
 
92
- def add_carry(fields = EMPTY_HASH, owned: false, **keyword_fields)
89
+ def add_carry(fields = nil, owned: false, **keyword_fields)
93
90
  add_field(:carry, field_input(fields, keyword_fields), owned: owned)
94
91
  end
95
92
 
96
- def add_attributes(fields = EMPTY_HASH, owned: false, **keyword_fields)
93
+ def add_attributes(fields = nil, owned: false, **keyword_fields)
97
94
  add_field(:attributes, field_input(fields, keyword_fields), owned: owned)
98
95
  end
99
96
 
100
- def add_neutral(fields = EMPTY_HASH, owned: false, **keyword_fields)
97
+ def add_neutral(fields = nil, owned: false, **keyword_fields)
101
98
  add_field(:neutral, field_input(fields, keyword_fields), owned: owned)
102
99
  end
103
100
 
104
101
  def delete_carry(path)
105
102
  path = Fields::Internal.normalize_path(path)
106
- return if path.empty?
107
-
108
103
  if current_scope
109
104
  current_scope.delete_carry(path)
110
105
  else
@@ -112,19 +107,19 @@ module Julewire
112
107
  end
113
108
  end
114
109
 
115
- def with_context(fields = EMPTY_HASH, owned: false, **keyword_fields, &)
110
+ def with_context(fields = nil, owned: false, **keyword_fields, &)
116
111
  with_scope_or_ambient_overlay(:context, field_input(fields, keyword_fields), owned: owned, &)
117
112
  end
118
113
 
119
- def with_carry(fields = EMPTY_HASH, owned: false, **keyword_fields, &)
114
+ def with_carry(fields = nil, owned: false, **keyword_fields, &)
120
115
  with_scope_or_ambient_overlay(:carry, field_input(fields, keyword_fields), owned: owned, &)
121
116
  end
122
117
 
123
- def with_attributes(fields = EMPTY_HASH, owned: false, **keyword_fields, &)
118
+ def with_attributes(fields = nil, owned: false, **keyword_fields, &)
124
119
  with_scope_or_ambient_overlay(:attributes, field_input(fields, keyword_fields), owned: owned, &)
125
120
  end
126
121
 
127
- def with_neutral(fields = EMPTY_HASH, owned: false, **keyword_fields, &)
122
+ def with_neutral(fields = nil, owned: false, **keyword_fields, &)
128
123
  with_scope_or_ambient_overlay(:neutral, field_input(fields, keyword_fields), owned: owned, &)
129
124
  end
130
125
 
@@ -140,30 +135,21 @@ module Julewire
140
135
  end
141
136
  end
142
137
 
143
- def with_propagation(context: {}, carry: {}, execution: {}, link_executions: false, owned: false, &)
144
- scope = current_scope
138
+ def with_propagation(context:, carry:, execution:, link_executions:, owned:, &)
145
139
  execution = if owned
146
- Fields::FieldSet.deep_symbolize_owned_keys(execution)
140
+ Serialization::DeepFreeze.validate_symbol_hash(execution)
147
141
  else
148
142
  Fields::FieldSet.deep_symbolize_keys(execution)
149
143
  end
150
- @execution_overlays.push(execution)
151
- @execution_lineage_overlays.push(link_executions ? Execution::Lineage.from_execution_hash(execution) : nil)
144
+ @execution_overlays.push(PropagationOverlay.new(execution: execution, link_executions: link_executions))
152
145
  invalidate_propagation_cache!
153
146
 
154
147
  begin
155
- if scope
156
- scope.with_carry(carry, owned: owned) do
157
- scope.with_context(context, owned: owned, &)
158
- end
159
- else
160
- @ambient_fields.with(:carry, carry, owned: owned) do
161
- @ambient_fields.with(:context, context, owned: owned, &)
162
- end
148
+ with_carry(carry, owned: owned) do
149
+ with_context(context, owned: owned, &)
163
150
  end
164
151
  ensure
165
152
  @execution_overlays.pop
166
- @execution_lineage_overlays.pop
167
153
  invalidate_propagation_cache!
168
154
  end
169
155
  end
@@ -214,12 +200,12 @@ module Julewire
214
200
  Execution::Scope.new(
215
201
  type: options.fetch(:type),
216
202
  id: options[:id],
217
- execution: merged_execution_hash(options.fetch(:execution, EMPTY_HASH)),
203
+ execution: merged_execution_hash(options.fetch(:execution, {}), owned: options.fetch(:owned, false)),
218
204
  execution_owned: true,
219
205
  context: fields.stack(:context),
220
206
  attributes: fields.stack(:attributes),
221
207
  neutral: fields.stack(:neutral),
222
- labels: options.fetch(:labels, EMPTY_HASH),
208
+ labels: options[:labels],
223
209
  carry: fields.stack(:carry),
224
210
  parent: parent_scope || linked_propagation_scope_snapshot,
225
211
  started_at: options[:started_at],
@@ -238,7 +224,7 @@ module Julewire
238
224
  end
239
225
 
240
226
  def add_scope_stack(stack_set, options, section:, key:)
241
- value = options.fetch(key, EMPTY_HASH)
227
+ value = options[key]
242
228
  if options.fetch(:owned, false)
243
229
  stack_set.add(section, value, owned: true)
244
230
  else
@@ -246,37 +232,26 @@ module Julewire
246
232
  end
247
233
  end
248
234
 
249
- def add_field(section, fields, owned: false)
235
+ def add_field(section, fields, owned:)
250
236
  scope = current_scope
251
237
  if scope
252
238
  scope.add_field(section, fields, owned: owned)
253
- elsif owned
254
- @ambient_fields.add(section, fields, owned: true)
255
239
  else
256
- @ambient_fields.add(section, fields)
240
+ @ambient_fields.add(section, fields, owned: owned)
257
241
  end
258
242
  end
259
243
 
260
244
  def field_input(fields, keyword_fields)
261
245
  return fields if keyword_fields.empty?
262
- return keyword_fields if empty_field_input?(fields)
263
-
264
- fields.is_a?(Hash) ? fields.merge(keyword_fields) : fields
265
- end
266
246
 
267
- def empty_field_input?(fields)
268
- fields.nil? || (fields.respond_to?(:empty?) && fields.empty?)
247
+ Fields::FieldSet.coerce(fields, keyword_fields, invalid: :wrap)
269
248
  end
270
249
 
271
- def with_scope_or_ambient_overlay(section, fields, owned: false, &)
250
+ def with_scope_or_ambient_overlay(section, fields, owned:, &)
272
251
  scope = current_scope
273
252
  return scope.with_field(section, fields, owned: owned, &) if scope
274
253
 
275
- if owned
276
- @ambient_fields.with(section, fields, owned: true, &)
277
- else
278
- @ambient_fields.with(section, fields, &)
279
- end
254
+ @ambient_fields.with(section, fields, owned: owned, &)
280
255
  end
281
256
 
282
257
  def current_field_stack(section)
@@ -297,32 +272,31 @@ module Julewire
297
272
  end
298
273
 
299
274
  def execution_hash
300
- return {} if @execution_overlays.empty?
301
-
302
- Fields::FieldSet.deep_dup(propagation_execution_hash)
275
+ Fields::FieldSet.deep_dup_owned(propagation_execution_hash)
303
276
  end
304
277
 
305
278
  def propagation_execution_hash
306
- @propagation_execution_hash ||= Fields::Internal.frozen_copy(@execution_overlays.reduce({}) do |memo, overlay|
307
- Fields::FieldSet.merge!(memo, overlay)
308
- 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
+ )
309
284
  end
310
285
 
311
286
  def propagation_scope_snapshot
312
287
  execution = propagation_execution_hash
313
288
  return if execution.empty?
314
289
 
315
- @propagation_scope_snapshot ||= Execution::ScopeSnapshot.new(execution: execution)
290
+ @propagation_scope_snapshot ||= Execution::ScopeSnapshot.new(execution: execution, owned: true)
316
291
  end
317
292
 
318
293
  def linked_propagation_scope_snapshot
319
- lineage = linked_propagation_lineage
320
- return unless lineage
294
+ return unless linked_propagation?
321
295
 
322
- execution = propagation_execution_hash
323
- return if execution.empty?
324
-
325
- @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
+ )
326
300
  end
327
301
 
328
302
  def invalidate_propagation_cache!
@@ -331,56 +305,46 @@ module Julewire
331
305
  @linked_propagation_scope_snapshot = nil
332
306
  end
333
307
 
334
- def linked_propagation_lineage
335
- (@execution_overlays.length - 1).downto(0) do |index|
336
- execution = @execution_overlays.fetch(index)
337
- next if execution.empty?
338
-
339
- return @execution_lineage_overlays.fetch(index)
340
- end
341
- nil
308
+ def linked_propagation?
309
+ @execution_overlays.reverse_each.find { |overlay| !overlay.execution.empty? }&.link_executions
342
310
  end
343
311
 
344
- def merged_execution_hash(execution)
345
- inherited = inherited_execution_hash
346
- 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
347
314
 
348
- Fields::FieldSet.merge!(inherited, execution)
315
+ Serialization::DeepFreeze.validate_symbol_hash(execution)
316
+ Fields::Internal.merge_owned!(inherited_execution_hash, execution)
349
317
  end
350
318
 
351
319
  def inherited_execution_hash
352
320
  scope = current_scope
353
321
  return execution_hash unless scope
354
322
 
355
- inherited = scope.inheritable_execution_hash
356
- return inherited if @execution_overlays.empty?
357
-
358
- overlay = execution_hash
359
- return inherited if overlay.empty?
360
-
361
- Fields::FieldSet.merge!(inherited, overlay)
323
+ Fields::Internal.merge_owned!(scope.inheritable_execution_hash, execution_hash)
362
324
  end
363
325
 
364
326
  def finish_scope(scope, on_finish, on_finish_failure, active_exception: nil)
365
327
  return unless on_finish
366
328
 
367
- contain_finish_failure(on_finish_failure, active_exception) { scope.finish_owned unless scope.finished? }
368
- 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) }
369
333
  end
370
334
 
371
- def contain_finish_failure(on_finish_failure, active_exception)
335
+ def contain_finish_failure(on_finish_failure, active_exception, phase:)
372
336
  yield
373
337
  rescue StandardError => e
374
- report_finish_failure(on_finish_failure, e)
338
+ report_finish_failure(on_finish_failure, e, phase: phase)
375
339
  rescue SystemStackError => e
376
340
  # Preserve the app's active stack error during unwind.
377
341
  raise unless active_exception
378
342
 
379
- report_finish_failure(on_finish_failure, e)
343
+ report_finish_failure(on_finish_failure, e, phase: phase)
380
344
  end
381
345
 
382
- def report_finish_failure(on_finish_failure, error)
383
- on_finish_failure&.call(error)
346
+ def report_finish_failure(on_finish_failure, error, phase:)
347
+ on_finish_failure.call(error, phase: phase)
384
348
  rescue StandardError
385
349
  nil
386
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,48 +188,6 @@ 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)
@@ -244,8 +195,9 @@ module Julewire
244
195
 
245
196
  def call_output_lifecycle_safely(method_name, timeout)
246
197
  # Sink.wrap centralizes timeout-aware lifecycle dispatch for every output.
198
+ degradation_marker = @health.degradation_marker
247
199
  result = @output.public_send(method_name, timeout: timeout)
248
- clear_degradation if method_name == :flush && result != false
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
@@ -9,7 +9,6 @@ module Julewire
9
9
  reject_output_array!(output)
10
10
  return output if wrapped?(output)
11
11
 
12
- validate_writeable!(output)
13
12
  SynchronizedOutput.new(output, close_output: close_output)
14
13
  end
15
14