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
@@ -3,129 +3,123 @@
3
3
  module Julewire
4
4
  module Core
5
5
  module Serialization
6
+ module ValueCopyTruncation
7
+ private
8
+
9
+ def validate_optional_limit(value, name:)
10
+ return unless value
11
+
12
+ Validation.validate_integer_limit!(value, name: name)
13
+ end
14
+
15
+ def finish_hash(result, fields)
16
+ add_truncation_metadata!(result, fields)
17
+ finish_container(result, fields)
18
+ end
19
+
20
+ def finish_array(result, fields)
21
+ if @track_truncation && fields
22
+ result << freeze_container({ Serializer::TRUNCATION_METADATA_KEY.to_sym => truncation_metadata(fields) })
23
+ end
24
+ finish_container(result, fields)
25
+ end
26
+
27
+ def add_truncation_metadata!(result, fields)
28
+ return unless @track_truncation && fields
29
+
30
+ result[Serializer::TRUNCATION_METADATA_KEY.to_sym] = truncation_metadata(fields)
31
+ end
32
+
33
+ def finish_container(result, fields)
34
+ value = freeze_container(result)
35
+ fields ? mark_truncated(value) : value
36
+ end
37
+
38
+ def truncation_metadata(fields)
39
+ TruncationMetadata.build(
40
+ fields,
41
+ key_style: :symbol,
42
+ compact_limits: true,
43
+ freeze_values: @freeze_values,
44
+ max_array_items: @max_array_items,
45
+ max_depth: @max_depth,
46
+ max_hash_keys: @max_hash_keys,
47
+ max_string_bytes: @max_string_bytes
48
+ )
49
+ end
50
+
51
+ def mark_truncated(value)
52
+ @last_truncated = true
53
+ value
54
+ end
55
+
56
+ def consume_truncated
57
+ truncated = @last_truncated
58
+ @last_truncated = false
59
+ truncated
60
+ end
61
+
62
+ def append_truncation_field(fields, field)
63
+ TruncationMetadata.append_field(fields, field)
64
+ end
65
+ end
66
+ private_constant :ValueCopyTruncation
67
+
6
68
  class ValueCopy
7
69
  include ValueTraversal
70
+ include ValueCopyTruncation
8
71
 
9
72
  CIRCULAR_REFERENCE = Core::CIRCULAR_REFERENCE
10
73
  EMPTY_ARRAY = [].freeze
11
74
  EMPTY_HASH = {}.freeze
12
- POOL_KEY = :julewire_core_value_copy_pool
13
- private_constant :EMPTY_ARRAY, :EMPTY_HASH, :POOL_KEY
75
+ private_constant :EMPTY_ARRAY, :EMPTY_HASH
14
76
 
15
77
  class << self
16
- def call(
78
+ def call( # rubocop:disable Metrics/ParameterLists
17
79
  value,
18
80
  compact_empty: false,
19
81
  freeze_values: false,
20
- max_depth: Core::NORMALIZATION_MAX_DEPTH,
82
+ max_array_items: nil,
83
+ max_depth: NORMALIZATION_MAX_DEPTH,
84
+ max_hash_keys: nil,
85
+ max_string_bytes: nil,
86
+ preserve_truncation_metadata: false,
21
87
  symbolize_keys: false
22
88
  )
23
- return copy_leaf(value, freeze_values: freeze_values) unless container?(value)
24
-
25
- copy_with(
26
- cached_copier(
27
- compact_empty: compact_empty,
28
- freeze_values: freeze_values,
29
- max_depth: max_depth,
30
- symbolize_keys: symbolize_keys
31
- ),
32
- value
33
- )
34
- end
35
-
36
- def omitted_empty?(value)
37
- value.nil? || (value.is_a?(Hash) && value.empty?) || (value.is_a?(Array) && value.empty?)
38
- end
39
-
40
- private
41
-
42
- def container?(value) = value.is_a?(Hash) || value.is_a?(Array)
43
-
44
- def cached_copier(compact_empty:, freeze_values:, max_depth:, symbolize_keys:)
45
- # One copier per thread/options avoids per-record walker allocation.
46
- pool = Thread.current.thread_variable_get(POOL_KEY)
47
- unless pool
48
- pool = {}
49
- Thread.current.thread_variable_set(POOL_KEY, pool)
50
- end
51
-
52
- key = cache_key(
53
- compact_empty: compact_empty,
54
- freeze_values: freeze_values,
55
- max_depth: max_depth,
56
- symbolize_keys: symbolize_keys
57
- )
58
- pool[key] ||= new(
59
- compact_empty: compact_empty,
60
- freeze_values: freeze_values,
61
- max_depth: max_depth,
62
- symbolize_keys: symbolize_keys
63
- )
64
- end
65
-
66
- def cache_key(compact_empty:, freeze_values:, max_depth:, symbolize_keys:)
67
- depth_key = max_depth || -1
68
- flags = 0
69
- flags |= 1 if compact_empty
70
- flags |= 2 if freeze_values
71
- flags |= 4 if symbolize_keys
72
- (depth_key << 3) | flags
73
- end
74
-
75
- def copy_with(copier, value)
76
- return copier.call_reusable(value) unless copier.in_use?
77
-
78
89
  new(
79
- compact_empty: copier.compact_empty,
80
- freeze_values: copier.freeze_values,
81
- max_depth: copier.max_depth,
82
- symbolize_keys: copier.symbolize_keys
90
+ compact_empty:,
91
+ freeze_values:,
92
+ max_array_items:,
93
+ max_depth:,
94
+ max_hash_keys:,
95
+ max_string_bytes:,
96
+ preserve_truncation_metadata:,
97
+ symbolize_keys:
83
98
  ).call(value)
84
99
  end
85
100
 
86
- def copy_leaf(value, freeze_values:)
87
- return copy_string(value, freeze_values: freeze_values) if value.is_a?(String)
88
- return copy_time(value, freeze_values: freeze_values) if value.is_a?(Time)
89
-
90
- value
91
- end
92
-
93
- def copy_string(value, freeze_values:)
94
- copy = value.frozen? ? value : value.dup
95
- freeze_values ? copy.freeze : copy
96
- end
97
-
98
- def copy_time(value, freeze_values:)
99
- return value unless freeze_values
100
- return value if value.frozen?
101
-
102
- value.dup.freeze
101
+ def omitted_empty?(value)
102
+ value.nil? || (value.is_a?(Hash) && value.empty?) || (value.is_a?(Array) && value.empty?)
103
103
  end
104
104
  end
105
105
 
106
- attr_reader :compact_empty, :freeze_values, :max_depth, :symbolize_keys
107
-
108
- def initialize(compact_empty:, freeze_values:, max_depth:, symbolize_keys:)
106
+ def initialize(compact_empty:, freeze_values:, max_array_items:, max_depth:, max_hash_keys:, max_string_bytes:,
107
+ preserve_truncation_metadata:, symbolize_keys:)
109
108
  @compact_empty = compact_empty
110
109
  @freeze_values = freeze_values
110
+ @max_array_items = validate_optional_limit(max_array_items, name: :max_array_items)
111
111
  @max_depth = max_depth
112
+ @max_hash_keys = validate_optional_limit(max_hash_keys, name: :max_hash_keys)
113
+ @max_string_bytes = validate_optional_limit(max_string_bytes, name: :max_string_bytes)
114
+ @preserve_truncation_metadata = preserve_truncation_metadata
112
115
  @symbolize_keys = symbolize_keys
113
- @in_use = false
116
+ @track_truncation = @max_array_items || @max_hash_keys || @max_string_bytes
114
117
  end
115
118
 
116
119
  def call(value)
117
120
  traverse(value) { |root, depth| copy_value(root, depth) }
118
121
  end
119
122
 
120
- def call_reusable(value)
121
- @in_use = true
122
- call(value)
123
- ensure
124
- @in_use = false
125
- end
126
-
127
- def in_use? = @in_use
128
-
129
123
  private
130
124
 
131
125
  def copy_value(value, depth)
@@ -137,7 +131,7 @@ module Julewire
137
131
  end
138
132
 
139
133
  def copy_container(value, depth)
140
- return copy_string(Serializer::MAX_DEPTH_VALUE) if depth_limited?(depth)
134
+ return mark_truncated(Serializer::MAX_DEPTH_VALUE) if depth == @max_depth
141
135
  return frozen_empty_container(value) if @freeze_values && value.empty?
142
136
 
143
137
  with_traversal_container(value, CIRCULAR_REFERENCE) do
@@ -145,52 +139,132 @@ module Julewire
145
139
  end
146
140
  end
147
141
 
148
- def depth_limited?(depth)
149
- @max_depth && depth >= @max_depth
150
- end
151
-
152
142
  def frozen_empty_container(value)
153
143
  value.is_a?(Hash) ? EMPTY_HASH : EMPTY_ARRAY
154
144
  end
155
145
 
156
146
  def copy_hash(value, depth)
147
+ fields = nil
157
148
  result = {}
149
+ visited = 0
158
150
  value.each do |key, item|
151
+ if visited == @max_hash_keys
152
+ fields = append_truncation_field(fields, "hash_keys")
153
+ break
154
+ end
155
+
156
+ visited += 1
159
157
  next if @compact_empty && self.class.omitted_empty?(item)
160
158
 
161
- copied = copy_value(item, depth + 1)
162
- next if @compact_empty && self.class.omitted_empty?(copied)
159
+ fields = copy_hash_entry(result, fields, key, item, depth)
160
+ end
161
+ finish_hash(result, fields)
162
+ end
163
163
 
164
- result[copy_key(key)] = copied
164
+ def copy_hash_entry(result, fields, key, item, depth)
165
+ return copy_truncation_metadata_entry(result, fields, key, item) if reserved_truncation_key?(key)
166
+
167
+ validate_symbolized_key_shape!(key)
168
+ copied = copy_value(item, depth + 1)
169
+ return fields if @compact_empty && self.class.omitted_empty?(copied)
170
+
171
+ copied_key = copied_key_value(key)
172
+ value_or_key_truncated = consume_truncated
173
+ result[copied_key] = copied
174
+ fields = append_truncation_field(fields, copied_key.to_s) if value_or_key_truncated
175
+ fields
176
+ end
177
+
178
+ def copy_truncation_metadata_entry(result, fields, key, item)
179
+ unless @preserve_truncation_metadata &&
180
+ allowed_truncation_metadata_key?(key) &&
181
+ TruncationMetadata.valid?(item, max_fields: truncation_metadata_field_limit)
182
+ raise_reserved_key!
165
183
  end
166
- freeze_container(result)
184
+
185
+ result[copy_truncation_metadata_key(key)] = TruncationMetadata.copy(
186
+ item,
187
+ freeze_values: @freeze_values
188
+ )
189
+ fields
190
+ end
191
+
192
+ def truncation_metadata_field_limit
193
+ @max_array_items || Serializer::DEFAULT_MAX_ARRAY_ITEMS
194
+ end
195
+
196
+ def allowed_truncation_metadata_key?(key)
197
+ key.instance_of?(Symbol) || @symbolize_keys
198
+ end
199
+
200
+ def reserved_truncation_key?(key)
201
+ key == Serializer::TRUNCATION_METADATA_KEY || key == Serializer::TRUNCATION_METADATA_KEY.to_sym
202
+ end
203
+
204
+ def copy_truncation_metadata_key(key)
205
+ key.to_sym
206
+ end
207
+
208
+ def validate_symbolized_key_shape!(key)
209
+ return unless @symbolize_keys
210
+ return if key.is_a?(String) || key.instance_of?(Symbol)
211
+
212
+ raise TypeError, Fields::Internal::FIELD_KEY_ERROR
167
213
  end
168
214
 
169
215
  def copy_array(value, depth)
170
216
  result = []
217
+ visited = 0
218
+ array_items_truncated = false
219
+ array_item_values_truncated = false
171
220
  value.each do |item|
221
+ if visited == @max_array_items
222
+ array_items_truncated = true
223
+ break
224
+ end
225
+
226
+ visited += 1
172
227
  next if @compact_empty && self.class.omitted_empty?(item)
173
228
 
174
229
  copied = copy_value(item, depth + 1)
230
+ child_truncated = consume_truncated
175
231
  next if @compact_empty && self.class.omitted_empty?(copied)
176
232
 
177
233
  result << copied
234
+ array_item_values_truncated ||= child_truncated
178
235
  end
179
- freeze_container(result)
236
+
237
+ finish_array(result, array_truncation_fields(array_items_truncated, array_item_values_truncated))
238
+ end
239
+
240
+ def array_truncation_fields(array_items_truncated, array_item_values_truncated)
241
+ return unless array_items_truncated || array_item_values_truncated
242
+
243
+ fields = []
244
+ fields << "array_item_values" if array_item_values_truncated
245
+ fields << "array_items" if array_items_truncated
246
+ fields
180
247
  end
181
248
 
182
- def copy_key(key)
183
- return key.to_sym if @symbolize_keys && key.is_a?(String)
184
- return copy_string(key) if key.is_a?(String)
249
+ def copied_key_value(key)
250
+ return key unless key.is_a?(String)
185
251
 
186
- key
252
+ copy = copy_string(key)
253
+ @symbolize_keys ? copy.to_sym : copy
254
+ end
255
+
256
+ def raise_reserved_key!
257
+ raise ArgumentError, "#{Serializer::TRUNCATION_METADATA_KEY} is reserved for Julewire truncation metadata"
187
258
  end
188
259
 
189
260
  def copy_string(value)
190
- return value unless value.is_a?(String)
261
+ if @max_string_bytes && value.bytesize > @max_string_bytes
262
+ copy = "#{value.byteslice(0, @max_string_bytes).scrub("?")}#{Serializer::TRUNCATED_SUFFIX}"
263
+ return mark_truncated(freeze_container(copy))
264
+ end
191
265
 
192
266
  copy = value.frozen? ? value : value.dup
193
- @freeze_values ? copy.freeze : copy
267
+ freeze_container(copy)
194
268
  end
195
269
 
196
270
  def copy_time(value)
@@ -5,142 +5,25 @@ module Julewire
5
5
  module Serialization
6
6
  module ValueTraversal
7
7
  def traverse(value)
8
- previous_seen = @traversal_seen
9
- previous_first_seen = @traversal_first_seen
10
- previous_second_seen = @traversal_second_seen
11
- previous_third_seen = @traversal_third_seen
12
- previous_fourth_seen = @traversal_fourth_seen
13
- @traversal_seen = nil
14
- @traversal_first_seen = nil
15
- @traversal_second_seen = nil
16
- @traversal_third_seen = nil
17
- @traversal_fourth_seen = nil
18
8
  yield(value, 0)
19
- ensure
20
- @traversal_seen = previous_seen
21
- @traversal_first_seen = previous_first_seen
22
- @traversal_second_seen = previous_second_seen
23
- @traversal_third_seen = previous_third_seen
24
- @traversal_fourth_seen = previous_fourth_seen
25
9
  end
26
10
 
27
11
  private
28
12
 
29
- def with_traversal_container(value, circular_value)
30
- added = false
31
- seen = @traversal_seen
32
- first_seen = @traversal_first_seen
33
- second_seen = @traversal_second_seen
34
- third_seen = @traversal_third_seen
35
- fourth_seen = @traversal_fourth_seen
36
- return circular_value if seen&.key?(value) || first_seen.equal?(value) || second_seen.equal?(value) ||
37
- third_seen.equal?(value) || fourth_seen.equal?(value)
13
+ def with_traversal_container(value, circular_value, &)
14
+ seen = traversal_seen
15
+ return circular_value if seen.include?(value)
38
16
 
39
- added = mark_traversal_container(value, seen, first_seen, second_seen, third_seen, fourth_seen)
40
- yield
41
- ensure
42
- unmark_traversal_container(value, added)
43
- end
44
-
45
- def traversal_seen?(value)
46
- @traversal_seen&.key?(value) || @traversal_first_seen.equal?(value) ||
47
- @traversal_second_seen.equal?(value) || @traversal_third_seen.equal?(value) ||
48
- @traversal_fourth_seen.equal?(value)
49
- end
50
-
51
- def with_marked_traversal_container(value)
52
- added = mark_traversal_container(
53
- value,
54
- @traversal_seen,
55
- @traversal_first_seen,
56
- @traversal_second_seen,
57
- @traversal_third_seen,
58
- @traversal_fourth_seen
59
- )
60
- yield
61
- ensure
62
- unmark_traversal_container(value, added)
63
- end
64
-
65
- def mark_traversal_container(value, seen, first_seen, second_seen, third_seen, fourth_seen)
66
- # Most records visit only a handful of live containers. Keep those in
67
- # slots and allocate the identity hash only for genuinely deep walks.
68
- if seen
69
- seen[value] = true
70
- :hash
71
- elsif first_seen.nil?
72
- mark_first_seen(value)
73
- elsif second_seen.nil?
74
- mark_second_seen(value)
75
- elsif third_seen.nil?
76
- mark_third_seen(value)
77
- elsif fourth_seen.nil?
78
- mark_fourth_seen(value)
79
- else
80
- promote_traversal_seen(value, first_seen, second_seen, third_seen, fourth_seen)
81
- end
82
- end
83
-
84
- def unmark_traversal_container(value, added)
85
- case added
86
- when :hash
87
- @traversal_seen.delete(value)
88
- when :first, :second, :third, :fourth
89
- unmark_traversal_slot(value, added)
90
- end
91
- end
92
-
93
- def mark_first_seen(value)
94
- @traversal_first_seen = value
95
- :first
96
- end
97
-
98
- def mark_second_seen(value)
99
- @traversal_second_seen = value
100
- :second
101
- end
102
-
103
- def mark_third_seen(value)
104
- @traversal_third_seen = value
105
- :third
106
- end
107
-
108
- def mark_fourth_seen(value)
109
- @traversal_fourth_seen = value
110
- :fourth
111
- end
112
-
113
- def promote_traversal_seen(value, first_seen, second_seen, third_seen, fourth_seen)
114
- @traversal_seen = {}.compare_by_identity
115
- @traversal_seen[first_seen] = true
116
- @traversal_seen[second_seen] = true
117
- @traversal_seen[third_seen] = true
118
- @traversal_seen[fourth_seen] = true
119
- @traversal_seen[value] = true
120
- clear_traversal_slots!
121
- :hash
122
- end
123
-
124
- def unmark_traversal_slot(value, added)
125
- return @traversal_seen.delete(value) if @traversal_seen
126
-
127
- clear_traversal_slot(added)
128
- end
129
-
130
- def clear_traversal_slot(slot)
131
- case slot
132
- when :first then @traversal_first_seen = nil
133
- when :second then @traversal_second_seen = nil
134
- when :third then @traversal_third_seen = nil
135
- when :fourth then @traversal_fourth_seen = nil
17
+ seen.add(value)
18
+ begin
19
+ yield
20
+ ensure
21
+ seen.delete(value)
136
22
  end
137
23
  end
138
24
 
139
- def clear_traversal_slots!
140
- @traversal_first_seen = nil
141
- @traversal_second_seen = nil
142
- @traversal_third_seen = nil
143
- @traversal_fourth_seen = nil
25
+ def traversal_seen
26
+ @traversal_seen ||= Set.new.compare_by_identity
144
27
  end
145
28
  end
146
29
 
@@ -19,20 +19,15 @@ module Julewire
19
19
  nil
20
20
  end
21
21
 
22
- def flush(*)
23
- self
24
- end
25
-
26
- def close(*)
27
- self
28
- end
22
+ def flush(*) = self
23
+ def close(*) = self
29
24
 
30
25
  def health
31
- { status: :ok, counts: { captured: @records.size } }
26
+ { status: :ok, counts: { captured: records.size } }
32
27
  end
33
28
 
34
29
  def clear
35
- @records.clear
30
+ records.clear
36
31
  self
37
32
  end
38
33
  end
@@ -46,17 +41,16 @@ module Julewire
46
41
  end
47
42
 
48
43
  def write(value)
49
- @writes << value
44
+ writes << value
50
45
  value.bytesize
51
46
  end
52
47
 
53
48
  def flush = self
54
-
55
49
  def close = self
56
50
  end
57
51
 
58
52
  class << self
59
- def configure_capture_destination(runtime = Julewire, **)
53
+ def configure_capture_destination(runtime, **)
60
54
  destination = CaptureDestination.new(**)
61
55
  runtime.configure do |config|
62
56
  config.destinations.clear
@@ -70,53 +64,9 @@ module Julewire
70
64
  yield records if block_given?
71
65
  records
72
66
  end
73
-
74
- def unregister_destination(kind)
75
- Core::Destinations.__send__(:unregister, kind)
76
- end
77
-
78
- def reset_shared_scheduler
79
- Core::Scheduling::SharedScheduler.__send__(:reset_for_test!)
80
- end
81
-
82
- def with_overridden_singleton_method(receiver, method_name, replacement)
83
- singleton_class = class << receiver; self; end
84
- method_exists =
85
- singleton_class.method_defined?(method_name) || singleton_class.private_method_defined?(method_name)
86
- original = singleton_class.instance_method(method_name) if method_exists
87
- verbose = $VERBOSE
88
- $VERBOSE = nil
89
- singleton_class.define_method(method_name, replacement)
90
- yield
91
- ensure
92
- $VERBOSE = nil
93
- if original
94
- singleton_class.define_method(method_name, original)
95
- elsif singleton_class&.method_defined?(method_name) || singleton_class&.private_method_defined?(method_name)
96
- singleton_class.remove_method(method_name)
97
- end
98
- $VERBOSE = verbose
99
- end
100
-
101
- def nonblocking_queue_values(queue)
102
- values = []
103
- loop do
104
- values << queue.pop(true)
105
- rescue ThreadError
106
- return values
107
- end
108
- end
109
67
  end
110
68
  end
111
69
  end
112
70
 
113
71
  Testing = Core::Testing unless const_defined?(:Testing, false)
114
72
  end
115
-
116
- require_relative "testing/chaos"
117
- require_relative "testing/chaos/catalog"
118
- require_relative "testing/chaos/core_runtime"
119
- require_relative "testing/chaos/destination"
120
- require_relative "testing/chaos/emitter"
121
- require_relative "testing/chaos/raising_output"
122
- require_relative "testing/contracts"
@@ -7,9 +7,8 @@ module Julewire
7
7
  class << self
8
8
  def validate_byte_limit!(value, name:)
9
9
  return if value.nil?
10
+ return value if value.instance_of?(Integer) && value.positive?
10
11
 
11
- validate_integer_limit!(value, name: name, positive: true)
12
- rescue ArgumentError
13
12
  raise ArgumentError, "#{name} must be nil or a positive Integer"
14
13
  end
15
14
 
@@ -18,7 +17,7 @@ module Julewire
18
17
  end
19
18
 
20
19
  def validate_integer_limit!(value, name:, positive: false)
21
- return value if value.is_a?(Integer) && valid_integer_limit?(value, positive: positive)
20
+ return value if value.instance_of?(Integer) && valid_integer_limit?(value, positive: positive)
22
21
 
23
22
  qualifier = positive ? "positive" : "non-negative"
24
23
  raise ArgumentError, "#{name} must be a #{qualifier} Integer"
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Julewire
4
4
  module Core
5
- VERSION = "1.0.0"
5
+ VERSION = "1.1.0"
6
6
  end
7
7
  end
data/lib/julewire/core.rb CHANGED
@@ -6,7 +6,7 @@ module Julewire
6
6
  # Each gem extends the shared Julewire namespace.
7
7
  loader = Zeitwerk::Loader.for_gem_extension(self)
8
8
  loader.inflector.inflect("cli" => "CLI")
9
- loader.ignore("#{__dir__}/core/testing.rb", "#{__dir__}/core/testing")
9
+ loader.ignore("#{__dir__}/core/testing.rb")
10
10
  loader.setup
11
11
 
12
12
  module Core
@@ -25,7 +25,7 @@ module Julewire
25
25
 
26
26
  value.to_sym
27
27
  when Symbol
28
- raise ArgumentError, "#{name} must not be empty" if value.name.empty?
28
+ raise ArgumentError, "#{name} must not be empty" if value.empty?
29
29
 
30
30
  value
31
31
  else
@@ -54,11 +54,6 @@ module Julewire
54
54
  extend Core::FacadeMethods
55
55
 
56
56
  Core::RuntimeLocator.current = Core::Runtime.new
57
- Core.singleton_class.class_eval do
58
- define_method(:loader) { loader }
59
- private :loader
60
- end
61
-
62
57
  ConsoleFormatter = Core::Records::ConsoleFormatter
63
58
  JsonEncoder = Core::Serialization::JsonEncoder
64
59
  Match = Core::Processing::Match