julewire-core 1.0.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 (164) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +6 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +73 -0
  5. data/docs/advanced-configuration.md +66 -0
  6. data/docs/attribute-keys.md +74 -0
  7. data/docs/configuration.md +327 -0
  8. data/docs/context-and-propagation.md +353 -0
  9. data/docs/contracts.md +211 -0
  10. data/docs/development.md +49 -0
  11. data/docs/extensions-and-api.md +567 -0
  12. data/docs/health-schema.md +104 -0
  13. data/docs/instrumentation-cheatsheet.md +29 -0
  14. data/docs/internals.md +135 -0
  15. data/docs/outputs-and-lifecycle.md +206 -0
  16. data/docs/quickstart.md +133 -0
  17. data/docs/record-sources.md +17 -0
  18. data/docs/records-and-data-policy.md +230 -0
  19. data/docs/security-and-wire.md +45 -0
  20. data/docs/tail.md +91 -0
  21. data/exe/julewire +6 -0
  22. data/julewire-core.gemspec +41 -0
  23. data/lib/julewire/core/cli/doctor.rb +143 -0
  24. data/lib/julewire/core/cli/line_helpers.rb +77 -0
  25. data/lib/julewire/core/cli/log_formats/console_text.rb +25 -0
  26. data/lib/julewire/core/cli/log_formats/core_json_decoder.rb +46 -0
  27. data/lib/julewire/core/cli/log_formats/core_json_encoder.rb +21 -0
  28. data/lib/julewire/core/cli/log_formats/record_decoder.rb +39 -0
  29. data/lib/julewire/core/cli/log_formats.rb +123 -0
  30. data/lib/julewire/core/cli/tail.rb +153 -0
  31. data/lib/julewire/core/cli/transcode.rb +105 -0
  32. data/lib/julewire/core/cli.rb +73 -0
  33. data/lib/julewire/core/configuration.rb +99 -0
  34. data/lib/julewire/core/context_store.rb +384 -0
  35. data/lib/julewire/core/destinations/chaos_output.rb +91 -0
  36. data/lib/julewire/core/destinations/collection.rb +177 -0
  37. data/lib/julewire/core/destinations/definition.rb +125 -0
  38. data/lib/julewire/core/destinations/destination.rb +268 -0
  39. data/lib/julewire/core/destinations/registry.rb +81 -0
  40. data/lib/julewire/core/destinations/sink.rb +35 -0
  41. data/lib/julewire/core/destinations/synchronized_output.rb +57 -0
  42. data/lib/julewire/core/destinations/tail_sampling.rb +321 -0
  43. data/lib/julewire/core/destinations/write_step.rb +119 -0
  44. data/lib/julewire/core/destinations.rb +33 -0
  45. data/lib/julewire/core/diagnostics/callback_notifier.rb +63 -0
  46. data/lib/julewire/core/diagnostics/doctor.rb +114 -0
  47. data/lib/julewire/core/diagnostics/failure_snapshot.rb +39 -0
  48. data/lib/julewire/core/diagnostics/health.rb +144 -0
  49. data/lib/julewire/core/diagnostics/integration_health_store.rb +64 -0
  50. data/lib/julewire/core/diagnostics/internal_records.rb +61 -0
  51. data/lib/julewire/core/diagnostics/invalid_severity_reporter.rb +112 -0
  52. data/lib/julewire/core/diagnostics/meta_observer.rb +161 -0
  53. data/lib/julewire/core/diagnostics/process_integration_health.rb +26 -0
  54. data/lib/julewire/core/diagnostics/tail/renderer.rb +36 -0
  55. data/lib/julewire/core/diagnostics/tail.rb +168 -0
  56. data/lib/julewire/core/diagnostics.rb +8 -0
  57. data/lib/julewire/core/error.rb +7 -0
  58. data/lib/julewire/core/execution/boundary.rb +106 -0
  59. data/lib/julewire/core/execution/handle.rb +77 -0
  60. data/lib/julewire/core/execution/lineage.rb +192 -0
  61. data/lib/julewire/core/execution/measurement_handle.rb +28 -0
  62. data/lib/julewire/core/execution/no_current_error.rb +9 -0
  63. data/lib/julewire/core/execution/scope.rb +246 -0
  64. data/lib/julewire/core/execution/scope_fields.rb +76 -0
  65. data/lib/julewire/core/execution/scope_identity.rb +71 -0
  66. data/lib/julewire/core/execution/scope_snapshot.rb +92 -0
  67. data/lib/julewire/core/execution/summary_state.rb +206 -0
  68. data/lib/julewire/core/execution/view.rb +56 -0
  69. data/lib/julewire/core/facade_methods.rb +181 -0
  70. data/lib/julewire/core/fields/attribute_keys.rb +54 -0
  71. data/lib/julewire/core/fields/attributes_proxy.rb +11 -0
  72. data/lib/julewire/core/fields/bags.rb +123 -0
  73. data/lib/julewire/core/fields/carry_proxy.rb +22 -0
  74. data/lib/julewire/core/fields/context_proxy.rb +11 -0
  75. data/lib/julewire/core/fields/field_set.rb +78 -0
  76. data/lib/julewire/core/fields/field_stack.rb +269 -0
  77. data/lib/julewire/core/fields/internal/deletion.rb +68 -0
  78. data/lib/julewire/core/fields/internal.rb +87 -0
  79. data/lib/julewire/core/fields/lookup.rb +35 -0
  80. data/lib/julewire/core/fields/section_proxy.rb +88 -0
  81. data/lib/julewire/core/fields/stack_set.rb +69 -0
  82. data/lib/julewire/core/fields/static_labels.rb +43 -0
  83. data/lib/julewire/core/fields/summary_proxy.rb +62 -0
  84. data/lib/julewire/core/integration/configurable.rb +52 -0
  85. data/lib/julewire/core/integration/destination_health.rb +43 -0
  86. data/lib/julewire/core/integration/event_subscriber.rb +62 -0
  87. data/lib/julewire/core/integration/facade.rb +131 -0
  88. data/lib/julewire/core/integration/fork_hooks.rb +79 -0
  89. data/lib/julewire/core/integration/health.rb +41 -0
  90. data/lib/julewire/core/integration/ivar_state.rb +38 -0
  91. data/lib/julewire/core/integration/lifecycle.rb +22 -0
  92. data/lib/julewire/core/integration/scoped.rb +34 -0
  93. data/lib/julewire/core/integration/settings.rb +92 -0
  94. data/lib/julewire/core/integration/subscriber_install.rb +39 -0
  95. data/lib/julewire/core/integration/subscription.rb +29 -0
  96. data/lib/julewire/core/integration/values.rb +192 -0
  97. data/lib/julewire/core/lifecycle_error.rb +7 -0
  98. data/lib/julewire/core/local_storage.rb +91 -0
  99. data/lib/julewire/core/processing/level_threshold.rb +53 -0
  100. data/lib/julewire/core/processing/match.rb +74 -0
  101. data/lib/julewire/core/processing/pipeline.rb +360 -0
  102. data/lib/julewire/core/processing/processor_chain.rb +69 -0
  103. data/lib/julewire/core/processing/processor_registry.rb +115 -0
  104. data/lib/julewire/core/processing/processor_wrapper.rb +44 -0
  105. data/lib/julewire/core/processing/record_field_transform.rb +124 -0
  106. data/lib/julewire/core/processing/sampling.rb +109 -0
  107. data/lib/julewire/core/processing.rb +41 -0
  108. data/lib/julewire/core/propagation/carrier.rb +93 -0
  109. data/lib/julewire/core/propagation.rb +50 -0
  110. data/lib/julewire/core/records/console_formatter.rb +24 -0
  111. data/lib/julewire/core/records/deconstruct.rb +19 -0
  112. data/lib/julewire/core/records/display_message.rb +166 -0
  113. data/lib/julewire/core/records/draft.rb +576 -0
  114. data/lib/julewire/core/records/formatter.rb +14 -0
  115. data/lib/julewire/core/records/lazy_emit_input.rb +99 -0
  116. data/lib/julewire/core/records/metadata.rb +23 -0
  117. data/lib/julewire/core/records/public_projection.rb +51 -0
  118. data/lib/julewire/core/records/raw_input.rb +41 -0
  119. data/lib/julewire/core/records/record.rb +175 -0
  120. data/lib/julewire/core/records/severity.rb +44 -0
  121. data/lib/julewire/core/runtime.rb +515 -0
  122. data/lib/julewire/core/runtime_locator.rb +20 -0
  123. data/lib/julewire/core/runtime_registry.rb +48 -0
  124. data/lib/julewire/core/runtime_state.rb +39 -0
  125. data/lib/julewire/core/scheduling/deadline.rb +24 -0
  126. data/lib/julewire/core/scheduling/deadline_scheduler.rb +207 -0
  127. data/lib/julewire/core/scheduling/shared_scheduler.rb +48 -0
  128. data/lib/julewire/core/sentinel.rb +18 -0
  129. data/lib/julewire/core/serialization/backtrace_limiter.rb +50 -0
  130. data/lib/julewire/core/serialization/bounded_transform.rb +55 -0
  131. data/lib/julewire/core/serialization/bounded_traversal.rb +274 -0
  132. data/lib/julewire/core/serialization/deep_compact_empty.rb +67 -0
  133. data/lib/julewire/core/serialization/deep_freeze.rb +63 -0
  134. data/lib/julewire/core/serialization/encoding_sanitizer.rb +40 -0
  135. data/lib/julewire/core/serialization/exception_shape.rb +88 -0
  136. data/lib/julewire/core/serialization/json_encoder.rb +69 -0
  137. data/lib/julewire/core/serialization/serializer.rb +233 -0
  138. data/lib/julewire/core/serialization/serializer_pool.rb +21 -0
  139. data/lib/julewire/core/serialization/text_encoder.rb +147 -0
  140. data/lib/julewire/core/serialization/value_copy.rb +209 -0
  141. data/lib/julewire/core/serialization/value_traversal.rb +150 -0
  142. data/lib/julewire/core/testing/chaos/catalog.rb +72 -0
  143. data/lib/julewire/core/testing/chaos/core_runtime.rb +120 -0
  144. data/lib/julewire/core/testing/chaos/destination.rb +55 -0
  145. data/lib/julewire/core/testing/chaos/emitter.rb +20 -0
  146. data/lib/julewire/core/testing/chaos/raising_output.rb +42 -0
  147. data/lib/julewire/core/testing/chaos.rb +80 -0
  148. data/lib/julewire/core/testing/contracts/component.rb +162 -0
  149. data/lib/julewire/core/testing/contracts/deadline_scheduler.rb +59 -0
  150. data/lib/julewire/core/testing/contracts/integration.rb +166 -0
  151. data/lib/julewire/core/testing/contracts/integration_fields.rb +36 -0
  152. data/lib/julewire/core/testing/contracts/record_draft.rb +37 -0
  153. data/lib/julewire/core/testing/contracts/runtime.rb +178 -0
  154. data/lib/julewire/core/testing/contracts/wire.rb +60 -0
  155. data/lib/julewire/core/testing/contracts.rb +24 -0
  156. data/lib/julewire/core/testing/coverage.rb +58 -0
  157. data/lib/julewire/core/testing/test_reports.rb +78 -0
  158. data/lib/julewire/core/testing.rb +122 -0
  159. data/lib/julewire/core/validation.rb +69 -0
  160. data/lib/julewire/core/version.rb +7 -0
  161. data/lib/julewire/core.rb +80 -0
  162. data/lib/julewire/error.rb +5 -0
  163. data/lib/julewire-core.rb +3 -0
  164. metadata +237 -0
@@ -0,0 +1,515 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent/atomic/atomic_reference"
4
+ require "concurrent/atomic/atomic_fixnum"
5
+
6
+ module Julewire
7
+ module Core
8
+ class Runtime # rubocop:disable Metrics/ClassLength
9
+ CONFIGURE_GUARD_KEY = :__julewire_core_configure_guard__
10
+ RUNTIME_COUNTER_KEYS = %i[
11
+ close_attempts
12
+ configure_attempts
13
+ flush_attempts
14
+ lifecycle_warnings
15
+ post_close_emits_total
16
+ reset_attempts
17
+ runtime_callback_failures
18
+ runtime_failures
19
+ ].freeze
20
+ CloseTransition = Data.define(:state, :close_pipeline, :timeout)
21
+ PipelineReplacement = Data.define(
22
+ :old_pipeline,
23
+ :close_timeout,
24
+ :old_on_failure,
25
+ :close_pipeline,
26
+ :retained_resources
27
+ )
28
+ ResetTransition = Data.define(:old_pipeline, :close_timeout, :old_on_failure, :close_pipeline)
29
+
30
+ def initialize
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)
35
+ @runtime_health = build_runtime_health
36
+ @integration_health = Diagnostics::IntegrationHealthStore.new
37
+ @invalid_severity_reporter = Diagnostics::InvalidSeverityReporter.counter
38
+ @state_ref = Concurrent::AtomicReference.new(
39
+ RuntimeState.default(invalid_severity_reporter: @invalid_severity_reporter)
40
+ )
41
+ @execution_boundary = build_execution_boundary
42
+ end
43
+
44
+ def config = runtime_state.configuration
45
+
46
+ def labels = config.labels
47
+
48
+ def attributes = ContextStore.current.attributes_proxy
49
+
50
+ def carry = ContextStore.current.carry_proxy
51
+
52
+ def context = ContextStore.current.context_proxy
53
+
54
+ def summary = ContextStore.current.summary_proxy
55
+
56
+ def current_execution
57
+ scope = ContextStore.current.current_scope
58
+ scope && Execution::View.new(scope)
59
+ end
60
+
61
+ def current_execution?
62
+ ContextStore.current.current_scope?
63
+ end
64
+
65
+ def with_execution(...) = @execution_boundary.with_execution(...)
66
+
67
+ def start_execution(...) = @execution_boundary.start_execution(...)
68
+
69
+ def emit(record = Core::UNSET, **fields, &)
70
+ emit_with_level_check(record, true, fields, &)
71
+ end
72
+
73
+ def emit_without_level(record = Core::UNSET, **fields, &)
74
+ emit_with_level_check(record, false, fields, &)
75
+ end
76
+
77
+ def emit_integration(record, enforce_level: true)
78
+ with_emit_guard(:emit_integration) do |state|
79
+ state.pipeline.emit_integration(record, enforce_level: enforce_level)
80
+ end
81
+ end
82
+
83
+ def emit_envelope(input:, context:, scope:, carry: {}, attributes: {}, neutral: {}, enforce_level: true)
84
+ reject_runtime_call_during_configure!(:emit_envelope)
85
+ state = runtime_state
86
+ return record_post_close_emit(state) if state.pipeline_closed
87
+
88
+ 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
99
+ state.pipeline.emit_record(record, enforce_level: enforce_level)
100
+ rescue StandardError => e
101
+ notify_failure(e, state, action: :emit_envelope)
102
+ nil
103
+ end
104
+ end
105
+
106
+ def emit_summary_record(scope)
107
+ reject_runtime_call_during_configure!(:emit_summary_record)
108
+ state = runtime_state
109
+ return record_post_close_emit(state) if state.pipeline_closed
110
+
111
+ begin
112
+ input = scope.owned_summary_record_input
113
+ state.pipeline.emit_isolated_input(input)
114
+ rescue StandardError => e
115
+ notify_failure(e, state, action: :emit_summary_record)
116
+ nil
117
+ end
118
+ end
119
+
120
+ def configure(&)
121
+ raise ArgumentError, "Julewire.configure requires a block" unless block_given?
122
+
123
+ increment_runtime_count(:configure_attempts)
124
+ replacement = build_configured_pipeline(&)
125
+ deadline = Scheduling::Deadline.for(replacement.close_timeout)
126
+ if replacement.close_pipeline
127
+ report_pipeline_close_result(
128
+ replacement.old_pipeline,
129
+ timeout: Scheduling::Deadline.remaining(deadline),
130
+ on_failure: replacement.old_on_failure,
131
+ operation: :configure,
132
+ skip_resource_identities: replacement.retained_resources
133
+ )
134
+ end
135
+ config
136
+ end
137
+
138
+ def flush(timeout: Core::UNSET)
139
+ call_validated_lifecycle(:flush, timeout)
140
+ end
141
+
142
+ def close(timeout: Core::UNSET)
143
+ close_state_resources(close_state(timeout))
144
+ end
145
+
146
+ def reset!
147
+ increment_runtime_count(:reset_attempts)
148
+ reset_result = reject_runtime_call_during_configure!(:reset!) do
149
+ @configure_mutex.synchronize do
150
+ @state_mutex.synchronize { reset_under_lock }
151
+ end
152
+ end
153
+ deadline = Scheduling::Deadline.for(reset_result.close_timeout)
154
+ return unless reset_result.close_pipeline
155
+
156
+ report_pipeline_close_result(
157
+ reset_result.old_pipeline,
158
+ timeout: Scheduling::Deadline.remaining(deadline),
159
+ on_failure: reset_result.old_on_failure,
160
+ operation: :reset
161
+ )
162
+ end
163
+
164
+ def after_fork!
165
+ reject_runtime_call_during_configure!(:after_fork!)
166
+ RuntimeRegistry.reset_after_fork(primary: self)
167
+ end
168
+
169
+ def reset_after_fork_runtime!
170
+ reset_after_fork_state!
171
+ runtime_state.pipeline.after_fork!
172
+ self
173
+ end
174
+
175
+ def record_integration_failure(integration, error, **metadata)
176
+ @integration_health.record_failure(integration, error, **metadata)
177
+ end
178
+
179
+ def record_integration_success(integration)
180
+ @integration_health.record_success(integration)
181
+ end
182
+
183
+ def health
184
+ state = runtime_state
185
+ pipeline_health = state.pipeline.health
186
+ integrations = @integration_health.health
187
+ process_integrations = Diagnostics::ProcessIntegrationHealth.health
188
+ {
189
+ closed: state.pipeline_closed,
190
+ counts: runtime_counts_snapshot,
191
+ generation: state.pipeline_generation,
192
+ integrations: integrations,
193
+ last_callback_failure: @runtime_health.last_callback_failure,
194
+ last_failure: @runtime_health.last_failure,
195
+ pipeline: pipeline_health,
196
+ process_integrations: process_integrations,
197
+ status: runtime_status(state, pipeline_health, integrations, process_integrations)
198
+ }
199
+ end
200
+
201
+ private
202
+
203
+ def before_execution_boundary_call!(action)
204
+ reject_runtime_call_during_configure!(action)
205
+ end
206
+
207
+ def runtime_state = @state_ref.get
208
+
209
+ def build_configured_pipeline(&)
210
+ reject_runtime_call_during_configure!(:configure) do
211
+ with_configure_guard do
212
+ @configure_mutex.synchronize { configure_transaction(&) }
213
+ end
214
+ end
215
+ end
216
+
217
+ def configure_transaction
218
+ state = runtime_state
219
+ next_configuration_builder = state.configuration.copy
220
+ yield next_configuration_builder
221
+ next_configuration = next_configuration_builder.snapshot
222
+ next_pipeline = next_configuration.build_pipeline(invalid_severity_reporter: @invalid_severity_reporter)
223
+
224
+ install_and_replace_pipeline(state, next_configuration, next_pipeline)
225
+ end
226
+
227
+ 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
233
+ PipelineReplacement.new(
234
+ replaced_pipeline,
235
+ state.configuration.pipeline_close_timeout,
236
+ state.configuration.on_failure,
237
+ !state.pipeline_closed,
238
+ next_pipeline.lifecycle_resource_identities
239
+ )
240
+ rescue StandardError
241
+ next_pipeline.close(timeout: next_configuration.pipeline_close_timeout)
242
+ raise
243
+ end
244
+
245
+ def with_configure_guard
246
+ previous = Fiber[CONFIGURE_GUARD_KEY]
247
+ Fiber[CONFIGURE_GUARD_KEY] = [object_id, @configure_generation.increment]
248
+ yield
249
+ ensure
250
+ @configure_generation.increment
251
+ Fiber[CONFIGURE_GUARD_KEY] = previous
252
+ end
253
+
254
+ def replace_pipeline(configuration, pipeline)
255
+ state = runtime_state
256
+ @post_close_emit_count.value = 0
257
+ @runtime_health.clear_degradation
258
+ @state_ref.set(state.next_generation(configuration: configuration, pipeline: pipeline))
259
+ state.pipeline
260
+ end
261
+
262
+ def call_validated_lifecycle(method_name, timeout)
263
+ degradation_marker = @runtime_health.degradation_marker
264
+ reject_runtime_call_during_configure!(method_name)
265
+ state = runtime_state
266
+ timeout = normalize_lifecycle_timeout(timeout, state)
267
+ validate_lifecycle_timeout!(timeout, name: :timeout)
268
+ increment_lifecycle_attempt(method_name)
269
+ return true if state.pipeline_closed
270
+
271
+ result = call_pipeline_lifecycle_on(state.pipeline, method_name, timeout: timeout, state: state)
272
+ clear_runtime_degradation_if_unchanged(degradation_marker) unless result == false
273
+ result
274
+ end
275
+
276
+ def normalize_lifecycle_timeout(timeout, state)
277
+ timeout.equal?(Core::UNSET) ? state.configuration.pipeline_close_timeout : timeout
278
+ end
279
+
280
+ def validate_lifecycle_timeout!(timeout, name:)
281
+ Validation.validate_timeout!(timeout, name: name)
282
+ end
283
+
284
+ def close_state_resources(transition)
285
+ return true unless transition.close_pipeline
286
+
287
+ deadline = Scheduling::Deadline.for(transition.timeout)
288
+ call_pipeline_lifecycle_on(
289
+ transition.state.pipeline,
290
+ :close,
291
+ timeout: Scheduling::Deadline.remaining(deadline),
292
+ state: transition.state
293
+ )
294
+ end
295
+
296
+ def close_state(timeout)
297
+ 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)
308
+ end
309
+ end
310
+
311
+ def call_pipeline_lifecycle_on(pipeline, method_name, timeout:, state:)
312
+ pipeline.public_send(method_name, timeout: timeout)
313
+ rescue StandardError => e
314
+ notify_failure(e, state, action: method_name)
315
+ false
316
+ end
317
+
318
+ def emit_with_level_check(record, enforce_level, fields, &)
319
+ with_emit_guard(:emit) do |state|
320
+ if enforce_level
321
+ state.pipeline.emit(record, **fields, &)
322
+ else
323
+ state.pipeline.emit_without_level(record, **fields, &)
324
+ end
325
+ end
326
+ end
327
+
328
+ def with_emit_guard(action)
329
+ degradation_marker = @runtime_health.degradation_marker
330
+ reject_runtime_call_during_configure!(action)
331
+ state = runtime_state
332
+ return record_post_close_emit(state) if state.pipeline_closed
333
+
334
+ begin
335
+ yield state
336
+ clear_runtime_degradation_if_unchanged(degradation_marker)
337
+ nil
338
+ rescue StandardError => e
339
+ notify_failure(e, state, action: action)
340
+ nil
341
+ end
342
+ end
343
+
344
+ def envelope_hash(value)
345
+ value.is_a?(Hash) ? value : {}
346
+ end
347
+
348
+ def record_post_close_emit(state)
349
+ @post_close_emit_count.increment
350
+ increment_runtime_count(:post_close_emits_total)
351
+ metadata = { phase: :runtime, reason: :runtime_closed }
352
+ callback_result = Diagnostics::CallbackNotifier.call(state.configuration.on_drop, :runtime_closed, metadata)
353
+ if Diagnostics::CallbackNotifier.failure?(callback_result)
354
+ @runtime_health.record_callback_failure(callback_result)
355
+ end
356
+ nil
357
+ end
358
+
359
+ def runtime_status(state, pipeline_health, integrations, process_integrations)
360
+ return :closed if state.pipeline_closed
361
+
362
+ runtime_degraded?(pipeline_health, integrations, process_integrations) ? :degraded : :ok
363
+ end
364
+
365
+ def runtime_degraded?(pipeline_health, integrations, process_integrations)
366
+ @runtime_health.degraded? ||
367
+ pipeline_degraded?(pipeline_health) ||
368
+ integrations_degraded?(process_integrations) ||
369
+ integrations_degraded?(integrations)
370
+ end
371
+
372
+ def pipeline_degraded?(pipeline_health)
373
+ return true if pipeline_health[:status] && pipeline_health[:status] != :ok
374
+
375
+ pipeline_health.fetch(:destinations).values.any? do |destination_health|
376
+ destination_health[:status] && destination_health[:status] != :ok
377
+ end
378
+ end
379
+
380
+ def integrations_degraded?(integrations)
381
+ integrations.values.any? do |integration_health|
382
+ integration_health[:status] && integration_health[:status] != :ok
383
+ end
384
+ end
385
+
386
+ def clear_runtime_degradation_if_unchanged(marker)
387
+ @runtime_health.clear_degradation_if_unchanged(marker)
388
+ end
389
+
390
+ def summary_finalizer_failure
391
+ @summary_finalizer_failure ||= ->(error) { handle_summary_finalizer_failure(error) }
392
+ end
393
+
394
+ 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)
400
+ @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
405
+ end
406
+
407
+ def reset_under_lock
408
+ state = runtime_state
409
+ configuration = Configuration.new.snapshot
410
+ @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!
414
+ @post_close_emit_count.value = 0
415
+ @runtime_health.clear_failures!
416
+ @integration_health.reset!
417
+ Diagnostics::ProcessIntegrationHealth.reset!
418
+ Diagnostics::InvalidSeverityReporter.reset!
419
+ ResetTransition.new(
420
+ state.pipeline,
421
+ state.configuration.pipeline_close_timeout,
422
+ state.configuration.on_failure,
423
+ !state.pipeline_closed
424
+ )
425
+ end
426
+
427
+ def reject_runtime_call_during_configure!(method_name)
428
+ if configure_guard_active?
429
+ raise Error, "Julewire.#{method_name} cannot be called from inside Julewire.configure"
430
+ end
431
+
432
+ block_given? ? yield : nil
433
+ end
434
+
435
+ def configure_guard_active?
436
+ token = Fiber[CONFIGURE_GUARD_KEY]
437
+ token.is_a?(Array) && token.fetch(0) == object_id && token.fetch(1) == @configure_generation.value
438
+ end
439
+
440
+ def increment_lifecycle_attempt(method_name)
441
+ increment_runtime_count(:"#{method_name}_attempts")
442
+ end
443
+
444
+ def increment_runtime_count(key)
445
+ @runtime_health.increment(key)
446
+ end
447
+
448
+ def runtime_counts_snapshot
449
+ @runtime_health.counts.merge(
450
+ invalid_record_severities: @invalid_severity_reporter.health.fetch(:count),
451
+ post_close_emits: @post_close_emit_count.value
452
+ )
453
+ end
454
+
455
+ def notify_failure(error, state, **metadata)
456
+ metadata = { phase: :runtime }.merge(metadata)
457
+ @runtime_health.record_failure(error, callback: state.configuration.on_failure, **metadata)
458
+ end
459
+
460
+ def handle_summary_finalizer_failure(error)
461
+ state = runtime_state
462
+ metadata = { phase: :summary_finalizer }
463
+ @runtime_health.record_failure(error, callback: state.configuration.on_failure, **metadata)
464
+ end
465
+
466
+ def report_pipeline_close_result(pipeline, timeout:, on_failure:, operation:, skip_resource_identities: nil)
467
+ return unless pipeline.close(timeout: timeout, skip_resource_identities: skip_resource_identities) == false
468
+
469
+ notify_lifecycle_warning(
470
+ record_lifecycle_warning(
471
+ LifecycleError.new("Julewire pipeline close returned false"),
472
+ on_failure: on_failure,
473
+ action: :close,
474
+ operation: operation,
475
+ phase: :pipeline_teardown,
476
+ timeout: timeout
477
+ )
478
+ )
479
+ end
480
+
481
+ def record_lifecycle_warning(error, on_failure:, **metadata)
482
+ increment_runtime_count(:lifecycle_warnings)
483
+ { error: error, metadata: metadata, on_failure: on_failure }
484
+ end
485
+
486
+ def notify_lifecycle_warning(warning)
487
+ return unless warning
488
+
489
+ result = Diagnostics::CallbackNotifier.call(warning.fetch(:on_failure), warning.fetch(:error),
490
+ warning.fetch(:metadata))
491
+ @runtime_health.record_callback_failure(result) if Diagnostics::CallbackNotifier.failure?(result)
492
+ end
493
+
494
+ def build_runtime_health
495
+ Diagnostics::Health.new(
496
+ counter_keys: RUNTIME_COUNTER_KEYS,
497
+ callback_metadata: {},
498
+ callback_failure_counter: :runtime_callback_failures,
499
+ failure_counter: :runtime_failures
500
+ )
501
+ end
502
+
503
+ def emit_non_standard_exception_summaries? = runtime_state.configuration.emit_non_standard_exception_summaries
504
+
505
+ def build_execution_boundary
506
+ Execution::Boundary.new(
507
+ before_call: ->(action) { before_execution_boundary_call!(action) },
508
+ emit_summary_record: ->(scope) { emit_summary_record(scope) },
509
+ summary_finalizer_failure: summary_finalizer_failure,
510
+ emit_non_standard_exception_summaries: -> { emit_non_standard_exception_summaries? }
511
+ )
512
+ end
513
+ end
514
+ end
515
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ # @api bridge_spi
6
+ # Runtime swapping exists for concurrency bridges and integration tests.
7
+ # Application code should prefer the top-level Julewire facade.
8
+ module RuntimeLocator
9
+ class << self
10
+ def current
11
+ LocalStorage.runtime
12
+ end
13
+
14
+ def current=(runtime)
15
+ LocalStorage.runtime = runtime
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ module RuntimeRegistry
6
+ DEFAULT_NAME = :default
7
+ private_constant :DEFAULT_NAME
8
+
9
+ @mutex = Mutex.new
10
+ @runtimes = {}
11
+
12
+ class << self
13
+ def fetch(name, current: RuntimeLocator.current)
14
+ name = Core.normalize_name(name, name: "runtime name")
15
+ return current if name == DEFAULT_NAME
16
+
17
+ unless current.is_a?(Runtime)
18
+ raise Error, "named Julewire runtimes are not available from the current runtime"
19
+ end
20
+
21
+ @mutex.synchronize { @runtimes[name] ||= Runtime.new }
22
+ end
23
+
24
+ def clear!
25
+ @mutex.synchronize { @runtimes.clear }
26
+ nil
27
+ end
28
+
29
+ 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
34
+
35
+ LocalStorage.after_fork!
36
+ ContextStore.reset_current!
37
+ Scheduling::SharedScheduler.after_fork!
38
+ Diagnostics::ProcessIntegrationHealth.after_fork!
39
+ Integration::ForkHooks.after_fork!
40
+ Diagnostics::InvalidSeverityReporter.reset_after_fork!
41
+ runtimes.each(&:reset_after_fork_runtime!)
42
+ Integration::ForkHooks.run
43
+ nil
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ RuntimeState = Data.define(
6
+ :configuration,
7
+ :pipeline,
8
+ :pipeline_closed,
9
+ :pipeline_generation
10
+ ) do
11
+ class << self
12
+ def default(invalid_severity_reporter: Diagnostics::InvalidSeverityReporter.counter)
13
+ configuration = Configuration.new.snapshot
14
+ pipeline = configuration.build_pipeline(invalid_severity_reporter: invalid_severity_reporter)
15
+
16
+ new(
17
+ configuration: configuration,
18
+ pipeline: pipeline,
19
+ pipeline_closed: false,
20
+ pipeline_generation: 0
21
+ )
22
+ end
23
+ end
24
+
25
+ def closed
26
+ with(pipeline_closed: true)
27
+ end
28
+
29
+ def next_generation(configuration:, pipeline:)
30
+ self.class.new(
31
+ configuration: configuration,
32
+ pipeline: pipeline,
33
+ pipeline_closed: false,
34
+ pipeline_generation: pipeline_generation + 1
35
+ )
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ module Scheduling
6
+ module Deadline
7
+ CLOCK = Process::CLOCK_MONOTONIC
8
+
9
+ class << self
10
+ def for(timeout)
11
+ Process.clock_gettime(CLOCK) + timeout if timeout
12
+ end
13
+
14
+ def remaining(deadline)
15
+ return unless deadline
16
+
17
+ remaining = deadline - Process.clock_gettime(CLOCK)
18
+ remaining.positive? ? remaining : 0
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end