julewire-core 1.0.0 → 1.0.1

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -0
  3. data/docs/advanced-configuration.md +2 -1
  4. data/docs/context-and-propagation.md +6 -4
  5. data/docs/contracts.md +2 -1
  6. data/docs/extensions-and-api.md +9 -6
  7. data/docs/internals.md +3 -3
  8. data/docs/outputs-and-lifecycle.md +5 -2
  9. data/docs/records-and-data-policy.md +2 -2
  10. data/lib/julewire/core/cli/log_formats/record_decoder.rb +1 -1
  11. data/lib/julewire/core/context_store.rb +11 -6
  12. data/lib/julewire/core/destinations/destination.rb +3 -3
  13. data/lib/julewire/core/destinations/synchronized_output.rb +51 -15
  14. data/lib/julewire/core/diagnostics/doctor.rb +11 -11
  15. data/lib/julewire/core/diagnostics/failure_snapshot.rb +3 -1
  16. data/lib/julewire/core/execution/handle.rb +2 -0
  17. data/lib/julewire/core/execution/lineage.rb +10 -6
  18. data/lib/julewire/core/execution/scope.rb +4 -4
  19. data/lib/julewire/core/execution/scope_fields.rb +2 -2
  20. data/lib/julewire/core/facade_methods.rb +2 -2
  21. data/lib/julewire/core/fields/field_set.rb +32 -6
  22. data/lib/julewire/core/fields/field_stack.rb +40 -14
  23. data/lib/julewire/core/fields/internal.rb +41 -13
  24. data/lib/julewire/core/fields/stack_set.rb +2 -2
  25. data/lib/julewire/core/integration/destination_health.rb +13 -2
  26. data/lib/julewire/core/propagation/carrier.rb +63 -9
  27. data/lib/julewire/core/propagation.rb +3 -2
  28. data/lib/julewire/core/records/draft.rb +12 -6
  29. data/lib/julewire/core/records/record.rb +43 -13
  30. data/lib/julewire/core/runtime.rb +30 -11
  31. data/lib/julewire/core/serialization/bounded_transform.rb +11 -0
  32. data/lib/julewire/core/serialization/bounded_traversal.rb +33 -27
  33. data/lib/julewire/core/serialization/serializer.rb +1 -1
  34. data/lib/julewire/core/serialization/truncation_metadata.rb +142 -0
  35. data/lib/julewire/core/serialization/value_copy.rb +292 -51
  36. data/lib/julewire/core/testing/contracts/integration.rb +1 -1
  37. data/lib/julewire/core/version.rb +1 -1
  38. metadata +2 -1
@@ -22,16 +22,13 @@ module Julewire
22
22
  max_hash_keys: DEFAULT_MAX_HASH_KEYS,
23
23
  max_string_bytes: DEFAULT_MAX_STRING_BYTES
24
24
  )
25
- {
26
- "truncated" => true,
27
- "truncated_fields" => Array(fields).uniq,
28
- "limits" => {
29
- "max_array_items" => max_array_items,
30
- "max_depth" => max_depth,
31
- "max_hash_keys" => max_hash_keys,
32
- "max_string_bytes" => max_string_bytes
33
- }
34
- }
25
+ TruncationMetadata.build(
26
+ fields,
27
+ max_array_items: max_array_items,
28
+ max_depth: max_depth,
29
+ max_hash_keys: max_hash_keys,
30
+ max_string_bytes: max_string_bytes
31
+ )
35
32
  end
36
33
  end
37
34
 
@@ -110,15 +107,17 @@ module Julewire
110
107
  fields = nil
111
108
  result = {}
112
109
  track_paths = @track_paths
110
+ visited = 0
113
111
  value.each do |raw_key, item|
114
- if result.length >= @max_hash_keys
112
+ if visited >= @max_hash_keys
115
113
  fields = append_truncation_field(fields, "hash_keys")
116
114
  break
117
115
  end
118
116
 
117
+ visited += 1
119
118
  child = walk_value(item, depth + 1, raw_key, track_paths ? path_for(path, raw_key) : nil)
120
119
  child_truncated = consume_truncated
121
- key = key_value(raw_key)
120
+ key = key_value(raw_key, depth, path)
122
121
  key_truncated = consume_truncated
123
122
  result[key] = child
124
123
  fields = record_hash_truncation(fields, raw_key, key, key_truncated, child_truncated)
@@ -130,19 +129,23 @@ module Julewire
130
129
  fields = nil
131
130
  result = {}
132
131
  track_paths = @track_paths
132
+ visited = 0
133
133
  value.each do |raw_key, item|
134
+ # The hash cap bounds visited input entries, not final output keys, so
135
+ # serialized-key collisions and compacted entries cannot hide work.
136
+ if visited >= @max_hash_keys
137
+ fields = append_truncation_field(fields, "hash_keys")
138
+ break
139
+ end
140
+
141
+ visited += 1
134
142
  next if raw_omitted_value?(item)
135
143
 
136
144
  child = walk_value(item, depth + 1, raw_key, track_paths ? path_for(path, raw_key) : nil)
137
145
  child_truncated = consume_truncated
138
146
  next if omitted_value?(child)
139
147
 
140
- if result.length >= @max_hash_keys
141
- fields = append_truncation_field(fields, "hash_keys")
142
- break
143
- end
144
-
145
- key = key_value(raw_key)
148
+ key = key_value(raw_key, depth, path)
146
149
  key_truncated = consume_truncated
147
150
  result[key] = child
148
151
  fields = record_hash_truncation(fields, raw_key, key, key_truncated, child_truncated)
@@ -159,12 +162,14 @@ module Julewire
159
162
  def walk_full_array(value, depth, path)
160
163
  fields = nil
161
164
  result = []
165
+ visited = 0
162
166
  value.each do |item|
163
- if result.length >= @max_array_items
167
+ if visited >= @max_array_items
164
168
  fields = append_truncation_field(fields, "array_items")
165
169
  break
166
170
  end
167
171
 
172
+ visited += 1
168
173
  child = walk_value(item, depth + 1, nil, path)
169
174
  child_truncated = consume_truncated
170
175
  result << child
@@ -176,18 +181,20 @@ module Julewire
176
181
  def walk_compact_array(value, depth, path)
177
182
  fields = nil
178
183
  result = []
184
+ visited = 0
179
185
  value.each do |item|
186
+ if visited >= @max_array_items
187
+ fields = append_truncation_field(fields, "array_items")
188
+ break
189
+ end
190
+
191
+ visited += 1
180
192
  next if raw_omitted_value?(item)
181
193
 
182
194
  child = walk_value(item, depth + 1, nil, path)
183
195
  child_truncated = consume_truncated
184
196
  next if omitted_value?(child)
185
197
 
186
- if result.length >= @max_array_items
187
- fields = append_truncation_field(fields, "array_items")
188
- break
189
- end
190
-
191
198
  result << child
192
199
  fields = append_truncation_field(fields, "array_items") if child_truncated
193
200
  end
@@ -198,7 +205,7 @@ module Julewire
198
205
 
199
206
  def raw_omitted_value?(_value) = false
200
207
 
201
- def key_value(key)
208
+ def key_value(key, _depth = nil, _path = nil)
202
209
  clear_truncated(key.is_a?(String) ? copy_string(key) : key)
203
210
  end
204
211
 
@@ -259,8 +266,7 @@ module Julewire
259
266
  end
260
267
 
261
268
  def append_truncation_field(fields, field)
262
- (fields ||= []) << field
263
- fields
269
+ TruncationMetadata.append_field(fields, field)
264
270
  end
265
271
 
266
272
  def path_for(parent_path, key)
@@ -152,7 +152,7 @@ module Julewire
152
152
 
153
153
  def raw_omitted_value?(value) = DeepCompactEmpty.omitted?(value)
154
154
 
155
- def key_value(key) = serialize_key(key)
155
+ def key_value(key, _depth = nil, _path = nil) = serialize_key(key)
156
156
 
157
157
  def error_value(error) = clear_truncated(unserializable_marker(error))
158
158
 
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ module Serialization
6
+ module TruncationMetadata
7
+ KEYS = {
8
+ string: {
9
+ truncated: "truncated",
10
+ truncated_fields: "truncated_fields",
11
+ limits: "limits",
12
+ max_array_items: "max_array_items",
13
+ max_depth: "max_depth",
14
+ max_hash_keys: "max_hash_keys",
15
+ max_string_bytes: "max_string_bytes"
16
+ }.freeze,
17
+ symbol: {
18
+ truncated: :truncated,
19
+ truncated_fields: :truncated_fields,
20
+ limits: :limits,
21
+ max_array_items: :max_array_items,
22
+ max_depth: :max_depth,
23
+ max_hash_keys: :max_hash_keys,
24
+ max_string_bytes: :max_string_bytes
25
+ }.freeze
26
+ }.freeze
27
+ METADATA_KEYS = KEYS.fetch(:symbol).values_at(:truncated, :truncated_fields, :limits).freeze
28
+ METADATA_KEY_NAMES = METADATA_KEYS.map(&:to_s).freeze
29
+ LIMIT_KEYS = KEYS.fetch(:symbol).values_at(
30
+ :max_array_items,
31
+ :max_depth,
32
+ :max_hash_keys,
33
+ :max_string_bytes
34
+ ).freeze
35
+ LIMIT_KEY_NAMES = LIMIT_KEYS.map(&:to_s).freeze
36
+ [KEYS, METADATA_KEYS, METADATA_KEY_NAMES, LIMIT_KEYS, LIMIT_KEY_NAMES].each do |constant|
37
+ ::Ractor.make_shareable(constant) if defined?(::Ractor) && ::Ractor.respond_to?(:make_shareable)
38
+ end
39
+ private_constant :KEYS, :METADATA_KEYS, :METADATA_KEY_NAMES, :LIMIT_KEYS, :LIMIT_KEY_NAMES
40
+
41
+ class << self
42
+ def build(fields, max_array_items:, max_depth:, max_hash_keys:, max_string_bytes:, key_style: :string,
43
+ compact_limits: false, freeze_values: false)
44
+ keys = KEYS.fetch(key_style)
45
+ limits = limits_hash(
46
+ keys,
47
+ max_array_items: max_array_items,
48
+ max_depth: max_depth,
49
+ max_hash_keys: max_hash_keys,
50
+ max_string_bytes: max_string_bytes,
51
+ compact_limits: compact_limits
52
+ )
53
+ metadata = {
54
+ keys.fetch(:truncated) => true,
55
+ keys.fetch(:truncated_fields) => field_list(fields),
56
+ keys.fetch(:limits) => limits
57
+ }
58
+ freeze_values ? deep_freeze(metadata, keys) : metadata
59
+ end
60
+
61
+ def append_field(fields, field)
62
+ fields ||= []
63
+ fields << field unless fields.include?(field)
64
+ fields
65
+ end
66
+
67
+ def valid?(value, max_fields: nil)
68
+ return false unless value.is_a?(Hash)
69
+ return false unless valid_top_level_keys?(value)
70
+ return false unless fetch_key(value, :truncated) == true
71
+
72
+ fields = fetch_key(value, :truncated_fields)
73
+ limits = fetch_key(value, :limits)
74
+ valid_fields?(fields, max_fields: max_fields) && valid_limits?(limits)
75
+ end
76
+
77
+ private
78
+
79
+ def field_list(fields)
80
+ Array(fields).uniq
81
+ end
82
+
83
+ def limits_hash(keys, max_array_items:, max_depth:, max_hash_keys:, max_string_bytes:, compact_limits:)
84
+ limits = {
85
+ keys.fetch(:max_array_items) => max_array_items,
86
+ keys.fetch(:max_depth) => max_depth,
87
+ keys.fetch(:max_hash_keys) => max_hash_keys,
88
+ keys.fetch(:max_string_bytes) => max_string_bytes
89
+ }
90
+ compact_limits ? limits.compact : limits
91
+ end
92
+
93
+ def deep_freeze(metadata, keys)
94
+ metadata.fetch(keys.fetch(:truncated_fields)).each(&:freeze)
95
+ metadata.fetch(keys.fetch(:truncated_fields)).freeze
96
+ metadata.fetch(keys.fetch(:limits)).freeze
97
+ metadata.freeze
98
+ end
99
+
100
+ def fetch_key(value, key)
101
+ return value[key] if value.key?(key)
102
+
103
+ value[key.to_s]
104
+ end
105
+
106
+ def valid_top_level_keys?(value)
107
+ return false if value.length > METADATA_KEYS.length * 2
108
+
109
+ value.keys.all? { known_key?(it, METADATA_KEYS, METADATA_KEY_NAMES) } &&
110
+ METADATA_KEYS.all? { value.key?(it) || value.key?(it.to_s) }
111
+ end
112
+
113
+ def valid_fields?(fields, max_fields:)
114
+ return false unless fields.is_a?(Array)
115
+
116
+ seen = 0
117
+ fields.each do |field|
118
+ seen += 1
119
+ return false if max_fields && seen > max_fields
120
+ return false unless field.is_a?(String) || field.is_a?(Symbol)
121
+ end
122
+ true
123
+ end
124
+
125
+ def valid_limits?(limits)
126
+ return false unless limits.is_a?(Hash)
127
+ return false if limits.length > LIMIT_KEYS.length * 2
128
+
129
+ limits.all? do |key, value|
130
+ known_key?(key, LIMIT_KEYS, LIMIT_KEY_NAMES) && (value.nil? || value.is_a?(Integer))
131
+ end
132
+ end
133
+
134
+ def known_key?(key, symbol_keys, string_keys)
135
+ symbol_keys.include?(key) || string_keys.include?(key)
136
+ end
137
+ end
138
+ end
139
+ private_constant :TruncationMetadata
140
+ end
141
+ end
142
+ end