julewire-core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +73 -0
- data/docs/advanced-configuration.md +66 -0
- data/docs/attribute-keys.md +74 -0
- data/docs/configuration.md +327 -0
- data/docs/context-and-propagation.md +353 -0
- data/docs/contracts.md +211 -0
- data/docs/development.md +49 -0
- data/docs/extensions-and-api.md +567 -0
- data/docs/health-schema.md +104 -0
- data/docs/instrumentation-cheatsheet.md +29 -0
- data/docs/internals.md +135 -0
- data/docs/outputs-and-lifecycle.md +206 -0
- data/docs/quickstart.md +133 -0
- data/docs/record-sources.md +17 -0
- data/docs/records-and-data-policy.md +230 -0
- data/docs/security-and-wire.md +45 -0
- data/docs/tail.md +91 -0
- data/exe/julewire +6 -0
- data/julewire-core.gemspec +41 -0
- data/lib/julewire/core/cli/doctor.rb +143 -0
- data/lib/julewire/core/cli/line_helpers.rb +77 -0
- data/lib/julewire/core/cli/log_formats/console_text.rb +25 -0
- data/lib/julewire/core/cli/log_formats/core_json_decoder.rb +46 -0
- data/lib/julewire/core/cli/log_formats/core_json_encoder.rb +21 -0
- data/lib/julewire/core/cli/log_formats/record_decoder.rb +39 -0
- data/lib/julewire/core/cli/log_formats.rb +123 -0
- data/lib/julewire/core/cli/tail.rb +153 -0
- data/lib/julewire/core/cli/transcode.rb +105 -0
- data/lib/julewire/core/cli.rb +73 -0
- data/lib/julewire/core/configuration.rb +99 -0
- data/lib/julewire/core/context_store.rb +384 -0
- data/lib/julewire/core/destinations/chaos_output.rb +91 -0
- data/lib/julewire/core/destinations/collection.rb +177 -0
- data/lib/julewire/core/destinations/definition.rb +125 -0
- data/lib/julewire/core/destinations/destination.rb +268 -0
- data/lib/julewire/core/destinations/registry.rb +81 -0
- data/lib/julewire/core/destinations/sink.rb +35 -0
- data/lib/julewire/core/destinations/synchronized_output.rb +57 -0
- data/lib/julewire/core/destinations/tail_sampling.rb +321 -0
- data/lib/julewire/core/destinations/write_step.rb +119 -0
- data/lib/julewire/core/destinations.rb +33 -0
- data/lib/julewire/core/diagnostics/callback_notifier.rb +63 -0
- data/lib/julewire/core/diagnostics/doctor.rb +114 -0
- data/lib/julewire/core/diagnostics/failure_snapshot.rb +39 -0
- data/lib/julewire/core/diagnostics/health.rb +144 -0
- data/lib/julewire/core/diagnostics/integration_health_store.rb +64 -0
- data/lib/julewire/core/diagnostics/internal_records.rb +61 -0
- data/lib/julewire/core/diagnostics/invalid_severity_reporter.rb +112 -0
- data/lib/julewire/core/diagnostics/meta_observer.rb +161 -0
- data/lib/julewire/core/diagnostics/process_integration_health.rb +26 -0
- data/lib/julewire/core/diagnostics/tail/renderer.rb +36 -0
- data/lib/julewire/core/diagnostics/tail.rb +168 -0
- data/lib/julewire/core/diagnostics.rb +8 -0
- data/lib/julewire/core/error.rb +7 -0
- data/lib/julewire/core/execution/boundary.rb +106 -0
- data/lib/julewire/core/execution/handle.rb +77 -0
- data/lib/julewire/core/execution/lineage.rb +192 -0
- data/lib/julewire/core/execution/measurement_handle.rb +28 -0
- data/lib/julewire/core/execution/no_current_error.rb +9 -0
- data/lib/julewire/core/execution/scope.rb +246 -0
- data/lib/julewire/core/execution/scope_fields.rb +76 -0
- data/lib/julewire/core/execution/scope_identity.rb +71 -0
- data/lib/julewire/core/execution/scope_snapshot.rb +92 -0
- data/lib/julewire/core/execution/summary_state.rb +206 -0
- data/lib/julewire/core/execution/view.rb +56 -0
- data/lib/julewire/core/facade_methods.rb +181 -0
- data/lib/julewire/core/fields/attribute_keys.rb +54 -0
- data/lib/julewire/core/fields/attributes_proxy.rb +11 -0
- data/lib/julewire/core/fields/bags.rb +123 -0
- data/lib/julewire/core/fields/carry_proxy.rb +22 -0
- data/lib/julewire/core/fields/context_proxy.rb +11 -0
- data/lib/julewire/core/fields/field_set.rb +78 -0
- data/lib/julewire/core/fields/field_stack.rb +269 -0
- data/lib/julewire/core/fields/internal/deletion.rb +68 -0
- data/lib/julewire/core/fields/internal.rb +87 -0
- data/lib/julewire/core/fields/lookup.rb +35 -0
- data/lib/julewire/core/fields/section_proxy.rb +88 -0
- data/lib/julewire/core/fields/stack_set.rb +69 -0
- data/lib/julewire/core/fields/static_labels.rb +43 -0
- data/lib/julewire/core/fields/summary_proxy.rb +62 -0
- data/lib/julewire/core/integration/configurable.rb +52 -0
- data/lib/julewire/core/integration/destination_health.rb +43 -0
- data/lib/julewire/core/integration/event_subscriber.rb +62 -0
- data/lib/julewire/core/integration/facade.rb +131 -0
- data/lib/julewire/core/integration/fork_hooks.rb +79 -0
- data/lib/julewire/core/integration/health.rb +41 -0
- data/lib/julewire/core/integration/ivar_state.rb +38 -0
- data/lib/julewire/core/integration/lifecycle.rb +22 -0
- data/lib/julewire/core/integration/scoped.rb +34 -0
- data/lib/julewire/core/integration/settings.rb +92 -0
- data/lib/julewire/core/integration/subscriber_install.rb +39 -0
- data/lib/julewire/core/integration/subscription.rb +29 -0
- data/lib/julewire/core/integration/values.rb +192 -0
- data/lib/julewire/core/lifecycle_error.rb +7 -0
- data/lib/julewire/core/local_storage.rb +91 -0
- data/lib/julewire/core/processing/level_threshold.rb +53 -0
- data/lib/julewire/core/processing/match.rb +74 -0
- data/lib/julewire/core/processing/pipeline.rb +360 -0
- data/lib/julewire/core/processing/processor_chain.rb +69 -0
- data/lib/julewire/core/processing/processor_registry.rb +115 -0
- data/lib/julewire/core/processing/processor_wrapper.rb +44 -0
- data/lib/julewire/core/processing/record_field_transform.rb +124 -0
- data/lib/julewire/core/processing/sampling.rb +109 -0
- data/lib/julewire/core/processing.rb +41 -0
- data/lib/julewire/core/propagation/carrier.rb +93 -0
- data/lib/julewire/core/propagation.rb +50 -0
- data/lib/julewire/core/records/console_formatter.rb +24 -0
- data/lib/julewire/core/records/deconstruct.rb +19 -0
- data/lib/julewire/core/records/display_message.rb +166 -0
- data/lib/julewire/core/records/draft.rb +576 -0
- data/lib/julewire/core/records/formatter.rb +14 -0
- data/lib/julewire/core/records/lazy_emit_input.rb +99 -0
- data/lib/julewire/core/records/metadata.rb +23 -0
- data/lib/julewire/core/records/public_projection.rb +51 -0
- data/lib/julewire/core/records/raw_input.rb +41 -0
- data/lib/julewire/core/records/record.rb +175 -0
- data/lib/julewire/core/records/severity.rb +44 -0
- data/lib/julewire/core/runtime.rb +515 -0
- data/lib/julewire/core/runtime_locator.rb +20 -0
- data/lib/julewire/core/runtime_registry.rb +48 -0
- data/lib/julewire/core/runtime_state.rb +39 -0
- data/lib/julewire/core/scheduling/deadline.rb +24 -0
- data/lib/julewire/core/scheduling/deadline_scheduler.rb +207 -0
- data/lib/julewire/core/scheduling/shared_scheduler.rb +48 -0
- data/lib/julewire/core/sentinel.rb +18 -0
- data/lib/julewire/core/serialization/backtrace_limiter.rb +50 -0
- data/lib/julewire/core/serialization/bounded_transform.rb +55 -0
- data/lib/julewire/core/serialization/bounded_traversal.rb +274 -0
- data/lib/julewire/core/serialization/deep_compact_empty.rb +67 -0
- data/lib/julewire/core/serialization/deep_freeze.rb +63 -0
- data/lib/julewire/core/serialization/encoding_sanitizer.rb +40 -0
- data/lib/julewire/core/serialization/exception_shape.rb +88 -0
- data/lib/julewire/core/serialization/json_encoder.rb +69 -0
- data/lib/julewire/core/serialization/serializer.rb +233 -0
- data/lib/julewire/core/serialization/serializer_pool.rb +21 -0
- data/lib/julewire/core/serialization/text_encoder.rb +147 -0
- data/lib/julewire/core/serialization/value_copy.rb +209 -0
- data/lib/julewire/core/serialization/value_traversal.rb +150 -0
- data/lib/julewire/core/testing/chaos/catalog.rb +72 -0
- data/lib/julewire/core/testing/chaos/core_runtime.rb +120 -0
- data/lib/julewire/core/testing/chaos/destination.rb +55 -0
- data/lib/julewire/core/testing/chaos/emitter.rb +20 -0
- data/lib/julewire/core/testing/chaos/raising_output.rb +42 -0
- data/lib/julewire/core/testing/chaos.rb +80 -0
- data/lib/julewire/core/testing/contracts/component.rb +162 -0
- data/lib/julewire/core/testing/contracts/deadline_scheduler.rb +59 -0
- data/lib/julewire/core/testing/contracts/integration.rb +166 -0
- data/lib/julewire/core/testing/contracts/integration_fields.rb +36 -0
- data/lib/julewire/core/testing/contracts/record_draft.rb +37 -0
- data/lib/julewire/core/testing/contracts/runtime.rb +178 -0
- data/lib/julewire/core/testing/contracts/wire.rb +60 -0
- data/lib/julewire/core/testing/contracts.rb +24 -0
- data/lib/julewire/core/testing/coverage.rb +58 -0
- data/lib/julewire/core/testing/test_reports.rb +78 -0
- data/lib/julewire/core/testing.rb +122 -0
- data/lib/julewire/core/validation.rb +69 -0
- data/lib/julewire/core/version.rb +7 -0
- data/lib/julewire/core.rb +80 -0
- data/lib/julewire/error.rb +5 -0
- data/lib/julewire-core.rb +3 -0
- metadata +237 -0
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Core
|
|
5
|
+
# @api internal
|
|
6
|
+
# Fiber-local context stack used by the runtime facade. Use Julewire.context
|
|
7
|
+
# and Julewire.with_execution instead of reaching into this class directly.
|
|
8
|
+
class ContextStore # rubocop:disable Metrics/ClassLength
|
|
9
|
+
EMPTY_HASH = {}.freeze
|
|
10
|
+
private_constant :EMPTY_HASH
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
def current
|
|
14
|
+
LocalStorage.context_store
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Reset only clears the caller's current thread/fiber context.
|
|
18
|
+
def reset_current! = LocalStorage.reset_context_store!
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def initialize
|
|
22
|
+
reset!
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def reset!
|
|
26
|
+
@scopes = []
|
|
27
|
+
@ambient_fields = Fields::StackSet.new
|
|
28
|
+
@execution_overlays = []
|
|
29
|
+
@execution_lineage_overlays = []
|
|
30
|
+
@propagation_execution_hash = nil
|
|
31
|
+
@propagation_scope_snapshot = nil
|
|
32
|
+
@linked_propagation_scope_snapshot = nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def current_scope = @scopes.last
|
|
36
|
+
|
|
37
|
+
def current_scope? = !!current_scope
|
|
38
|
+
|
|
39
|
+
def current_scope_or_snapshot
|
|
40
|
+
current_scope || propagation_scope_snapshot
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def context_proxy
|
|
44
|
+
@context_proxy ||= Fields::ContextProxy.new(self)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def carry_proxy
|
|
48
|
+
@carry_proxy ||= Fields::CarryProxy.new(self)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def attributes_proxy
|
|
52
|
+
@attributes_proxy ||= Fields::AttributesProxy.new(self)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def summary_proxy
|
|
56
|
+
@summary_proxy ||= Fields::SummaryProxy.new(self)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def context_hash
|
|
60
|
+
current_field_hash(:context)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def carry_hash
|
|
64
|
+
current_field_hash(:carry)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def attributes_hash
|
|
68
|
+
current_field_hash(:attributes)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def neutral_hash
|
|
72
|
+
current_field_hash(:neutral)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def context_value(key, default:)
|
|
76
|
+
current_field_stack(:context).value_for(key, default: default)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def carry_value(key, default:)
|
|
80
|
+
current_field_stack(:carry).value_for(key, default: default)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def attributes_value(key, default:)
|
|
84
|
+
current_field_stack(:attributes).value_for(key, default: default)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def add_context(fields = EMPTY_HASH, owned: false, **keyword_fields)
|
|
88
|
+
add_field(:context, field_input(fields, keyword_fields), owned: owned)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def add_carry(fields = EMPTY_HASH, owned: false, **keyword_fields)
|
|
92
|
+
add_field(:carry, field_input(fields, keyword_fields), owned: owned)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def add_attributes(fields = EMPTY_HASH, owned: false, **keyword_fields)
|
|
96
|
+
add_field(:attributes, field_input(fields, keyword_fields), owned: owned)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def add_neutral(fields = EMPTY_HASH, owned: false, **keyword_fields)
|
|
100
|
+
add_field(:neutral, field_input(fields, keyword_fields), owned: owned)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def delete_carry(path)
|
|
104
|
+
path = Fields::Internal.normalize_path(path)
|
|
105
|
+
return if path.empty?
|
|
106
|
+
|
|
107
|
+
if current_scope
|
|
108
|
+
current_scope.delete_carry(path)
|
|
109
|
+
else
|
|
110
|
+
@ambient_fields.delete(:carry, path)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def with_context(fields = EMPTY_HASH, owned: false, **keyword_fields, &)
|
|
115
|
+
with_scope_or_ambient_overlay(:context, field_input(fields, keyword_fields), owned: owned, &)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def with_carry(fields = EMPTY_HASH, owned: false, **keyword_fields, &)
|
|
119
|
+
with_scope_or_ambient_overlay(:carry, field_input(fields, keyword_fields), owned: owned, &)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def with_attributes(fields = EMPTY_HASH, owned: false, **keyword_fields, &)
|
|
123
|
+
with_scope_or_ambient_overlay(:attributes, field_input(fields, keyword_fields), owned: owned, &)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def with_neutral(fields = EMPTY_HASH, owned: false, **keyword_fields, &)
|
|
127
|
+
with_scope_or_ambient_overlay(:neutral, field_input(fields, keyword_fields), owned: owned, &)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def without_carry(path, &)
|
|
131
|
+
scope = current_scope
|
|
132
|
+
normalized_path = Fields::Internal.normalize_path(path)
|
|
133
|
+
raise ArgumentError, "carry path is required" if normalized_path.empty?
|
|
134
|
+
|
|
135
|
+
if scope
|
|
136
|
+
scope.without_carry(normalized_path, &)
|
|
137
|
+
else
|
|
138
|
+
@ambient_fields.without(:carry, normalized_path, &)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def with_propagation(context: {}, carry: {}, execution: {}, link_executions: false, &)
|
|
143
|
+
scope = current_scope
|
|
144
|
+
execution = Fields::FieldSet.deep_symbolize_keys(execution)
|
|
145
|
+
@execution_overlays.push(execution)
|
|
146
|
+
@execution_lineage_overlays.push(link_executions ? Execution::Lineage.from_execution_hash(execution) : nil)
|
|
147
|
+
invalidate_propagation_cache!
|
|
148
|
+
|
|
149
|
+
begin
|
|
150
|
+
if scope
|
|
151
|
+
scope.with_carry(carry) do
|
|
152
|
+
scope.with_context(context, &)
|
|
153
|
+
end
|
|
154
|
+
else
|
|
155
|
+
@ambient_fields.with(:carry, carry) do
|
|
156
|
+
@ambient_fields.with(:context, context, &)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
ensure
|
|
160
|
+
@execution_overlays.pop
|
|
161
|
+
@execution_lineage_overlays.pop
|
|
162
|
+
invalidate_propagation_cache!
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def with_execution(**options)
|
|
167
|
+
scope = build_scope(options)
|
|
168
|
+
active_exception = nil
|
|
169
|
+
|
|
170
|
+
@scopes.push(scope)
|
|
171
|
+
begin
|
|
172
|
+
yield Execution::View.new(scope)
|
|
173
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
174
|
+
active_exception = e
|
|
175
|
+
raise
|
|
176
|
+
ensure
|
|
177
|
+
scope.record_error(active_exception) if active_exception
|
|
178
|
+
@scopes.pop
|
|
179
|
+
finish_scope(
|
|
180
|
+
scope,
|
|
181
|
+
options[:on_finish],
|
|
182
|
+
options[:on_finish_failure],
|
|
183
|
+
active_exception: active_exception
|
|
184
|
+
)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def start_execution(**options)
|
|
189
|
+
scope = build_scope(options)
|
|
190
|
+
Execution::Handle.new(
|
|
191
|
+
scope: scope,
|
|
192
|
+
on_finish: options[:on_finish],
|
|
193
|
+
on_finish_failure: options[:on_finish_failure]
|
|
194
|
+
)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def with_scope(scope)
|
|
198
|
+
@scopes.push(scope)
|
|
199
|
+
yield Execution::View.new(scope)
|
|
200
|
+
ensure
|
|
201
|
+
@scopes.pop
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
private
|
|
205
|
+
|
|
206
|
+
def build_scope(options)
|
|
207
|
+
parent_scope = current_scope
|
|
208
|
+
fields = inherited_fields(options, parent_scope)
|
|
209
|
+
Execution::Scope.new(
|
|
210
|
+
type: options.fetch(:type),
|
|
211
|
+
id: options[:id],
|
|
212
|
+
execution: merged_execution_hash(options.fetch(:execution, EMPTY_HASH)),
|
|
213
|
+
execution_owned: true,
|
|
214
|
+
context: fields.stack(:context),
|
|
215
|
+
attributes: fields.stack(:attributes),
|
|
216
|
+
neutral: fields.stack(:neutral),
|
|
217
|
+
labels: options.fetch(:labels, EMPTY_HASH),
|
|
218
|
+
carry: fields.stack(:carry),
|
|
219
|
+
parent: parent_scope || linked_propagation_scope_snapshot,
|
|
220
|
+
started_at: options[:started_at],
|
|
221
|
+
summary_event: options[:summary_event],
|
|
222
|
+
summary_severity: options[:summary_severity],
|
|
223
|
+
summary_source: options[:summary_source]
|
|
224
|
+
)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def inherited_fields(options, parent_scope)
|
|
228
|
+
inherit = options.fetch(:inherit_attributes, true)
|
|
229
|
+
fields = inherited_stack_set(parent_scope, inherit_attributes: inherit)
|
|
230
|
+
add_scope_stack(fields, options, section: :attributes, key: :attributes)
|
|
231
|
+
add_scope_stack(fields, options, section: :neutral, key: :neutral)
|
|
232
|
+
fields
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def add_scope_stack(stack_set, options, section:, key:)
|
|
236
|
+
value = options.fetch(key, EMPTY_HASH)
|
|
237
|
+
if options.fetch(:owned, false)
|
|
238
|
+
stack_set.add(section, value, owned: true)
|
|
239
|
+
else
|
|
240
|
+
stack_set.add(section, value)
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def add_field(section, fields, owned: false)
|
|
245
|
+
scope = current_scope
|
|
246
|
+
if scope
|
|
247
|
+
scope.add_field(section, fields, owned: owned)
|
|
248
|
+
elsif owned
|
|
249
|
+
@ambient_fields.add(section, fields, owned: true)
|
|
250
|
+
else
|
|
251
|
+
@ambient_fields.add(section, fields)
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def field_input(fields, keyword_fields)
|
|
256
|
+
return fields if keyword_fields.empty?
|
|
257
|
+
return keyword_fields if empty_field_input?(fields)
|
|
258
|
+
|
|
259
|
+
fields.is_a?(Hash) ? fields.merge(keyword_fields) : fields
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def empty_field_input?(fields)
|
|
263
|
+
fields.nil? || (fields.respond_to?(:empty?) && fields.empty?)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def with_scope_or_ambient_overlay(section, fields, owned: false, &)
|
|
267
|
+
scope = current_scope
|
|
268
|
+
return scope.with_field(section, fields, owned: owned, &) if scope
|
|
269
|
+
|
|
270
|
+
if owned
|
|
271
|
+
@ambient_fields.with(section, fields, owned: true, &)
|
|
272
|
+
else
|
|
273
|
+
@ambient_fields.with(section, fields, &)
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def current_field_stack(section)
|
|
278
|
+
scope = current_scope
|
|
279
|
+
return @ambient_fields.stack(section) unless scope
|
|
280
|
+
|
|
281
|
+
scope.field_stack(section)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def current_field_hash(section)
|
|
285
|
+
scope = current_scope
|
|
286
|
+
scope ? scope.field_hash(section) : @ambient_fields.snapshot(section)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def inherited_stack_set(parent_scope, inherit_attributes:)
|
|
290
|
+
source = parent_scope ? parent_scope.field_stacks : @ambient_fields
|
|
291
|
+
Fields::StackSet.inherit_from(source, inherit_attributes: inherit_attributes)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def execution_hash
|
|
295
|
+
return {} if @execution_overlays.empty?
|
|
296
|
+
|
|
297
|
+
Fields::FieldSet.deep_dup(propagation_execution_hash)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def propagation_execution_hash
|
|
301
|
+
@propagation_execution_hash ||= Fields::Internal.frozen_copy(@execution_overlays.reduce({}) do |memo, overlay|
|
|
302
|
+
Fields::FieldSet.merge!(memo, overlay)
|
|
303
|
+
end)
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def propagation_scope_snapshot
|
|
307
|
+
execution = propagation_execution_hash
|
|
308
|
+
return if execution.empty?
|
|
309
|
+
|
|
310
|
+
@propagation_scope_snapshot ||= Execution::ScopeSnapshot.new(execution: execution)
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def linked_propagation_scope_snapshot
|
|
314
|
+
lineage = linked_propagation_lineage
|
|
315
|
+
return unless lineage
|
|
316
|
+
|
|
317
|
+
execution = propagation_execution_hash
|
|
318
|
+
return if execution.empty?
|
|
319
|
+
|
|
320
|
+
@linked_propagation_scope_snapshot ||= Execution::ScopeSnapshot.new(execution: execution, lineage: lineage)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def invalidate_propagation_cache!
|
|
324
|
+
@propagation_execution_hash = nil
|
|
325
|
+
@propagation_scope_snapshot = nil
|
|
326
|
+
@linked_propagation_scope_snapshot = nil
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def linked_propagation_lineage
|
|
330
|
+
(@execution_overlays.length - 1).downto(0) do |index|
|
|
331
|
+
execution = @execution_overlays.fetch(index)
|
|
332
|
+
next if execution.empty?
|
|
333
|
+
|
|
334
|
+
return @execution_lineage_overlays.fetch(index)
|
|
335
|
+
end
|
|
336
|
+
nil
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def merged_execution_hash(execution)
|
|
340
|
+
inherited = inherited_execution_hash
|
|
341
|
+
return inherited unless execution.is_a?(Hash) && !execution.empty?
|
|
342
|
+
|
|
343
|
+
Fields::FieldSet.merge!(inherited, execution)
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def inherited_execution_hash
|
|
347
|
+
scope = current_scope
|
|
348
|
+
return execution_hash unless scope
|
|
349
|
+
|
|
350
|
+
inherited = scope.inheritable_execution_hash
|
|
351
|
+
return inherited if @execution_overlays.empty?
|
|
352
|
+
|
|
353
|
+
overlay = execution_hash
|
|
354
|
+
return inherited if overlay.empty?
|
|
355
|
+
|
|
356
|
+
Fields::FieldSet.merge!(inherited, overlay)
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def finish_scope(scope, on_finish, on_finish_failure, active_exception: nil)
|
|
360
|
+
return unless on_finish
|
|
361
|
+
|
|
362
|
+
contain_finish_failure(on_finish_failure, active_exception) { scope.finish_owned unless scope.finished? }
|
|
363
|
+
contain_finish_failure(on_finish_failure, active_exception) { on_finish.call(scope) }
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def contain_finish_failure(on_finish_failure, active_exception)
|
|
367
|
+
yield
|
|
368
|
+
rescue StandardError => e
|
|
369
|
+
report_finish_failure(on_finish_failure, e)
|
|
370
|
+
rescue SystemStackError => e
|
|
371
|
+
# Preserve the app's active stack error during unwind.
|
|
372
|
+
raise unless active_exception
|
|
373
|
+
|
|
374
|
+
report_finish_failure(on_finish_failure, e)
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def report_finish_failure(on_finish_failure, error)
|
|
378
|
+
on_finish_failure&.call(error)
|
|
379
|
+
rescue StandardError
|
|
380
|
+
nil
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Core
|
|
5
|
+
module Destinations
|
|
6
|
+
class ChaosOutput
|
|
7
|
+
MODES = %i[mixed raise reject sleep].freeze
|
|
8
|
+
DEFAULT_RATE = 0.1
|
|
9
|
+
DEFAULT_SLEEP_MS = 10
|
|
10
|
+
|
|
11
|
+
def initialize(output, rate: DEFAULT_RATE, mode: :mixed, sleep_ms: DEFAULT_SLEEP_MS, seed: nil)
|
|
12
|
+
Sink.validate_writeable!(output)
|
|
13
|
+
@output = output
|
|
14
|
+
@rate = validate_rate(rate)
|
|
15
|
+
@mode = validate_mode(mode)
|
|
16
|
+
@sleep_seconds = validate_sleep_ms(sleep_ms) / 1000.0
|
|
17
|
+
@seed = seed
|
|
18
|
+
@random = random
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def write(value)
|
|
22
|
+
return @output.write(value) unless trigger?
|
|
23
|
+
|
|
24
|
+
case chaos_mode
|
|
25
|
+
when :raise then raise "julewire punk chaos output failure"
|
|
26
|
+
when :reject then false
|
|
27
|
+
when :sleep
|
|
28
|
+
sleep(@sleep_seconds)
|
|
29
|
+
@output.write(value)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def flush
|
|
34
|
+
@output.flush if @output.respond_to?(:flush)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def close
|
|
38
|
+
@output.close if @output.respond_to?(:close)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def closed?
|
|
42
|
+
@output.closed? if @output.respond_to?(:closed?)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def after_fork!
|
|
46
|
+
@random = random
|
|
47
|
+
@output.after_fork! if @output.respond_to?(:after_fork!)
|
|
48
|
+
self
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def resource_identity = @output
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def validate_rate(value)
|
|
56
|
+
return value if finite_orderable_number?(value) && value.between?(0, 1)
|
|
57
|
+
|
|
58
|
+
raise ArgumentError, "chaos rate must be a finite Numeric between 0 and 1"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def validate_mode(value)
|
|
62
|
+
Validation.validate_symbol_choice!(value, name: "chaos mode", choices: MODES)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def validate_sleep_ms(value)
|
|
66
|
+
return value if finite_orderable_number?(value) && value >= 0
|
|
67
|
+
|
|
68
|
+
raise ArgumentError, "chaos sleep_ms must be a non-negative finite Numeric"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def finite_orderable_number?(value)
|
|
72
|
+
value.is_a?(Numeric) && value.finite? && value.respond_to?(:between?)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def random
|
|
76
|
+
@seed ? Random.new(@seed) : Random.new
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def trigger?
|
|
80
|
+
@rate.positive? && @random.rand < @rate
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def chaos_mode
|
|
84
|
+
return @mode unless @mode == :mixed
|
|
85
|
+
|
|
86
|
+
%i[raise reject sleep].fetch(@random.rand(3))
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Julewire
|
|
4
|
+
module Core
|
|
5
|
+
# @api internal
|
|
6
|
+
module Destinations
|
|
7
|
+
class Collection
|
|
8
|
+
def initialize(destinations, on_drop:, on_failure:)
|
|
9
|
+
@destinations = destinations.freeze
|
|
10
|
+
@on_drop = on_drop
|
|
11
|
+
@on_failure = on_failure
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
def build(configuration:, defaults:, on_drop:, on_failure:)
|
|
16
|
+
new(
|
|
17
|
+
validate_destinations(configuration.destinations.build(defaults: defaults)),
|
|
18
|
+
on_drop: on_drop,
|
|
19
|
+
on_failure: on_failure
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def validate_destinations(destinations)
|
|
26
|
+
destinations.map do |destination|
|
|
27
|
+
Registry.validate!(destination)
|
|
28
|
+
end.freeze
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def empty? = @destinations.empty?
|
|
33
|
+
|
|
34
|
+
def emit(record)
|
|
35
|
+
@destinations.each do |destination|
|
|
36
|
+
emit_to_destination(destination, record)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def after_fork!
|
|
41
|
+
@destinations.each do |destination|
|
|
42
|
+
call_destination_after_fork(destination)
|
|
43
|
+
end
|
|
44
|
+
self
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def flush(timeout: nil)
|
|
48
|
+
call_lifecycle(:flush, timeout: timeout)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def close(timeout: nil, skip_resource_identities: nil)
|
|
52
|
+
call_lifecycle(:close, timeout: timeout, skip_resource_identities: skip_resource_identities)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def lifecycle_resource_identities
|
|
56
|
+
@destinations.each_with_object({}.compare_by_identity) do |destination, identities|
|
|
57
|
+
identities[resource_identity(destination)] = true
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def health
|
|
62
|
+
@destinations.to_h { [destination_name(it), destination_health(it)] }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def call_lifecycle(method_name, timeout:, skip_resource_identities: nil)
|
|
68
|
+
Validation.validate_timeout!(timeout, name: :timeout)
|
|
69
|
+
call_lifecycle_safely(method_name, timeout, skip_resource_identities)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def call_lifecycle_safely(method_name, timeout, skip_resource_identities)
|
|
73
|
+
deadline = Scheduling::Deadline.for(timeout)
|
|
74
|
+
ok = true
|
|
75
|
+
attempted = false
|
|
76
|
+
|
|
77
|
+
lifecycle_destinations(skip_resource_identities).each do |destination|
|
|
78
|
+
remaining_timeout = Scheduling::Deadline.remaining(deadline)
|
|
79
|
+
if attempted && deadline && remaining_timeout <= 0
|
|
80
|
+
ok = false
|
|
81
|
+
break
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
attempted = true
|
|
85
|
+
ok = false if destination.public_send(method_name, timeout: remaining_timeout) == false
|
|
86
|
+
rescue StandardError => e
|
|
87
|
+
notify_failure(e, action: method_name, destination: destination.name, phase: :destination_lifecycle)
|
|
88
|
+
ok = false
|
|
89
|
+
end
|
|
90
|
+
ok
|
|
91
|
+
rescue StandardError => e
|
|
92
|
+
notify_failure(e, action: method_name, phase: :output_lifecycle)
|
|
93
|
+
false
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def lifecycle_destinations(skip_resource_identities)
|
|
97
|
+
return @destinations unless skip_resource_identities
|
|
98
|
+
|
|
99
|
+
@destinations.reject { skip_lifecycle_destination?(it, skip_resource_identities) }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def call_destination_after_fork(destination)
|
|
103
|
+
destination.after_fork! if destination.respond_to?(:after_fork!)
|
|
104
|
+
rescue StandardError => e
|
|
105
|
+
notify_failure(
|
|
106
|
+
e,
|
|
107
|
+
action: :after_fork,
|
|
108
|
+
destination: destination_name(destination),
|
|
109
|
+
phase: :destination_lifecycle
|
|
110
|
+
)
|
|
111
|
+
nil
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def skip_lifecycle_destination?(destination, identities)
|
|
115
|
+
return false unless identities
|
|
116
|
+
|
|
117
|
+
identities.key?(resource_identity(destination))
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def resource_identity(destination)
|
|
121
|
+
return destination.resource_identity if destination.respond_to?(:resource_identity)
|
|
122
|
+
|
|
123
|
+
destination
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def emit_to_destination(destination, record)
|
|
127
|
+
result = destination.emit(record)
|
|
128
|
+
record_drop(:destination_rejected, destination, record) if result == false
|
|
129
|
+
rescue StandardError => e
|
|
130
|
+
metadata = destination_metadata(destination, record)
|
|
131
|
+
notify_failure(
|
|
132
|
+
e,
|
|
133
|
+
**metadata,
|
|
134
|
+
phase: :destination
|
|
135
|
+
)
|
|
136
|
+
record_drop(:destination_exception, destination, record, metadata: metadata)
|
|
137
|
+
nil
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def destination_name(destination)
|
|
141
|
+
destination.name
|
|
142
|
+
rescue StandardError
|
|
143
|
+
destination.class.name
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def destination_health(destination)
|
|
147
|
+
destination.health
|
|
148
|
+
rescue StandardError => e
|
|
149
|
+
{
|
|
150
|
+
status: :unknown,
|
|
151
|
+
type: "destination",
|
|
152
|
+
last_failure: Diagnostics::FailureSnapshot.build(
|
|
153
|
+
e,
|
|
154
|
+
destination: destination_name(destination),
|
|
155
|
+
phase: :destination_health
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def notify_failure(error, **metadata)
|
|
161
|
+
@on_failure.call(error, **metadata)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def record_drop(reason, destination, record, metadata: destination_metadata(destination, record))
|
|
165
|
+
@on_drop.call(reason, phase: :destination, **metadata)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def destination_metadata(destination, record)
|
|
169
|
+
{
|
|
170
|
+
destination: destination_name(destination),
|
|
171
|
+
record_metadata: Records::Metadata.call(record)
|
|
172
|
+
}
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|