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
@@ -29,7 +29,7 @@ module Julewire
29
29
  end
30
30
 
31
31
  def each
32
- return enum_for(:each) unless block_given?
32
+ return enum_for unless block_given?
33
33
 
34
34
  @record.each do |key, value|
35
35
  next if INTERNAL_KEYS.include?(key)
@@ -41,7 +41,7 @@ module Julewire
41
41
  private
42
42
 
43
43
  def output_value(key, value)
44
- return self.class.public_execution(value) if key == :execution && value.is_a?(Hash)
44
+ return self.class.public_execution(value) if key == :execution
45
45
 
46
46
  value
47
47
  end
@@ -26,7 +26,7 @@ module Julewire
26
26
  def value(input, key, default: nil)
27
27
  return default unless hash_input?(input)
28
28
  return input[key] if input.key?(key)
29
- return input[key.to_s] if input.key?(key.to_s)
29
+ return input.fetch(key.to_s) if input.key?(key.to_s)
30
30
 
31
31
  default
32
32
  end
@@ -29,11 +29,14 @@ module Julewire
29
29
  end
30
30
 
31
31
  def from_owned_hash(record, lineage: nil, trust_frozen: false)
32
- validate_normalized_hash!(record)
32
+ validate_normalized_hash_structure!(record)
33
33
  lineage ||= Execution::Lineage.from_execution_hash(record.fetch(:execution))
34
34
  execution = Execution::Lineage.clean_normalized_lazy_relationship_hash(record.fetch(:execution))
35
35
  record = record.frozen? ? record.merge(execution: execution) : replace_execution(record, execution)
36
- new(Serialization::DeepFreeze.call(record, trust_frozen: trust_frozen), lineage: lineage)
36
+ new(
37
+ Serialization::DeepFreeze.call(record, trust_frozen: trust_frozen, validate_symbol_keys: true),
38
+ lineage: lineage
39
+ )
37
40
  end
38
41
 
39
42
  def validate_normalized!(record)
@@ -54,11 +57,22 @@ module Julewire
54
57
  record
55
58
  end
56
59
 
57
- def validate_hash!(record)
58
- raise TypeError, "record must be a normalized Hash" unless record.is_a?(Hash)
60
+ def validate_normalized_hash_structure!(record)
61
+ validate_record_hash!(record)
62
+ validate_structure!(record)
63
+ end
59
64
 
65
+ def validate_hash!(record)
66
+ validate_record_hash!(record)
60
67
  validate_symbol_keys!(record)
68
+ validate_structure!(record)
69
+ end
61
70
 
71
+ def validate_record_hash!(record)
72
+ raise TypeError, "record must be a normalized Hash" unless record.is_a?(Hash)
73
+ end
74
+
75
+ def validate_structure!(record)
62
76
  validate_required_keys!(record)
63
77
  validate_known_keys!(record)
64
78
 
@@ -69,39 +83,7 @@ module Julewire
69
83
  end
70
84
 
71
85
  def validate_symbol_keys!(record)
72
- validate_value_symbol_keys!(record)
73
- end
74
-
75
- def validate_value_symbol_keys!(root)
76
- queue = [[root, 0]]
77
- seen = {}.compare_by_identity
78
-
79
- queue.each do |value, depth|
80
- break if depth == NORMALIZATION_MAX_DEPTH
81
- next unless mark_symbol_key_container(value, seen)
82
-
83
- enqueue_symbol_key_children(queue, value, depth + 1)
84
- end
85
- end
86
-
87
- def mark_symbol_key_container(value, seen)
88
- return unless value.is_a?(Hash) || value.is_a?(Array)
89
- return if seen.key?(value)
90
-
91
- seen[value] = nil
92
- value
93
- end
94
-
95
- def enqueue_symbol_key_children(queue, value, depth)
96
- if value.is_a?(Hash)
97
- value.each do |key, item|
98
- raise TypeError, "record must not use string keys" if key.is_a?(String)
99
-
100
- queue << [item, depth]
101
- end
102
- else
103
- value.each { queue << [it, depth] }
104
- end
86
+ Serialization::DeepFreeze.validate_symbol_keys(record)
105
87
  end
106
88
 
107
89
  def validate_required_keys!(record)
@@ -12,29 +12,21 @@ module Julewire
12
12
 
13
13
  class << self
14
14
  def normalize(value)
15
- return value if RANKS.key?(value)
16
-
17
- severity = severity_symbol(value)
18
- return severity if RANKS.key?(severity)
19
-
20
- raise ArgumentError, "unsupported severity: #{value.inspect}"
21
- end
22
-
23
- def severity_symbol(value)
24
15
  case value
25
16
  when Symbol
26
- value.downcase
17
+ return value if RANKS.key?(value)
27
18
  when String
28
- STRING_VALUES[value.downcase]
19
+ severity = STRING_VALUES[value.downcase]
20
+ return severity unless severity.nil?
29
21
  when Integer
30
- LOGGER_INTEGER_VALUES[value]
22
+ severity = LOGGER_INTEGER_VALUES[value]
23
+ return severity unless severity.nil?
31
24
  end
25
+
26
+ raise ArgumentError, "unsupported severity: #{value.inspect}"
32
27
  end
33
28
 
34
29
  def rank(value)
35
- rank = RANKS[value]
36
- return rank unless rank.nil?
37
-
38
30
  RANKS.fetch(normalize(value))
39
31
  end
40
32
  end
@@ -17,7 +17,7 @@ module Julewire
17
17
  runtime_callback_failures
18
18
  runtime_failures
19
19
  ].freeze
20
- CloseTransition = Data.define(:state, :close_pipeline, :timeout)
20
+ CloseTransition = Data.define(:state, :timeout)
21
21
  PipelineReplacement = Data.define(
22
22
  :old_pipeline,
23
23
  :close_timeout,
@@ -29,15 +29,12 @@ module Julewire
29
29
 
30
30
  def initialize
31
31
  @configure_mutex = Mutex.new
32
- @configure_generation = Concurrent::AtomicFixnum.new(0)
33
- @state_mutex = Mutex.new
34
- @post_close_emit_count = Concurrent::AtomicFixnum.new(0)
32
+ @configure_generation = Concurrent::AtomicFixnum.new
33
+ @post_close_emit_count = Concurrent::AtomicFixnum.new
35
34
  @runtime_health = build_runtime_health
36
35
  @integration_health = Diagnostics::IntegrationHealthStore.new
37
36
  @invalid_severity_reporter = Diagnostics::InvalidSeverityReporter.counter
38
- @state_ref = Concurrent::AtomicReference.new(
39
- RuntimeState.default(invalid_severity_reporter: @invalid_severity_reporter)
40
- )
37
+ @state_ref = Concurrent::AtomicReference.new(RuntimeState.default)
41
38
  @execution_boundary = build_execution_boundary
42
39
  end
43
40
 
@@ -66,11 +63,11 @@ module Julewire
66
63
 
67
64
  def start_execution(...) = @execution_boundary.start_execution(...)
68
65
 
69
- def emit(record = Core::UNSET, **fields, &)
66
+ def emit(record = UNSET, **fields, &)
70
67
  emit_with_level_check(record, true, fields, &)
71
68
  end
72
69
 
73
- def emit_without_level(record = Core::UNSET, **fields, &)
70
+ def emit_without_level(record = UNSET, **fields, &)
74
71
  emit_with_level_check(record, false, fields, &)
75
72
  end
76
73
 
@@ -80,8 +77,7 @@ module Julewire
80
77
  end
81
78
  end
82
79
 
83
- def emit_envelope(input:, context:, scope:, carry: {}, attributes: {}, neutral: {}, enforce_level: true,
84
- owned: false)
80
+ def emit_envelope(input:, context:, scope:, carry:, attributes:, neutral:, enforce_level: true, owned: false)
85
81
  reject_runtime_call_during_configure!(:emit_envelope)
86
82
  state = runtime_state
87
83
  return record_post_close_emit(state) if state.pipeline_closed
@@ -102,6 +98,7 @@ module Julewire
102
98
 
103
99
  begin
104
100
  input = scope.owned_summary_record_input
101
+ Serialization::DeepFreeze.validate_symbol_keys(input)
105
102
  state.pipeline.emit_isolated_input(input)
106
103
  rescue StandardError => e
107
104
  notify_failure(e, state, action: :emit_summary_record)
@@ -127,20 +124,18 @@ module Julewire
127
124
  config
128
125
  end
129
126
 
130
- def flush(timeout: Core::UNSET)
127
+ def flush(timeout: UNSET)
131
128
  call_validated_lifecycle(:flush, timeout)
132
129
  end
133
130
 
134
- def close(timeout: Core::UNSET)
131
+ def close(timeout: UNSET)
135
132
  close_state_resources(close_state(timeout))
136
133
  end
137
134
 
138
135
  def reset!
139
136
  increment_runtime_count(:reset_attempts)
140
137
  reset_result = reject_runtime_call_during_configure!(:reset!) do
141
- @configure_mutex.synchronize do
142
- @state_mutex.synchronize { reset_under_lock }
143
- end
138
+ @configure_mutex.synchronize { reset_under_lock }
144
139
  end
145
140
  deadline = Scheduling::Deadline.for(reset_result.close_timeout)
146
141
  return unless reset_result.close_pipeline
@@ -153,6 +148,8 @@ module Julewire
153
148
  )
154
149
  end
155
150
 
151
+ def reset_facade! = RuntimeRegistry.reset(primary: self)
152
+
156
153
  def after_fork!
157
154
  reject_runtime_call_during_configure!(:after_fork!)
158
155
  RuntimeRegistry.reset_after_fork(primary: self)
@@ -161,7 +158,6 @@ module Julewire
161
158
  def reset_after_fork_runtime!
162
159
  reset_after_fork_state!
163
160
  runtime_state.pipeline.after_fork!
164
- self
165
161
  end
166
162
 
167
163
  def record_integration_failure(integration, error, **metadata)
@@ -196,10 +192,10 @@ module Julewire
196
192
  if owned
197
193
  Records::Draft.build_pipeline_owned(
198
194
  input,
199
- context: envelope_hash(context),
200
- attributes: envelope_hash(attributes),
201
- neutral: envelope_hash(neutral),
202
- carry: envelope_hash(carry),
195
+ context: envelope_hash(context, owned: true),
196
+ attributes: envelope_hash(attributes, owned: true),
197
+ neutral: envelope_hash(neutral, owned: true),
198
+ carry: envelope_hash(carry, owned: true),
203
199
  scope: scope,
204
200
  error_backtrace_lines: state.configuration.error_backtrace_lines,
205
201
  input_owned: true,
@@ -208,10 +204,10 @@ module Julewire
208
204
  else
209
205
  Records::Draft.build(
210
206
  input,
211
- context: envelope_hash(context),
212
- attributes: envelope_hash(attributes),
213
- neutral: envelope_hash(neutral),
214
- carry: envelope_hash(carry),
207
+ context: envelope_hash(context, owned: false),
208
+ attributes: envelope_hash(attributes, owned: false),
209
+ neutral: envelope_hash(neutral, owned: false),
210
+ carry: envelope_hash(carry, owned: false),
215
211
  scope: scope,
216
212
  error_backtrace_lines: state.configuration.error_backtrace_lines,
217
213
  invalid_severity_reporter: @invalid_severity_reporter
@@ -244,11 +240,7 @@ module Julewire
244
240
  end
245
241
 
246
242
  def install_and_replace_pipeline(state, next_configuration, next_pipeline)
247
- replaced_pipeline = @state_mutex.synchronize do
248
- raise Error, "Julewire.configure state changed before install completed" unless runtime_state.equal?(state)
249
-
250
- replace_pipeline(next_configuration, next_pipeline)
251
- end
243
+ replaced_pipeline = replace_pipeline(next_configuration, next_pipeline, state: state)
252
244
  PipelineReplacement.new(
253
245
  replaced_pipeline,
254
246
  state.configuration.pipeline_close_timeout,
@@ -270,11 +262,14 @@ module Julewire
270
262
  Fiber[CONFIGURE_GUARD_KEY] = previous
271
263
  end
272
264
 
273
- def replace_pipeline(configuration, pipeline)
274
- state = runtime_state
265
+ def replace_pipeline(configuration, pipeline, state:)
266
+ next_state = state.next_generation(configuration: configuration, pipeline: pipeline)
267
+ unless @state_ref.compare_and_set(state, next_state)
268
+ raise Error, "Julewire.configure state changed before install completed"
269
+ end
270
+
275
271
  @post_close_emit_count.value = 0
276
- @runtime_health.clear_degradation
277
- @state_ref.set(state.next_generation(configuration: configuration, pipeline: pipeline))
272
+ @runtime_health.record_success
278
273
  state.pipeline
279
274
  end
280
275
 
@@ -293,7 +288,7 @@ module Julewire
293
288
  end
294
289
 
295
290
  def normalize_lifecycle_timeout(timeout, state)
296
- timeout.equal?(Core::UNSET) ? state.configuration.pipeline_close_timeout : timeout
291
+ timeout.equal?(UNSET) ? state.configuration.pipeline_close_timeout : timeout
297
292
  end
298
293
 
299
294
  def validate_lifecycle_timeout!(timeout, name:)
@@ -301,7 +296,7 @@ module Julewire
301
296
  end
302
297
 
303
298
  def close_state_resources(transition)
304
- return true unless transition.close_pipeline
299
+ return true unless transition
305
300
 
306
301
  deadline = Scheduling::Deadline.for(transition.timeout)
307
302
  call_pipeline_lifecycle_on(
@@ -314,17 +309,22 @@ module Julewire
314
309
 
315
310
  def close_state(timeout)
316
311
  reject_runtime_call_during_configure!(:close)
317
- @state_mutex.synchronize do
318
- state = runtime_state
319
- timeout = normalize_lifecycle_timeout(timeout, state)
320
- validate_lifecycle_timeout!(timeout, name: :timeout)
321
- increment_runtime_count(:close_attempts)
322
- close_pipeline = !state.pipeline_closed
323
- return CloseTransition.new(state, false, timeout) unless close_pipeline
324
-
325
- @state_ref.set(state.closed)
326
- CloseTransition.new(state, close_pipeline, timeout)
312
+ state = runtime_state
313
+ resolved_timeout = normalize_lifecycle_timeout(timeout, state)
314
+ validate_lifecycle_timeout!(resolved_timeout, name: :timeout)
315
+ increment_runtime_count(:close_attempts)
316
+ transition = nil
317
+
318
+ @state_ref.update do |current_state|
319
+ transition = unless current_state.pipeline_closed
320
+ CloseTransition.new(
321
+ current_state,
322
+ normalize_lifecycle_timeout(timeout, current_state)
323
+ )
324
+ end
325
+ current_state.closed
327
326
  end
327
+ transition
328
328
  end
329
329
 
330
330
  def call_pipeline_lifecycle_on(pipeline, method_name, timeout:, state:)
@@ -360,8 +360,8 @@ module Julewire
360
360
  end
361
361
  end
362
362
 
363
- def envelope_hash(value)
364
- value.is_a?(Hash) ? value : {}
363
+ def envelope_hash(value, owned:)
364
+ value if owned || value.is_a?(Hash)
365
365
  end
366
366
 
367
367
  def record_post_close_emit(state)
@@ -389,48 +389,48 @@ module Julewire
389
389
  end
390
390
 
391
391
  def pipeline_degraded?(pipeline_health)
392
- return true if pipeline_health[:status] && pipeline_health[:status] != :ok
392
+ return true if degraded_health?(pipeline_health)
393
393
 
394
394
  pipeline_health.fetch(:destinations).values.any? do |destination_health|
395
- destination_health[:status] && destination_health[:status] != :ok
395
+ degraded_health?(destination_health)
396
396
  end
397
397
  end
398
398
 
399
399
  def integrations_degraded?(integrations)
400
400
  integrations.values.any? do |integration_health|
401
- integration_health[:status] && integration_health[:status] != :ok
401
+ degraded_health?(integration_health)
402
402
  end
403
403
  end
404
404
 
405
+ def degraded_health?(health)
406
+ status = health[:status]
407
+ status && !status.equal?(:ok)
408
+ end
409
+
405
410
  def clear_runtime_degradation_if_unchanged(marker)
406
411
  @runtime_health.clear_degradation_if_unchanged(marker)
407
412
  end
408
413
 
409
414
  def summary_finalizer_failure
410
- @summary_finalizer_failure ||= ->(error) { handle_summary_finalizer_failure(error) }
415
+ @summary_finalizer_failure ||= ->(error, **metadata) { handle_summary_finalizer_failure(error, **metadata) }
411
416
  end
412
417
 
413
418
  def reset_after_fork_state!
414
- state = runtime_state
415
- @configure_mutex = Mutex.new
416
- @configure_generation = Concurrent::AtomicFixnum.new(0)
417
- @state_mutex = Mutex.new
418
- @post_close_emit_count = Concurrent::AtomicFixnum.new(0)
419
+ @post_close_emit_count.value = 0
419
420
  @runtime_health = build_runtime_health
420
- @integration_health.after_fork!
421
- @invalid_severity_reporter.reset_after_fork!
422
- @state_ref = Concurrent::AtomicReference.new(state)
423
- nil
421
+ @integration_health.reset!
422
+ @invalid_severity_reporter.reset!
424
423
  end
425
424
 
426
425
  def reset_under_lock
427
426
  state = runtime_state
428
427
  configuration = Configuration.new.snapshot
429
428
  @invalid_severity_reporter.reset!
430
- next_pipeline = configuration.build_pipeline(invalid_severity_reporter: @invalid_severity_reporter)
431
- replace_pipeline(configuration, next_pipeline)
432
- ContextStore.reset_current!
429
+ next_pipeline = configuration.build_pipeline
430
+ next_state = state.next_generation(configuration: configuration, pipeline: next_pipeline)
431
+ state = @state_ref.get_and_set(next_state)
433
432
  @post_close_emit_count.value = 0
433
+ ContextStore.reset_current!
434
434
  @runtime_health.clear_failures!
435
435
  @integration_health.reset!
436
436
  Diagnostics::ProcessIntegrationHealth.reset!
@@ -448,12 +448,12 @@ module Julewire
448
448
  raise Error, "Julewire.#{method_name} cannot be called from inside Julewire.configure"
449
449
  end
450
450
 
451
- block_given? ? yield : nil
451
+ yield if block_given?
452
452
  end
453
453
 
454
454
  def configure_guard_active?
455
455
  token = Fiber[CONFIGURE_GUARD_KEY]
456
- token.is_a?(Array) && token.fetch(0) == object_id && token.fetch(1) == @configure_generation.value
456
+ token.instance_of?(Array) && token.fetch(0) == object_id && token.fetch(1) == @configure_generation.value
457
457
  end
458
458
 
459
459
  def increment_lifecycle_attempt(method_name)
@@ -476,9 +476,8 @@ module Julewire
476
476
  @runtime_health.record_failure(error, callback: state.configuration.on_failure, **metadata)
477
477
  end
478
478
 
479
- def handle_summary_finalizer_failure(error)
479
+ def handle_summary_finalizer_failure(error, **metadata)
480
480
  state = runtime_state
481
- metadata = { phase: :summary_finalizer }
482
481
  @runtime_health.record_failure(error, callback: state.configuration.on_failure, **metadata)
483
482
  end
484
483
 
@@ -503,8 +502,6 @@ module Julewire
503
502
  end
504
503
 
505
504
  def notify_lifecycle_warning(warning)
506
- return unless warning
507
-
508
505
  result = Diagnostics::CallbackNotifier.call(warning.fetch(:on_failure), warning.fetch(:error),
509
506
  warning.fetch(:metadata))
510
507
  @runtime_health.record_callback_failure(result) if Diagnostics::CallbackNotifier.failure?(result)
@@ -513,7 +510,6 @@ module Julewire
513
510
  def build_runtime_health
514
511
  Diagnostics::Health.new(
515
512
  counter_keys: RUNTIME_COUNTER_KEYS,
516
- callback_metadata: {},
517
513
  callback_failure_counter: :runtime_callback_failures,
518
514
  failure_counter: :runtime_failures
519
515
  )
@@ -1,13 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "concurrent/atomic/atomic_reference"
4
+
3
5
  module Julewire
4
6
  module Core
5
7
  module RuntimeRegistry
6
8
  DEFAULT_NAME = :default
7
- private_constant :DEFAULT_NAME
9
+ EMPTY_RUNTIMES = {}.freeze
10
+ private_constant :DEFAULT_NAME, :EMPTY_RUNTIMES
8
11
 
9
- @mutex = Mutex.new
10
- @runtimes = {}
12
+ @runtimes = Concurrent::AtomicReference.new(EMPTY_RUNTIMES)
11
13
 
12
14
  class << self
13
15
  def fetch(name, current: RuntimeLocator.current)
@@ -18,29 +20,32 @@ module Julewire
18
20
  raise Error, "named Julewire runtimes are not available from the current runtime"
19
21
  end
20
22
 
21
- @mutex.synchronize { @runtimes[name] ||= Runtime.new }
23
+ @runtimes.update do |runtimes|
24
+ if runtimes.key?(name)
25
+ runtimes
26
+ else
27
+ runtimes.merge(name => Runtime.new)
28
+ end
29
+ end.fetch(name)
22
30
  end
23
31
 
24
- def clear!
25
- @mutex.synchronize { @runtimes.clear }
32
+ def reset(primary:)
33
+ runtimes = @runtimes.get_and_set(EMPTY_RUNTIMES).values
34
+
35
+ primary.reset!
36
+ runtimes.each(&:reset!)
26
37
  nil
27
38
  end
28
39
 
29
40
  def reset_after_fork(primary:)
30
- # Post-fork execution is single-threaded; do not touch inherited locks
31
- # before rebuilding them.
32
- runtimes = ([primary] + @runtimes.values).uniq
33
- @mutex = Mutex.new
41
+ runtimes = [primary] + @runtimes.get.values
34
42
 
35
- LocalStorage.after_fork!
36
43
  ContextStore.reset_current!
37
44
  Scheduling::SharedScheduler.after_fork!
38
- Diagnostics::ProcessIntegrationHealth.after_fork!
39
- Integration::ForkHooks.after_fork!
40
- Diagnostics::InvalidSeverityReporter.reset_after_fork!
45
+ Diagnostics::ProcessIntegrationHealth.reset!
46
+ Diagnostics::InvalidSeverityReporter.reset!
41
47
  runtimes.each(&:reset_after_fork_runtime!)
42
48
  Integration::ForkHooks.run
43
- nil
44
49
  end
45
50
  end
46
51
  end
@@ -7,11 +7,13 @@ module Julewire
7
7
  :pipeline,
8
8
  :pipeline_closed,
9
9
  :pipeline_generation
10
- ) do
10
+ )
11
+
12
+ class RuntimeState
11
13
  class << self
12
- def default(invalid_severity_reporter: Diagnostics::InvalidSeverityReporter.counter)
14
+ def default
13
15
  configuration = Configuration.new.snapshot
14
- pipeline = configuration.build_pipeline(invalid_severity_reporter: invalid_severity_reporter)
16
+ pipeline = configuration.build_pipeline
15
17
 
16
18
  new(
17
19
  configuration: configuration,
@@ -1,47 +1,53 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "concurrent/executor/thread_pool_executor"
4
+ require "concurrent/executor/timer_set"
5
+
3
6
  module Julewire
4
7
  module Core
5
8
  module Scheduling
6
9
  module SharedScheduler
7
10
  THREAD_NAME = "julewire-deadline-scheduler"
8
-
9
- @mutex = Mutex.new
11
+ EXECUTOR_OPTIONS = { max_threads: 1, idletime: 0 }.freeze
12
+ private_constant :EXECUTOR_OPTIONS
10
13
 
11
14
  class << self
12
- def schedule(timeout, &)
13
- current = scheduler
14
- current.schedule(timeout, &)
15
- end
15
+ def schedule(timeout, &block)
16
+ raise ArgumentError, "block required" unless block
16
17
 
17
- def cancel(token)
18
- current = scheduler
19
- current.cancel(token)
18
+ timeout = Float(timeout)
19
+ if timeout <= 0
20
+ yield
21
+ return
22
+ end
23
+
24
+ @scheduler.post(timeout) do
25
+ Thread.current.name = THREAD_NAME
26
+ yield
27
+ end
20
28
  end
21
29
 
22
- def after_fork!
23
- current = @scheduler
24
- @mutex = Mutex.new
25
- current&.after_fork!
30
+ def cancel(task)
31
+ task&.cancel
26
32
  nil
27
33
  end
28
34
 
29
- # Private testing seam for isolating process-wide scheduler state.
30
- def reset_for_test!
31
- @mutex.synchronize { @scheduler = nil }
35
+ def after_fork!
36
+ previous = @scheduler
37
+ @scheduler = build_scheduler
38
+ previous.kill
32
39
  nil
33
40
  end
34
41
 
35
42
  private
36
43
 
37
- private :reset_for_test!
38
-
39
- def scheduler
40
- @mutex.synchronize do
41
- @scheduler ||= DeadlineScheduler.new(thread_name: THREAD_NAME)
42
- end
44
+ def build_scheduler
45
+ executor = Concurrent::ThreadPoolExecutor.new(**EXECUTOR_OPTIONS)
46
+ Concurrent::TimerSet.new(executor: executor)
43
47
  end
44
48
  end
49
+
50
+ @scheduler = build_scheduler
45
51
  end
46
52
  end
47
53
  end
@@ -4,6 +4,9 @@ module Julewire
4
4
  module Core
5
5
  module Serialization
6
6
  class BacktraceLimiter
7
+ MAX_CAUSE_DEPTH = Core::NORMALIZATION_MAX_DEPTH
8
+ private_constant :MAX_CAUSE_DEPTH
9
+
7
10
  class << self
8
11
  def call(value, max_backtrace_lines:)
9
12
  new(max_backtrace_lines: max_backtrace_lines).call(value)
@@ -18,18 +21,18 @@ module Julewire
18
21
  end
19
22
 
20
23
  def call(value)
21
- @seen = {}.compare_by_identity
22
- limit_backtraces(value)
24
+ limit_backtraces(value, Set.new.compare_by_identity)
23
25
  value
24
- ensure
25
- @seen = nil
26
26
  end
27
27
 
28
28
  private
29
29
 
30
- def limit_backtraces(value)
31
- while value.is_a?(Hash) && !@seen.key?(value)
32
- @seen[value] = true
30
+ def limit_backtraces(value, seen)
31
+ MAX_CAUSE_DEPTH.times do
32
+ break unless value.is_a?(Hash)
33
+ break if seen.include?(value)
34
+
35
+ seen.add(value)
33
36
  limit_backtrace_field!(value)
34
37
  value = value[:cause]
35
38
  end
@@ -40,8 +43,9 @@ module Julewire
40
43
 
41
44
  if @max_backtrace_lines.zero?
42
45
  error.delete(:backtrace)
43
- elsif error[:backtrace].is_a?(Array)
44
- error[:backtrace] = error[:backtrace].first(@max_backtrace_lines)
46
+ else
47
+ backtrace = error.fetch(:backtrace)
48
+ error[:backtrace] = backtrace.first(@max_backtrace_lines) if backtrace.is_a?(Array)
45
49
  end
46
50
  end
47
51
  end