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
@@ -9,7 +9,6 @@ module Julewire
9
9
  reject_output_array!(output)
10
10
  return output if wrapped?(output)
11
11
 
12
- validate_writeable!(output)
13
12
  SynchronizedOutput.new(output, close_output: close_output)
14
13
  end
15
14
 
@@ -4,16 +4,21 @@ module Julewire
4
4
  module Core
5
5
  module Destinations
6
6
  class SynchronizedOutput
7
+ TIMEOUT_PARAMETER_TYPES = %i[key keyreq].freeze
8
+ private_constant :TIMEOUT_PARAMETER_TYPES
9
+
7
10
  def initialize(output, close_output: false)
8
11
  Sink.validate_writeable!(output)
9
12
  @output = output
10
13
  @close_output = close_output
11
14
  @mutex = Mutex.new
15
+ @lifecycle_mutex = Mutex.new
16
+ @lifecycle = lifecycle_methods
12
17
  end
13
18
 
14
19
  def after_fork!
15
- @mutex = Mutex.new
16
20
  @output.after_fork! if @output.respond_to?(:after_fork!)
21
+ @lifecycle = lifecycle_methods
17
22
  self
18
23
  end
19
24
 
@@ -22,34 +27,66 @@ module Julewire
22
27
  def resource_identity = @output
23
28
 
24
29
  def write(value)
25
- @mutex.synchronize { @output.write(value) }
30
+ @mutex.synchronize do
31
+ return false if output_closed?
32
+
33
+ @output.write(value)
34
+ end
26
35
  end
27
36
 
28
- def flush
29
- @mutex.synchronize do
30
- return true unless @output.respond_to?(:flush)
37
+ def flush(timeout: nil)
38
+ @lifecycle_mutex.synchronize do
39
+ lifecycle = @lifecycle[:flush]
40
+ return true unless lifecycle
31
41
 
32
- @output.flush != false
42
+ lifecycle_succeeded?(call_lifecycle(:flush, lifecycle, timeout: timeout))
33
43
  end
34
44
  end
35
45
 
36
- def close
37
- @mutex.synchronize do
38
- return true if output_closed?
39
-
40
- result = if @close_output && @output.respond_to?(:close)
41
- @output.close
42
- elsif @output.respond_to?(:flush)
43
- @output.flush
44
- end
45
- result != false
46
+ def close(timeout: nil)
47
+ @lifecycle_mutex.synchronize do
48
+ # Close is terminal: lifecycle calls stay serialized, and the write mutex
49
+ # keeps the underlying output from being closed while a write is in flight.
50
+ @mutex.synchronize do
51
+ return true if output_closed?
52
+
53
+ if @close_output && (close_lifecycle = @lifecycle[:close])
54
+ return lifecycle_succeeded?(call_lifecycle(:close, close_lifecycle, timeout: timeout))
55
+ end
56
+
57
+ return true unless (flush_lifecycle = @lifecycle[:flush])
58
+
59
+ lifecycle_succeeded?(call_lifecycle(:flush, flush_lifecycle, timeout: timeout))
60
+ end
46
61
  end
47
62
  end
48
63
 
49
64
  private
50
65
 
51
66
  def output_closed?
52
- @output.respond_to?(:closed?) ? @output.closed? : false
67
+ @output.respond_to?(:closed?) && @output.closed?
68
+ end
69
+
70
+ def lifecycle_succeeded?(result) = !result.equal?(false)
71
+
72
+ def call_lifecycle(name, lifecycle, timeout:)
73
+ return @output.public_send(name, timeout: timeout) if lifecycle.fetch(:timeout)
74
+
75
+ @output.public_send(name)
76
+ end
77
+
78
+ def lifecycle_methods
79
+ %i[flush close].each_with_object({}) do |name, methods|
80
+ next unless @output.respond_to?(name)
81
+
82
+ method = @output.public_method(name)
83
+ methods[name] = { timeout: accepts_timeout_keyword?(method) }
84
+ end
85
+ end
86
+
87
+ def accepts_timeout_keyword?(method)
88
+ method.parameters.any? { |type, name| TIMEOUT_PARAMETER_TYPES.include?(type) && name == :timeout } ||
89
+ method.parameters.any? { |type, _name| type == :keyrest }
53
90
  end
54
91
  end
55
92
  end
@@ -29,7 +29,8 @@ module Julewire
29
29
  slow_ms
30
30
  ].freeze
31
31
  private_constant :COUNTER_KEYS, :ERROR_RANK, :OPTION_KEYS
32
-
32
+ Sampling = Processing::Sampling
33
+ private_constant :Sampling
33
34
  TailOptions = Data.define(:decider, :max_executions, :max_records_per_execution, :name, :on_drop, :on_failure,
34
35
  :sample_rate, :slow_ms)
35
36
  private_constant :TailOptions
@@ -42,7 +43,7 @@ module Julewire
42
43
  @destination = Registry.validate!(destination)
43
44
  @name = Destinations.normalize_name(options.name)
44
45
  @sample_rate = options.sample_rate
45
- Processing::Sampling.threshold_for(options.sample_rate)
46
+ Sampling.threshold_for(options.sample_rate)
46
47
  @slow_ms = validate_slow_ms(options.slow_ms)
47
48
  @max_executions = Validation.validate_integer_limit!(
48
49
  options.max_executions,
@@ -65,7 +66,7 @@ module Julewire
65
66
 
66
67
  def emit(record)
67
68
  result = @mutex.synchronize { accept_record(record) }
68
- result.losses.compact.each { notify_drop(it) }
69
+ result.losses.each { notify_drop(it) }
69
70
  result.records.each { emit_target(it) }
70
71
  nil
71
72
  rescue StandardError => e
@@ -160,7 +161,7 @@ module Julewire
160
161
  @order << key
161
162
  Buffer.new([])
162
163
  end)
163
- if buffer.records.length >= @max_records_per_execution
164
+ if buffer.records.length == @max_records_per_execution
164
165
  dropped = buffer.records.shift
165
166
  losses << record_loss(:overflow_dropped, dropped)
166
167
  end
@@ -174,8 +175,7 @@ module Julewire
174
175
  return [] if @buffers.length < @max_executions
175
176
 
176
177
  oldest = @order.shift
177
- buffer = @buffers.delete(oldest)
178
- Array(buffer&.records).map { record_loss(:overflow_dropped, it) }
178
+ @buffers.delete(oldest).records.map { record_loss(:overflow_dropped, it) }
179
179
  end
180
180
 
181
181
  def finish_execution(key, summary)
@@ -205,7 +205,7 @@ module Julewire
205
205
  end
206
206
 
207
207
  def keep_execution?(record, key)
208
- return !!@decider.call(record, key: key) if @decider
208
+ return @decider.call(record, key: key) if @decider
209
209
 
210
210
  default_keep_execution?(record, key)
211
211
  rescue StandardError => e
@@ -214,7 +214,7 @@ module Julewire
214
214
  end
215
215
 
216
216
  def default_keep_execution?(record, key)
217
- error_record?(record) || slow_record?(record) || Processing::Sampling.keep?(rate: @sample_rate, key: key)
217
+ error_record?(record) || slow_record?(record) || Sampling.keep?(rate: @sample_rate, key: key)
218
218
  end
219
219
 
220
220
  def error_record?(record)
@@ -235,7 +235,7 @@ module Julewire
235
235
  def summary_record?(record) = record[:kind] == :summary
236
236
 
237
237
  def execution_key(record)
238
- reference = record.respond_to?(:lineage) ? record.lineage.root_reference : nil
238
+ reference = record.lineage.root_reference if record.respond_to?(:lineage)
239
239
  reference = record[:execution] unless reference.is_a?(Hash)
240
240
  id = field_value(reference, :id)
241
241
  return unless id
@@ -256,7 +256,7 @@ module Julewire
256
256
  end
257
257
 
258
258
  def lifecycle(method_name, timeout:)
259
- @destination.public_send(method_name, timeout: timeout) != false
259
+ !@destination.public_send(method_name, timeout: timeout).eql?(false)
260
260
  rescue StandardError => e
261
261
  record_failure(e, nil, phase: :tail_sampling_lifecycle, action: method_name)
262
262
  false
@@ -270,8 +270,6 @@ module Julewire
270
270
 
271
271
  def record_loss(reason, record)
272
272
  @health.record_loss(reason: reason, record_metadata: Records::Metadata.call(record))
273
- rescue StandardError => e
274
- record_failure(e, record, phase: :tail_sampling_drop)
275
273
  end
276
274
 
277
275
  def notify_drop(loss)
@@ -281,29 +279,22 @@ module Julewire
281
279
  end
282
280
 
283
281
  def record_failure(error, record, phase: :tail_sampling, **metadata)
284
- failure_metadata = if @mutex.owned?
285
- record_failure_state(error, record, phase: phase, **metadata)
286
- else
287
- @mutex.synchronize { record_failure_state(error, record, phase: phase, **metadata) }
288
- end
289
- @on_failure&.call(error, **failure_metadata)
282
+ failure_metadata = record_failure_state(error, record, phase: phase, **metadata)
283
+ @on_failure.call(error, **failure_metadata)
290
284
  rescue StandardError
291
285
  nil
292
286
  end
293
287
 
294
288
  def record_failure_state(error, record, phase:, **metadata)
295
- @health.record_failure(
296
- error,
297
- **metadata,
298
- destination: @name,
299
- phase: phase,
300
- record_metadata: record ? Records::Metadata.call(record) : nil
301
- )
302
- metadata.merge(destination: @name, phase: phase)
289
+ metadata = metadata.merge(destination: @name, phase: phase)
290
+ record_metadata = record && Records::Metadata.call(record)
291
+ metadata[:record_metadata] = record_metadata if record_metadata
292
+ @health.record_failure(error, **metadata)
293
+ metadata
303
294
  end
304
295
 
305
296
  def health_status
306
- return :degraded if @health.last_failure || @health.last_loss&.fetch(:reason) == :overflow_dropped
297
+ return if @health.last_failure || @health.last_loss&.fetch(:reason) == :overflow_dropped
307
298
 
308
299
  :ok
309
300
  end
@@ -28,16 +28,16 @@ module Julewire
28
28
  def call(record)
29
29
  increment(:received)
30
30
  payload = format_record(record)
31
- return :dropped unless payload
31
+ return false unless payload
32
32
 
33
33
  increment(:formatted)
34
34
  encoded = encode_payload(payload, record)
35
- return :dropped unless encoded
36
- return :dropped unless within_limit?(encoded, record)
37
- return :dropped unless write(encoded, record)
35
+ return false unless encoded
36
+ return false unless within_limit?(encoded, record)
37
+ return false unless write(encoded, record)
38
38
 
39
39
  increment(:output_accepted)
40
- :accepted
40
+ true
41
41
  end
42
42
 
43
43
  private
@@ -13,13 +13,6 @@ module Julewire
13
13
  nil
14
14
  end
15
15
 
16
- # Private testing seam for `Julewire::Testing.unregister_destination`.
17
- def unregister(kind)
18
- @factories.delete(normalize_name(kind))
19
- nil
20
- end
21
- private :unregister
22
-
23
16
  def factory_for(kind)
24
17
  @factories[normalize_name(kind)]
25
18
  end
@@ -11,7 +11,9 @@ module Julewire
11
11
 
12
12
  private_constant :NestedCallback
13
13
 
14
- Failure = Data.define(:at, :class_name, :metadata) do
14
+ Failure = Data.define(:at, :class_name, :metadata)
15
+
16
+ class Failure
15
17
  def to_h
16
18
  {
17
19
  action: metadata[:action],
@@ -42,7 +44,7 @@ module Julewire
42
44
  end
43
45
 
44
46
  def failure?(result)
45
- result.is_a?(Failure)
47
+ result.instance_of?(Failure)
46
48
  end
47
49
 
48
50
  def nested_callback_result(metadata)
@@ -52,25 +52,31 @@ module Julewire
52
52
 
53
53
  def destination_info(destinations)
54
54
  destinations.transform_values do |destination|
55
- {
56
- counts: destination[:counts],
57
- last_failure: destination[:last_failure],
58
- last_loss: destination[:last_loss],
59
- status: destination[:status]
60
- }.compact
55
+ destination_component_info(destination)
61
56
  end
62
57
  end
63
58
 
64
59
  def integration_info(integrations)
65
60
  integrations.transform_values do |integration|
66
- {
67
- counts: integration[:counts],
68
- last_failure: integration[:last_failure],
69
- status: integration[:status]
70
- }.compact
61
+ component_info(integration)
71
62
  end
72
63
  end
73
64
 
65
+ def destination_component_info(component)
66
+ info = component_info(component)
67
+ last_loss = component[:last_loss]
68
+ info[:last_loss] = last_loss if last_loss
69
+ info
70
+ end
71
+
72
+ def component_info(component)
73
+ {
74
+ counts: component.fetch(:counts),
75
+ last_failure: component[:last_failure],
76
+ status: component.fetch(:status)
77
+ }.compact
78
+ end
79
+
74
80
  def warnings(health)
75
81
  [].tap do |items|
76
82
  items << warning(:runtime_closed, "runtime is closed") if health.fetch(:closed)
@@ -82,7 +88,7 @@ module Julewire
82
88
 
83
89
  def pipeline_warnings(pipeline, items)
84
90
  items << warning(:no_destinations, "pipeline has no destinations") unless pipeline.fetch(:configured)
85
- if pipeline.fetch(:configured) && pipeline.fetch(:status) != :ok
91
+ if pipeline.fetch(:configured) && !pipeline.fetch(:status).equal?(:ok)
86
92
  items << warning(:pipeline_degraded, "pipeline is #{pipeline.fetch(:status)}")
87
93
  end
88
94
  component_warnings(
@@ -99,14 +105,15 @@ module Julewire
99
105
 
100
106
  def component_warnings(components, items, code:, label:)
101
107
  components.each do |name, component|
102
- next if component[:status] == :ok
108
+ status = component.fetch(:status)
109
+ next if status == :ok
103
110
 
104
- items << warning(code, "#{label} #{name} is #{component[:status]}")
111
+ items << warning(code, "#{label} #{name} is #{status}")
105
112
  end
106
113
  end
107
114
 
108
115
  def warning(code, message)
109
- { code: code, message: message }.freeze
116
+ { code: code, message: message }
110
117
  end
111
118
  end
112
119
  end
@@ -16,7 +16,11 @@ module Julewire
16
16
  integration: metadata[:integration],
17
17
  output_class: metadata[:output_class],
18
18
  phase: metadata[:phase],
19
- record: record_metadata(metadata[:record_metadata])
19
+ processor: metadata[:processor],
20
+ reason: metadata[:reason],
21
+ record: record_metadata(metadata[:record_metadata]),
22
+ result_class: metadata[:result_class],
23
+ status: metadata[:status]
20
24
  }.compact.freeze
21
25
  end
22
26
 
@@ -25,12 +29,14 @@ module Julewire
25
29
  def record_metadata(value)
26
30
  return unless value.is_a?(Hash)
27
31
 
28
- {
32
+ labels = value[:labels]
33
+ metadata = {
29
34
  event: value[:event],
30
- labels: value[:labels].is_a?(Hash) ? Fields::FieldSet.deep_dup(value[:labels]) : nil,
31
35
  severity: value[:severity],
32
36
  source: value[:source]
33
37
  }.compact
38
+ metadata[:labels] = Fields::FieldSet.deep_dup(labels) if labels.is_a?(Hash)
39
+ metadata
34
40
  end
35
41
  end
36
42
  end
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "concurrent/atomic/atomic_fixnum"
4
+ require "concurrent/atomic/atomic_reference"
5
+
3
6
  module Julewire
4
7
  module Core
5
8
  module Diagnostics
@@ -15,106 +18,91 @@ module Julewire
15
18
  @callback_metadata = callback_metadata
16
19
  @failure_counter = failure_counter
17
20
  @track_failures = track_failures
18
- @mutex = Mutex.new
19
- counter_keys = counter_keys.to_a
21
+ counter_keys = counter_keys.map { it }
20
22
  counter_keys = counter_keys.union([:failures]) if @track_failures
21
- @counts = counter_keys.to_h { [it, 0] }
22
- @current_degradation = nil
23
- @last_callback_failure = nil
24
- @last_failure = nil
25
- @last_loss = nil
23
+ @counts = counter_keys.to_h { [it, Concurrent::AtomicFixnum.new] }
24
+ @current_degradation = Concurrent::AtomicReference.new
25
+ @last_callback_failure = Concurrent::AtomicReference.new
26
+ @last_failure = Concurrent::AtomicReference.new
27
+ @last_loss = Concurrent::AtomicReference.new
26
28
  end
27
29
 
28
30
  def increment(key, by: 1)
29
- @mutex.synchronize { increment_unlocked(key, by: by) }
31
+ @counts.fetch(key).increment(by)
30
32
  end
31
33
 
32
34
  def counts
33
- @mutex.synchronize { @counts.dup.freeze }
35
+ @counts.to_h { |key, counter| [key, counter.value] }.freeze
34
36
  end
35
37
 
36
38
  def degradation_marker
37
- @mutex.synchronize { @current_degradation }
39
+ @current_degradation.get
38
40
  end
39
41
 
40
42
  def degraded?(status_from: :current)
41
- @mutex.synchronize { degraded_unlocked?(status_from) }
43
+ degraded_from?(status_from)
42
44
  end
43
45
 
44
46
  def last_callback_failure
45
- @mutex.synchronize { @last_callback_failure }
47
+ @last_callback_failure.get
46
48
  end
47
49
 
48
50
  def last_failure
49
- @mutex.synchronize { @last_failure }
51
+ @last_failure.get
50
52
  end
51
53
 
52
54
  def last_loss
53
- @mutex.synchronize { @last_loss }
54
- end
55
-
56
- def clear_degradation
57
- @mutex.synchronize { @current_degradation = nil }
55
+ @last_loss.get
58
56
  end
59
57
 
60
58
  def clear_degradation_if_unchanged(marker)
61
- @mutex.synchronize { @current_degradation = nil if @current_degradation.equal?(marker) }
59
+ @current_degradation.compare_and_set(marker, nil)
62
60
  end
63
61
 
64
62
  def clear_failures!
65
- @mutex.synchronize do
66
- @current_degradation = nil
67
- @last_callback_failure = nil
68
- @last_failure = nil
69
- @last_loss = nil
70
- end
63
+ @current_degradation.set(nil)
64
+ @last_callback_failure.set(nil)
65
+ @last_failure.set(nil)
66
+ @last_loss.set(nil)
71
67
  self
72
68
  end
73
69
 
74
70
  def record_failure(error, callback: nil, counter: @failure_counter, degrade: true, **metadata)
75
71
  failure = FailureSnapshot.build(error, **metadata)
76
- @mutex.synchronize do
77
- increment_unlocked(:failures) if @track_failures
78
- increment_unlocked(counter) if counter && counter != :failures && @counts.key?(counter)
79
- @last_failure = failure
80
- @current_degradation = failure if degrade
81
- end
72
+ increment(:failures) if @track_failures
73
+ increment(counter) if counter && !counter.equal?(:failures) && @counts.key?(counter)
74
+ @last_failure.set(failure)
75
+ @current_degradation.set(failure) if degrade
82
76
  notify_failure_callback(callback, error, metadata)
83
77
  failure
84
78
  end
85
79
 
86
80
  def record_callback_failure(callback_failure)
87
- @mutex.synchronize do
88
- @last_callback_failure = callback_failure.to_h
89
- increment_unlocked(@callback_failure_counter) if @callback_failure_counter
90
- end
81
+ @last_callback_failure.set(callback_failure.to_h)
82
+ increment(@callback_failure_counter) if @callback_failure_counter
91
83
  end
92
84
 
93
85
  def record_loss(reason:, counter: reason, degrade: true, **metadata)
94
86
  loss = { reason: reason }.merge(metadata).compact.freeze
95
- @mutex.synchronize do
96
- increment_unlocked(counter) if counter && @counts.key?(counter)
97
- @last_loss = loss
98
- @current_degradation = loss if degrade
99
- end
87
+ increment(counter) if counter && @counts.key?(counter)
88
+ @last_loss.set(loss)
89
+ @current_degradation.set(loss) if degrade
100
90
  loss
101
91
  end
102
92
 
103
93
  def record_success
104
- @mutex.synchronize { @current_degradation = nil }
94
+ @current_degradation.set(nil)
105
95
  self
106
96
  end
107
97
 
108
98
  def snapshot(status: nil, status_from: :current, include_loss: false, **fields)
109
- @mutex.synchronize do
110
- result = {
111
- counts: @counts.dup.freeze,
112
- last_failure: @last_failure,
113
- status: status || (degraded_unlocked?(status_from) ? :degraded : :ok)
114
- }
115
- result[:last_loss] = @last_loss if include_loss
116
- result.merge(fields).compact.freeze
117
- end
99
+ result = {
100
+ counts: counts,
101
+ last_failure: last_failure,
102
+ status: status || (degraded_from?(status_from) ? :degraded : :ok)
103
+ }
104
+ result[:last_loss] = last_loss if include_loss
105
+ result.merge(fields).compact.freeze
118
106
  end
119
107
 
120
108
  private
@@ -124,16 +112,12 @@ module Julewire
124
112
  record_callback_failure(callback_result) if CallbackNotifier.failure?(callback_result)
125
113
  end
126
114
 
127
- def increment_unlocked(key, by: 1)
128
- @counts[key] = @counts.fetch(key) + by
129
- end
130
-
131
- def degraded_unlocked?(status_from)
115
+ def degraded_from?(status_from)
132
116
  case status_from
133
117
  when :current
134
- !!@current_degradation
118
+ !!degradation_marker
135
119
  when :failure_or_loss
136
- !!(@last_failure || @last_loss)
120
+ !!(last_failure || last_loss)
137
121
  else
138
122
  raise ArgumentError, "unknown health status source: #{status_from.inspect}"
139
123
  end
@@ -1,60 +1,47 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "concurrent/map"
4
+
3
5
  module Julewire
4
6
  module Core
5
7
  module Diagnostics
6
8
  class IntegrationHealthStore
7
9
  def initialize
8
- @mutex = Mutex.new
9
- @entries = {}
10
+ @entries = Concurrent::Map.new
10
11
  end
11
12
 
12
13
  def record_failure(integration, error, **metadata)
13
14
  name = normalize_name(integration)
14
15
  metadata = { phase: :integration, integration: name }.merge(metadata)
15
- @mutex.synchronize do
16
- entry_for(name).record_failure(error, **metadata)
17
- end
18
- nil
19
- rescue StandardError
16
+ entry_for(name).record_failure(error, **metadata)
20
17
  nil
21
18
  end
22
19
 
23
20
  def record_success(integration)
24
21
  name = normalize_name(integration)
25
- @mutex.synchronize do
26
- entry_for(name).record_success
27
- end
28
- nil
29
- rescue StandardError
22
+ entry_for(name).record_success
30
23
  nil
31
24
  end
32
25
 
33
26
  def health
34
- @mutex.synchronize do
35
- @entries.to_h { |name, entry| [name, entry.snapshot] }.freeze
36
- end
27
+ @entries.each_pair.with_object({}) do |(name, entry), snapshot|
28
+ snapshot[name] = entry.snapshot
29
+ end.freeze
37
30
  end
38
31
 
39
32
  def reset!
40
- @mutex.synchronize { @entries.clear }
41
- nil
42
- end
43
-
44
- def after_fork!
45
- @mutex = Mutex.new
46
- @entries = {}
33
+ @entries.clear
47
34
  nil
48
35
  end
49
36
 
50
37
  private
51
38
 
52
39
  def entry_for(name)
53
- @entries[name] ||= Health.new(counter_keys: [:failures])
40
+ @entries.compute_if_absent(name) { Health.new(counter_keys: []) }
54
41
  end
55
42
 
56
43
  def normalize_name(value)
57
- Core.normalize_name(value, name: :integration)
44
+ Core.normalize_name(value)
58
45
  rescue StandardError
59
46
  :unknown
60
47
  end