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
@@ -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
@@ -19,22 +19,24 @@ module Julewire
19
19
 
20
20
  class << self
21
21
  def from_normalized_hash(record, lineage: nil)
22
- if record.is_a?(Hash)
23
- lineage ||= Execution::Lineage.from_execution_hash(record[:execution])
24
- record = record.merge(execution: Execution::Lineage.clean_lazy_relationship_hash(record[:execution]))
25
- end
26
22
  validate_normalized_hash!(record)
23
+ execution = record.fetch(:execution)
24
+ lineage ||= Execution::Lineage.from_execution_hash(execution)
25
+ record = record.merge(
26
+ execution: Execution::Lineage.clean_normalized_lazy_relationship_hash(execution)
27
+ )
27
28
  new(snapshot_hash(record), lineage: lineage)
28
29
  end
29
30
 
30
31
  def from_owned_hash(record, lineage: nil, trust_frozen: false)
31
- if record.is_a?(Hash)
32
- lineage ||= Execution::Lineage.from_execution_hash(record.fetch(:execution))
33
- execution = Execution::Lineage.clean_lazy_relationship_hash(record.fetch(:execution))
34
- record = record.frozen? ? record.merge(execution: execution) : replace_execution(record, execution)
35
- end
36
- validate_normalized_hash!(record)
37
- new(Serialization::DeepFreeze.call(record, trust_frozen: trust_frozen), lineage: lineage)
32
+ validate_normalized_hash_structure!(record)
33
+ lineage ||= Execution::Lineage.from_execution_hash(record.fetch(:execution))
34
+ execution = Execution::Lineage.clean_normalized_lazy_relationship_hash(record.fetch(:execution))
35
+ record = record.frozen? ? record.merge(execution: execution) : replace_execution(record, execution)
36
+ new(
37
+ Serialization::DeepFreeze.call(record, trust_frozen: trust_frozen, validate_symbol_keys: true),
38
+ lineage: lineage
39
+ )
38
40
  end
39
41
 
40
42
  def validate_normalized!(record)
@@ -55,11 +57,22 @@ module Julewire
55
57
  record
56
58
  end
57
59
 
58
- def validate_hash!(record)
59
- 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
60
64
 
65
+ def validate_hash!(record)
66
+ validate_record_hash!(record)
61
67
  validate_symbol_keys!(record)
68
+ validate_structure!(record)
69
+ end
62
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)
63
76
  validate_required_keys!(record)
64
77
  validate_known_keys!(record)
65
78
 
@@ -70,9 +83,7 @@ module Julewire
70
83
  end
71
84
 
72
85
  def validate_symbol_keys!(record)
73
- record.each_key do |key|
74
- raise TypeError, "record must not use string keys" if key.is_a?(String)
75
- end
86
+ Serialization::DeepFreeze.validate_symbol_keys(record)
76
87
  end
77
88
 
78
89
  def validate_required_keys!(record)
@@ -126,7 +137,8 @@ module Julewire
126
137
  def snapshot_hash(record)
127
138
  Serialization::ValueCopy.call(
128
139
  record,
129
- freeze_values: true
140
+ freeze_values: true,
141
+ preserve_truncation_metadata: true
130
142
  )
131
143
  end
132
144
  end
@@ -149,7 +161,7 @@ module Julewire
149
161
 
150
162
  def each(&) = @data.each(&)
151
163
 
152
- def to_h = Fields::FieldSet.deep_dup(@data)
164
+ def to_h = Fields::FieldSet.deep_dup_owned(@data)
153
165
 
154
166
  # @api internal
155
167
  def serializable_data = @data
@@ -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,22 +77,13 @@ module Julewire
80
77
  end
81
78
  end
82
79
 
83
- def emit_envelope(input:, context:, scope:, carry: {}, attributes: {}, neutral: {}, enforce_level: true)
80
+ def emit_envelope(input:, context:, scope:, carry:, attributes:, neutral:, enforce_level: true, owned: false)
84
81
  reject_runtime_call_during_configure!(:emit_envelope)
85
82
  state = runtime_state
86
83
  return record_post_close_emit(state) if state.pipeline_closed
87
84
 
88
85
  begin
89
- record = Records::Draft.build(
90
- input,
91
- context: envelope_hash(context),
92
- attributes: envelope_hash(attributes),
93
- neutral: envelope_hash(neutral),
94
- carry: envelope_hash(carry),
95
- scope: scope,
96
- error_backtrace_lines: state.configuration.error_backtrace_lines,
97
- invalid_severity_reporter: @invalid_severity_reporter
98
- ).to_record
86
+ record = envelope_draft(input, context, attributes, neutral, carry, scope, state, owned: owned).to_record
99
87
  state.pipeline.emit_record(record, enforce_level: enforce_level)
100
88
  rescue StandardError => e
101
89
  notify_failure(e, state, action: :emit_envelope)
@@ -110,6 +98,7 @@ module Julewire
110
98
 
111
99
  begin
112
100
  input = scope.owned_summary_record_input
101
+ Serialization::DeepFreeze.validate_symbol_keys(input)
113
102
  state.pipeline.emit_isolated_input(input)
114
103
  rescue StandardError => e
115
104
  notify_failure(e, state, action: :emit_summary_record)
@@ -135,20 +124,18 @@ module Julewire
135
124
  config
136
125
  end
137
126
 
138
- def flush(timeout: Core::UNSET)
127
+ def flush(timeout: UNSET)
139
128
  call_validated_lifecycle(:flush, timeout)
140
129
  end
141
130
 
142
- def close(timeout: Core::UNSET)
131
+ def close(timeout: UNSET)
143
132
  close_state_resources(close_state(timeout))
144
133
  end
145
134
 
146
135
  def reset!
147
136
  increment_runtime_count(:reset_attempts)
148
137
  reset_result = reject_runtime_call_during_configure!(:reset!) do
149
- @configure_mutex.synchronize do
150
- @state_mutex.synchronize { reset_under_lock }
151
- end
138
+ @configure_mutex.synchronize { reset_under_lock }
152
139
  end
153
140
  deadline = Scheduling::Deadline.for(reset_result.close_timeout)
154
141
  return unless reset_result.close_pipeline
@@ -161,6 +148,8 @@ module Julewire
161
148
  )
162
149
  end
163
150
 
151
+ def reset_facade! = RuntimeRegistry.reset(primary: self)
152
+
164
153
  def after_fork!
165
154
  reject_runtime_call_during_configure!(:after_fork!)
166
155
  RuntimeRegistry.reset_after_fork(primary: self)
@@ -169,7 +158,6 @@ module Julewire
169
158
  def reset_after_fork_runtime!
170
159
  reset_after_fork_state!
171
160
  runtime_state.pipeline.after_fork!
172
- self
173
161
  end
174
162
 
175
163
  def record_integration_failure(integration, error, **metadata)
@@ -200,6 +188,33 @@ module Julewire
200
188
 
201
189
  private
202
190
 
191
+ def envelope_draft(input, context, attributes, neutral, carry, scope, state, owned:)
192
+ if owned
193
+ Records::Draft.build_pipeline_owned(
194
+ input,
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),
199
+ scope: scope,
200
+ error_backtrace_lines: state.configuration.error_backtrace_lines,
201
+ input_owned: true,
202
+ invalid_severity_reporter: @invalid_severity_reporter
203
+ )
204
+ else
205
+ Records::Draft.build(
206
+ input,
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),
211
+ scope: scope,
212
+ error_backtrace_lines: state.configuration.error_backtrace_lines,
213
+ invalid_severity_reporter: @invalid_severity_reporter
214
+ )
215
+ end
216
+ end
217
+
203
218
  def before_execution_boundary_call!(action)
204
219
  reject_runtime_call_during_configure!(action)
205
220
  end
@@ -225,11 +240,7 @@ module Julewire
225
240
  end
226
241
 
227
242
  def install_and_replace_pipeline(state, next_configuration, next_pipeline)
228
- replaced_pipeline = @state_mutex.synchronize do
229
- raise Error, "Julewire.configure state changed before install completed" unless runtime_state.equal?(state)
230
-
231
- replace_pipeline(next_configuration, next_pipeline)
232
- end
243
+ replaced_pipeline = replace_pipeline(next_configuration, next_pipeline, state: state)
233
244
  PipelineReplacement.new(
234
245
  replaced_pipeline,
235
246
  state.configuration.pipeline_close_timeout,
@@ -251,11 +262,14 @@ module Julewire
251
262
  Fiber[CONFIGURE_GUARD_KEY] = previous
252
263
  end
253
264
 
254
- def replace_pipeline(configuration, pipeline)
255
- 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
+
256
271
  @post_close_emit_count.value = 0
257
- @runtime_health.clear_degradation
258
- @state_ref.set(state.next_generation(configuration: configuration, pipeline: pipeline))
272
+ @runtime_health.record_success
259
273
  state.pipeline
260
274
  end
261
275
 
@@ -274,7 +288,7 @@ module Julewire
274
288
  end
275
289
 
276
290
  def normalize_lifecycle_timeout(timeout, state)
277
- timeout.equal?(Core::UNSET) ? state.configuration.pipeline_close_timeout : timeout
291
+ timeout.equal?(UNSET) ? state.configuration.pipeline_close_timeout : timeout
278
292
  end
279
293
 
280
294
  def validate_lifecycle_timeout!(timeout, name:)
@@ -282,7 +296,7 @@ module Julewire
282
296
  end
283
297
 
284
298
  def close_state_resources(transition)
285
- return true unless transition.close_pipeline
299
+ return true unless transition
286
300
 
287
301
  deadline = Scheduling::Deadline.for(transition.timeout)
288
302
  call_pipeline_lifecycle_on(
@@ -295,17 +309,22 @@ module Julewire
295
309
 
296
310
  def close_state(timeout)
297
311
  reject_runtime_call_during_configure!(:close)
298
- @state_mutex.synchronize do
299
- state = runtime_state
300
- timeout = normalize_lifecycle_timeout(timeout, state)
301
- validate_lifecycle_timeout!(timeout, name: :timeout)
302
- increment_runtime_count(:close_attempts)
303
- close_pipeline = !state.pipeline_closed
304
- return CloseTransition.new(state, false, timeout) unless close_pipeline
305
-
306
- @state_ref.set(state.closed)
307
- 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
308
326
  end
327
+ transition
309
328
  end
310
329
 
311
330
  def call_pipeline_lifecycle_on(pipeline, method_name, timeout:, state:)
@@ -341,8 +360,8 @@ module Julewire
341
360
  end
342
361
  end
343
362
 
344
- def envelope_hash(value)
345
- value.is_a?(Hash) ? value : {}
363
+ def envelope_hash(value, owned:)
364
+ value if owned || value.is_a?(Hash)
346
365
  end
347
366
 
348
367
  def record_post_close_emit(state)
@@ -370,48 +389,48 @@ module Julewire
370
389
  end
371
390
 
372
391
  def pipeline_degraded?(pipeline_health)
373
- return true if pipeline_health[:status] && pipeline_health[:status] != :ok
392
+ return true if degraded_health?(pipeline_health)
374
393
 
375
394
  pipeline_health.fetch(:destinations).values.any? do |destination_health|
376
- destination_health[:status] && destination_health[:status] != :ok
395
+ degraded_health?(destination_health)
377
396
  end
378
397
  end
379
398
 
380
399
  def integrations_degraded?(integrations)
381
400
  integrations.values.any? do |integration_health|
382
- integration_health[:status] && integration_health[:status] != :ok
401
+ degraded_health?(integration_health)
383
402
  end
384
403
  end
385
404
 
405
+ def degraded_health?(health)
406
+ status = health[:status]
407
+ status && !status.equal?(:ok)
408
+ end
409
+
386
410
  def clear_runtime_degradation_if_unchanged(marker)
387
411
  @runtime_health.clear_degradation_if_unchanged(marker)
388
412
  end
389
413
 
390
414
  def summary_finalizer_failure
391
- @summary_finalizer_failure ||= ->(error) { handle_summary_finalizer_failure(error) }
415
+ @summary_finalizer_failure ||= ->(error, **metadata) { handle_summary_finalizer_failure(error, **metadata) }
392
416
  end
393
417
 
394
418
  def reset_after_fork_state!
395
- state = runtime_state
396
- @configure_mutex = Mutex.new
397
- @configure_generation = Concurrent::AtomicFixnum.new(0)
398
- @state_mutex = Mutex.new
399
- @post_close_emit_count = Concurrent::AtomicFixnum.new(0)
419
+ @post_close_emit_count.value = 0
400
420
  @runtime_health = build_runtime_health
401
- @integration_health.after_fork!
402
- @invalid_severity_reporter.reset_after_fork!
403
- @state_ref = Concurrent::AtomicReference.new(state)
404
- nil
421
+ @integration_health.reset!
422
+ @invalid_severity_reporter.reset!
405
423
  end
406
424
 
407
425
  def reset_under_lock
408
426
  state = runtime_state
409
427
  configuration = Configuration.new.snapshot
410
428
  @invalid_severity_reporter.reset!
411
- next_pipeline = configuration.build_pipeline(invalid_severity_reporter: @invalid_severity_reporter)
412
- replace_pipeline(configuration, next_pipeline)
413
- 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)
414
432
  @post_close_emit_count.value = 0
433
+ ContextStore.reset_current!
415
434
  @runtime_health.clear_failures!
416
435
  @integration_health.reset!
417
436
  Diagnostics::ProcessIntegrationHealth.reset!
@@ -429,12 +448,12 @@ module Julewire
429
448
  raise Error, "Julewire.#{method_name} cannot be called from inside Julewire.configure"
430
449
  end
431
450
 
432
- block_given? ? yield : nil
451
+ yield if block_given?
433
452
  end
434
453
 
435
454
  def configure_guard_active?
436
455
  token = Fiber[CONFIGURE_GUARD_KEY]
437
- 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
438
457
  end
439
458
 
440
459
  def increment_lifecycle_attempt(method_name)
@@ -457,9 +476,8 @@ module Julewire
457
476
  @runtime_health.record_failure(error, callback: state.configuration.on_failure, **metadata)
458
477
  end
459
478
 
460
- def handle_summary_finalizer_failure(error)
479
+ def handle_summary_finalizer_failure(error, **metadata)
461
480
  state = runtime_state
462
- metadata = { phase: :summary_finalizer }
463
481
  @runtime_health.record_failure(error, callback: state.configuration.on_failure, **metadata)
464
482
  end
465
483
 
@@ -484,8 +502,6 @@ module Julewire
484
502
  end
485
503
 
486
504
  def notify_lifecycle_warning(warning)
487
- return unless warning
488
-
489
505
  result = Diagnostics::CallbackNotifier.call(warning.fetch(:on_failure), warning.fetch(:error),
490
506
  warning.fetch(:metadata))
491
507
  @runtime_health.record_callback_failure(result) if Diagnostics::CallbackNotifier.failure?(result)
@@ -494,7 +510,6 @@ module Julewire
494
510
  def build_runtime_health
495
511
  Diagnostics::Health.new(
496
512
  counter_keys: RUNTIME_COUNTER_KEYS,
497
- callback_metadata: {},
498
513
  callback_failure_counter: :runtime_callback_failures,
499
514
  failure_counter: :runtime_failures
500
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