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
@@ -5,11 +5,10 @@ module Julewire
5
5
  module Diagnostics
6
6
  module InternalRecords
7
7
  class << self
8
- def emit_error(error, error_backtrace_lines:)
9
- Core::Records::Draft.build(
8
+ def emit_error(error)
9
+ Records::Draft.build(
10
10
  {
11
11
  severity: :error,
12
- kind: :point,
13
12
  event: "julewire.emit_error",
14
13
  source: "julewire",
15
14
  message: "Julewire emit failed",
@@ -17,17 +16,15 @@ module Julewire
17
16
  error: failure_details(error)
18
17
  }
19
18
  },
20
- context: {},
21
- scope: nil,
22
- error_backtrace_lines: error_backtrace_lines
19
+ context: nil,
20
+ scope: nil
23
21
  )
24
22
  end
25
23
 
26
- def processor_error(processor_name:, error:, record_metadata:, error_backtrace_lines:)
27
- Core::Records::Draft.build(
24
+ def processor_error(processor_name:, error:, record_metadata:)
25
+ Records::Draft.build(
28
26
  {
29
27
  severity: :error,
30
- kind: :point,
31
28
  event: "julewire.processor_error",
32
29
  source: "julewire",
33
30
  message: "Julewire processor failed",
@@ -38,9 +35,8 @@ module Julewire
38
35
  record: record_metadata
39
36
  }
40
37
  },
41
- context: {},
42
- scope: nil,
43
- error_backtrace_lines: error_backtrace_lines
38
+ context: nil,
39
+ scope: nil
44
40
  )
45
41
  end
46
42
 
@@ -1,64 +1,41 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "concurrent/atomic/atomic_boolean"
4
+ require "concurrent/atomic/atomic_fixnum"
5
+
3
6
  module Julewire
4
7
  module Core
5
8
  module Diagnostics
6
9
  module InvalidSeverityReporter
7
- @warned = false
8
- @mutex = Mutex.new
10
+ @warned = Concurrent::AtomicBoolean.new
9
11
 
10
12
  class RuntimeCounter
11
13
  def initialize
12
- @mutex = Mutex.new
13
- @count = 0
14
- @last = nil
14
+ @count = Concurrent::AtomicFixnum.new
15
15
  end
16
16
 
17
- def call(value, source: nil, event: nil)
18
- metadata = InvalidSeverityReporter.metadata(value, source: source, event: event)
19
- @mutex.synchronize do
20
- @count += 1
21
- @last = metadata
22
- end
17
+ def call(value)
18
+ metadata = InvalidSeverityReporter.metadata(value)
19
+ @count.increment
23
20
  InvalidSeverityReporter.warn_once(metadata)
24
21
  rescue StandardError
25
22
  nil
26
23
  end
27
24
 
28
25
  def health
29
- @mutex.synchronize do
30
- {
31
- count: @count,
32
- last_event: @last&.fetch(:event, nil),
33
- last_source: @last&.fetch(:source, nil),
34
- last_value_class: @last&.fetch(:value_class, nil)
35
- }.compact
36
- end
26
+ { count: @count.value }
37
27
  end
38
28
 
39
29
  def reset!
40
- @mutex.synchronize do
41
- @count = 0
42
- @last = nil
43
- end
44
- nil
45
- end
46
-
47
- def reset_after_fork!
48
- @mutex = Mutex.new
49
- @count = 0
50
- @last = nil
51
- nil
30
+ @count.value = 0
52
31
  end
53
32
  end
54
33
 
55
34
  private_constant :RuntimeCounter
56
35
 
57
36
  class << self
58
- def call(value, source: nil, event: nil)
59
- warning_only.call(value, source: source, event: event)
60
- rescue StandardError
61
- nil
37
+ def call(value)
38
+ warning_only.call(value)
62
39
  end
63
40
 
64
41
  def counter = RuntimeCounter.new
@@ -71,22 +48,11 @@ module Julewire
71
48
  end
72
49
 
73
50
  def reset!
74
- @mutex.synchronize { @warned = false }
75
- nil
76
- end
77
-
78
- def reset_after_fork!
79
- @mutex = Mutex.new
80
- @warned = false
81
- nil
51
+ @warned.make_false
82
52
  end
83
53
 
84
- def metadata(value, source:, event:)
85
- {
86
- event: event,
87
- source: source,
88
- value_class: value.class.name || value.class.to_s
89
- }.compact.freeze
54
+ def metadata(value)
55
+ { value_class: value.class.to_s }.freeze
90
56
  rescue StandardError
91
57
  { value_class: "unknown" }.freeze
92
58
  end
@@ -98,12 +64,7 @@ module Julewire
98
64
  end
99
65
 
100
66
  def first_warning?
101
- @mutex.synchronize do
102
- return false if @warned
103
-
104
- @warned = true
105
- true
106
- end
67
+ @warned.make_true
107
68
  end
108
69
  end
109
70
  end
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "concurrent/atomic/atomic_boolean"
4
+ require "concurrent/atomic/atomic_reference"
5
+
3
6
  module Julewire
4
7
  module Core
5
8
  module Diagnostics
@@ -39,31 +42,26 @@ module Julewire
39
42
  @interval = Validation.validate_integer_limit!(interval, name: :interval, positive: true)
40
43
  @include_ok = include_ok ? true : false
41
44
  @scheduler = scheduler
42
- @mutex = Mutex.new
43
- @last_signature = nil
44
- @last_failure = nil
45
- @started = false
46
- @stopped = false
47
- @token = nil
45
+ @last_failure = Concurrent::AtomicReference.new
46
+ @last_signature = Concurrent::AtomicReference.new
47
+ @running = Concurrent::AtomicBoolean.new
48
+ @schedule_mutex = Mutex.new
48
49
  @serializer_pool_key = :"julewire_core_meta_observer_serializers_#{object_id}"
49
50
  end
50
51
 
51
52
  def start!
52
- @mutex.synchronize do
53
- return self if @started && !@stopped
53
+ return self unless @running.make_true
54
54
 
55
- @started = true
56
- @stopped = false
57
- schedule_next
58
- end
55
+ schedule_next
59
56
  self
60
57
  end
61
58
 
62
59
  def stop!
63
- token = @mutex.synchronize do
64
- @stopped = true
65
- @started = false
66
- @token
60
+ token = @schedule_mutex.synchronize do
61
+ @running.make_false
62
+ token = @token
63
+ @token = nil
64
+ token
67
65
  end
68
66
  @scheduler.cancel(token) if token
69
67
  self
@@ -72,12 +70,7 @@ module Julewire
72
70
  def sample!
73
71
  health = @runtime.health
74
72
  signature = signature_for(health)
75
- changed = @mutex.synchronize do
76
- changed = signature != @last_signature
77
- @last_signature = signature
78
- changed
79
- end
80
- return false unless changed
73
+ return false if signature.eql?(@last_signature.get_and_set(signature))
81
74
  return false unless emit_health?(health)
82
75
 
83
76
  emit_health(health)
@@ -88,35 +81,38 @@ module Julewire
88
81
  end
89
82
 
90
83
  def health
91
- @mutex.synchronize do
92
- {
93
- event: @event,
94
- include_ok: @include_ok,
95
- interval: @interval,
96
- last_failure: @last_failure,
97
- observed_runtime: @runtime_name,
98
- running: @started && !@stopped,
99
- status: @last_failure ? :degraded : :ok,
100
- target_runtime: @target_name
101
- }.compact.freeze
102
- end
84
+ failure = @last_failure.get
85
+ {
86
+ event: @event,
87
+ include_ok: @include_ok,
88
+ interval: @interval,
89
+ last_failure: failure,
90
+ observed_runtime: @runtime_name,
91
+ running: @running.true?,
92
+ status: failure ? :degraded : :ok,
93
+ target_runtime: @target_name
94
+ }.compact.freeze
103
95
  end
104
96
 
105
97
  private
106
98
 
107
99
  def schedule_next
108
- @token = @scheduler.schedule(@interval) { scheduled_sample }
100
+ @schedule_mutex.synchronize do
101
+ @token = @scheduler.schedule(@interval) { scheduled_sample } if @running.true?
102
+ end
109
103
  end
110
104
 
111
105
  def scheduled_sample
106
+ return false unless @running.true?
107
+
112
108
  sample!
113
- @mutex.synchronize { schedule_next unless @stopped }
109
+ schedule_next
114
110
  rescue StandardError => e
115
111
  record_failure(e)
116
112
  end
117
113
 
118
114
  def emit_health?(health)
119
- @include_ok || health[:status] != :ok
115
+ @include_ok || !health[:status].eql?(:ok)
120
116
  end
121
117
 
122
118
  def emit_health(health)
@@ -137,23 +133,20 @@ module Julewire
137
133
  end
138
134
 
139
135
  def signature_for(health)
140
- serializer = cached_serializer
141
- return build_serializer.serialize(health).hash if serializer.in_use?
142
-
143
- serializer.serialize(health).hash
136
+ Serialization::SerializerPool.serialize_with(cached_serializer, health) { build_serializer }
144
137
  end
145
138
 
146
139
  def cached_serializer
147
- Serialization::SerializerPool.serializer(@serializer_pool_key, :signature) { build_serializer }
140
+ Serialization::SerializerPool.serializer(@serializer_pool_key, nil) { build_serializer }
148
141
  end
149
142
 
150
143
  def build_serializer
151
- Serialization::Serializer.new(compact_empty: true)
144
+ Serializer.new(compact_empty: true)
152
145
  end
153
146
 
154
147
  def record_failure(error)
155
148
  failure = FailureSnapshot.build(error, phase: :meta_observer)
156
- @mutex.synchronize { @last_failure = failure }
149
+ @last_failure.set(failure)
157
150
  end
158
151
  end
159
152
  end
@@ -14,11 +14,6 @@ module Julewire
14
14
  def health = @store.health
15
15
 
16
16
  def reset! = @store.reset!
17
-
18
- def after_fork!
19
- @store = IntegrationHealthStore.new
20
- nil
21
- end
22
17
  end
23
18
  end
24
19
  end
@@ -27,7 +27,7 @@ module Julewire
27
27
 
28
28
  def payload_for(entry)
29
29
  record = entry.record
30
- record.merge("timestamp" => record["timestamp"] || entry.at.iso8601(6))
30
+ record.merge("timestamp" => record["timestamp"] || entry.at)
31
31
  end
32
32
  end
33
33
  end
@@ -1,5 +1,7 @@
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 Diagnostics
@@ -8,6 +10,9 @@ module Julewire
8
10
  DEFAULT_NAME = :tail
9
11
  COUNTER_KEYS = %i[captured failures].freeze
10
12
  Entry = Data.define(:sequence, :at, :record)
13
+ State = Data.define(:sequence, :items)
14
+ EMPTY_ENTRIES = [].freeze
15
+ private_constant :State, :EMPTY_ENTRIES
11
16
 
12
17
  class << self
13
18
  def attach!(runtime = Julewire, **)
@@ -39,11 +44,10 @@ module Julewire
39
44
 
40
45
  def emit(record)
41
46
  snapshot = snapshot_record(record)
42
- @mutex.synchronize do
43
- @sequence += 1
44
- entry = Entry.new(@sequence, Time.now.utc, snapshot)
45
- @entries << entry
46
- @entries.shift while @entries.length > @capacity
47
+ @state_ref.update do |state|
48
+ sequence = state.sequence + 1
49
+ entries = (state.items + [Entry.new(sequence, Time.now.utc, snapshot)]).last(@capacity)
50
+ State.new(sequence, entries)
47
51
  end
48
52
  @health.increment(:captured)
49
53
  nil
@@ -54,7 +58,7 @@ module Julewire
54
58
 
55
59
  def entries(limit: nil)
56
60
  limit = normalize_limit(limit)
57
- snapshot = @mutex.synchronize { @entries.dup }
61
+ snapshot = @state_ref.get.items.dup
58
62
  limit ? snapshot.last(limit) : snapshot
59
63
  end
60
64
 
@@ -72,10 +76,8 @@ module Julewire
72
76
  end
73
77
 
74
78
  def clear
75
- @mutex.synchronize do
76
- @entries.clear
77
- end
78
- @health.clear_degraded!
79
+ @state_ref.update { State.new(it.sequence, EMPTY_ENTRIES) }
80
+ @health.clear_failures!
79
81
  self
80
82
  end
81
83
 
@@ -89,17 +91,14 @@ module Julewire
89
91
  end
90
92
 
91
93
  def health
92
- size = @mutex.synchronize { @entries.length }
93
- @health.snapshot(capacity: @capacity, size: size)
94
+ @health.snapshot(capacity: @capacity, size: entries.length)
94
95
  end
95
96
 
96
97
  private
97
98
 
98
99
  def initialize_state
99
- @mutex = Mutex.new
100
- @entries = []
100
+ @state_ref = Concurrent::AtomicReference.new(State.new(0, EMPTY_ENTRIES))
101
101
  @health = Integration::DestinationHealth.new(counter_keys: COUNTER_KEYS)
102
- @sequence = 0
103
102
  @serializer_mutex = Mutex.new
104
103
  end
105
104
 
@@ -123,7 +122,7 @@ module Julewire
123
122
  message = Records::DisplayMessage.call(record)
124
123
  return payload if blank?(message)
125
124
 
126
- payload = payload.dup if payload.frozen?
125
+ payload = payload.dup
127
126
  payload["message"] = message
128
127
  payload
129
128
  end
@@ -131,10 +130,7 @@ module Julewire
131
130
  def serialize_payload(payload)
132
131
  return serialize_custom_payload(payload) if @serializer
133
132
 
134
- serializer = cached_serializer
135
- return build_serializer.serialize(payload) if serializer.in_use?
136
-
137
- serializer.serialize(payload)
133
+ Serialization::SerializerPool.serialize_with(cached_serializer, payload) { build_serializer }
138
134
  end
139
135
 
140
136
  def serialize_custom_payload(payload)
@@ -142,11 +138,11 @@ module Julewire
142
138
  end
143
139
 
144
140
  def cached_serializer
145
- Serialization::SerializerPool.serializer(@serializer_pool_key, :default) { build_serializer }
141
+ Serialization::SerializerPool.serializer(@serializer_pool_key, nil) { build_serializer }
146
142
  end
147
143
 
148
144
  def build_serializer
149
- Serialization::Serializer.new(compact_empty: true)
145
+ Serializer.new(compact_empty: true)
150
146
  end
151
147
 
152
148
  def blank?(value)
@@ -74,15 +74,11 @@ module Julewire
74
74
 
75
75
  EXECUTION_OPTION_DEFAULTS.merge(options).tap do |normalized|
76
76
  normalized[:execution] = execution_fields(normalized.delete(:fields))
77
- normalized[:attributes] ||= EMPTY_HASH
78
- normalized[:neutral] ||= EMPTY_HASH
79
- normalized[:labels] ||= EMPTY_HASH
80
77
  end
81
78
  end
82
79
 
83
80
  def execution_fields(fields)
84
- return {} if fields.nil?
85
- raise ArgumentError, "execution fields must be a Hash" unless fields.is_a?(Hash)
81
+ raise ArgumentError, "execution fields must be a Hash" unless fields.nil? || fields.is_a?(Hash)
86
82
 
87
83
  fields
88
84
  end
@@ -91,9 +87,7 @@ module Julewire
91
87
  return unless emit_summary_enabled
92
88
 
93
89
  @summary_finalizer ||= lambda do |scope|
94
- next if suppress_summary_for_non_standard_exception?(scope)
95
-
96
- @emit_summary_record.call(scope)
90
+ @emit_summary_record.call(scope) unless suppress_summary_for_non_standard_exception?(scope)
97
91
  end
98
92
  end
99
93
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "concurrent/atomic/atomic_boolean"
4
+
3
5
  module Julewire
4
6
  module Core
5
7
  module Execution
@@ -8,12 +10,9 @@ module Julewire
8
10
  @scope = scope
9
11
  @on_finish = on_finish
10
12
  @on_finish_failure = on_finish_failure
11
- @mutex = Mutex.new
12
- @finished = false
13
+ @finished = Concurrent::AtomicBoolean.new
13
14
  end
14
15
 
15
- attr_reader :scope
16
-
17
16
  def snapshot = View.new(@scope)
18
17
 
19
18
  def run
@@ -32,42 +31,40 @@ module Julewire
32
31
  ContextStore.current.with_scope(@scope, &)
33
32
  end
34
33
 
35
- def finish(reason: :closed, fields: {}, attributes: {}, error: nil, severity: nil)
34
+ def finish(reason: :closed, fields: nil, attributes: nil, error: nil, severity: nil)
35
+ # Finishing is one-shot: retrying after partial summary mutation can
36
+ # duplicate completion data, so failures are reported and stop here.
36
37
  return false unless mark_finished
37
38
 
38
39
  add_completion_attributes(reason)
39
- @scope.add_summary(fields) unless fields.empty?
40
- @scope.add_summary_attributes(attributes) unless attributes.empty?
40
+ @scope.add_summary(fields)
41
+ @scope.add_summary_attributes(attributes)
41
42
  @scope.finish_owned(error: error, severity: severity)
42
43
  call_finish
43
44
  true
44
45
  rescue StandardError => e
45
- report_finish_failure(e)
46
+ report_finish_failure(e, phase: :summary_finish)
46
47
  false
47
48
  end
48
49
 
49
50
  private
50
51
 
51
52
  def mark_finished
52
- @mutex.synchronize do
53
- return false if @finished
54
-
55
- @finished = true
56
- end
53
+ @finished.make_true
57
54
  end
58
55
 
59
56
  def add_completion_attributes(reason)
60
- @scope.add_summary_attributes({ "julewire.completion": reason.to_s }, owned: true)
57
+ @scope.add_summary_attributes({ "julewire.completion": reason.to_s })
61
58
  end
62
59
 
63
60
  def call_finish
64
61
  @on_finish&.call(@scope)
65
62
  rescue StandardError => e
66
- report_finish_failure(e)
63
+ report_finish_failure(e, phase: :summary_emit)
67
64
  end
68
65
 
69
- def report_finish_failure(error)
70
- @on_finish_failure&.call(error)
66
+ def report_finish_failure(error, phase:)
67
+ @on_finish_failure.call(error, phase: phase)
71
68
  rescue StandardError
72
69
  nil
73
70
  end