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.
Files changed (164) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +6 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +73 -0
  5. data/docs/advanced-configuration.md +66 -0
  6. data/docs/attribute-keys.md +74 -0
  7. data/docs/configuration.md +327 -0
  8. data/docs/context-and-propagation.md +353 -0
  9. data/docs/contracts.md +211 -0
  10. data/docs/development.md +49 -0
  11. data/docs/extensions-and-api.md +567 -0
  12. data/docs/health-schema.md +104 -0
  13. data/docs/instrumentation-cheatsheet.md +29 -0
  14. data/docs/internals.md +135 -0
  15. data/docs/outputs-and-lifecycle.md +206 -0
  16. data/docs/quickstart.md +133 -0
  17. data/docs/record-sources.md +17 -0
  18. data/docs/records-and-data-policy.md +230 -0
  19. data/docs/security-and-wire.md +45 -0
  20. data/docs/tail.md +91 -0
  21. data/exe/julewire +6 -0
  22. data/julewire-core.gemspec +41 -0
  23. data/lib/julewire/core/cli/doctor.rb +143 -0
  24. data/lib/julewire/core/cli/line_helpers.rb +77 -0
  25. data/lib/julewire/core/cli/log_formats/console_text.rb +25 -0
  26. data/lib/julewire/core/cli/log_formats/core_json_decoder.rb +46 -0
  27. data/lib/julewire/core/cli/log_formats/core_json_encoder.rb +21 -0
  28. data/lib/julewire/core/cli/log_formats/record_decoder.rb +39 -0
  29. data/lib/julewire/core/cli/log_formats.rb +123 -0
  30. data/lib/julewire/core/cli/tail.rb +153 -0
  31. data/lib/julewire/core/cli/transcode.rb +105 -0
  32. data/lib/julewire/core/cli.rb +73 -0
  33. data/lib/julewire/core/configuration.rb +99 -0
  34. data/lib/julewire/core/context_store.rb +384 -0
  35. data/lib/julewire/core/destinations/chaos_output.rb +91 -0
  36. data/lib/julewire/core/destinations/collection.rb +177 -0
  37. data/lib/julewire/core/destinations/definition.rb +125 -0
  38. data/lib/julewire/core/destinations/destination.rb +268 -0
  39. data/lib/julewire/core/destinations/registry.rb +81 -0
  40. data/lib/julewire/core/destinations/sink.rb +35 -0
  41. data/lib/julewire/core/destinations/synchronized_output.rb +57 -0
  42. data/lib/julewire/core/destinations/tail_sampling.rb +321 -0
  43. data/lib/julewire/core/destinations/write_step.rb +119 -0
  44. data/lib/julewire/core/destinations.rb +33 -0
  45. data/lib/julewire/core/diagnostics/callback_notifier.rb +63 -0
  46. data/lib/julewire/core/diagnostics/doctor.rb +114 -0
  47. data/lib/julewire/core/diagnostics/failure_snapshot.rb +39 -0
  48. data/lib/julewire/core/diagnostics/health.rb +144 -0
  49. data/lib/julewire/core/diagnostics/integration_health_store.rb +64 -0
  50. data/lib/julewire/core/diagnostics/internal_records.rb +61 -0
  51. data/lib/julewire/core/diagnostics/invalid_severity_reporter.rb +112 -0
  52. data/lib/julewire/core/diagnostics/meta_observer.rb +161 -0
  53. data/lib/julewire/core/diagnostics/process_integration_health.rb +26 -0
  54. data/lib/julewire/core/diagnostics/tail/renderer.rb +36 -0
  55. data/lib/julewire/core/diagnostics/tail.rb +168 -0
  56. data/lib/julewire/core/diagnostics.rb +8 -0
  57. data/lib/julewire/core/error.rb +7 -0
  58. data/lib/julewire/core/execution/boundary.rb +106 -0
  59. data/lib/julewire/core/execution/handle.rb +77 -0
  60. data/lib/julewire/core/execution/lineage.rb +192 -0
  61. data/lib/julewire/core/execution/measurement_handle.rb +28 -0
  62. data/lib/julewire/core/execution/no_current_error.rb +9 -0
  63. data/lib/julewire/core/execution/scope.rb +246 -0
  64. data/lib/julewire/core/execution/scope_fields.rb +76 -0
  65. data/lib/julewire/core/execution/scope_identity.rb +71 -0
  66. data/lib/julewire/core/execution/scope_snapshot.rb +92 -0
  67. data/lib/julewire/core/execution/summary_state.rb +206 -0
  68. data/lib/julewire/core/execution/view.rb +56 -0
  69. data/lib/julewire/core/facade_methods.rb +181 -0
  70. data/lib/julewire/core/fields/attribute_keys.rb +54 -0
  71. data/lib/julewire/core/fields/attributes_proxy.rb +11 -0
  72. data/lib/julewire/core/fields/bags.rb +123 -0
  73. data/lib/julewire/core/fields/carry_proxy.rb +22 -0
  74. data/lib/julewire/core/fields/context_proxy.rb +11 -0
  75. data/lib/julewire/core/fields/field_set.rb +78 -0
  76. data/lib/julewire/core/fields/field_stack.rb +269 -0
  77. data/lib/julewire/core/fields/internal/deletion.rb +68 -0
  78. data/lib/julewire/core/fields/internal.rb +87 -0
  79. data/lib/julewire/core/fields/lookup.rb +35 -0
  80. data/lib/julewire/core/fields/section_proxy.rb +88 -0
  81. data/lib/julewire/core/fields/stack_set.rb +69 -0
  82. data/lib/julewire/core/fields/static_labels.rb +43 -0
  83. data/lib/julewire/core/fields/summary_proxy.rb +62 -0
  84. data/lib/julewire/core/integration/configurable.rb +52 -0
  85. data/lib/julewire/core/integration/destination_health.rb +43 -0
  86. data/lib/julewire/core/integration/event_subscriber.rb +62 -0
  87. data/lib/julewire/core/integration/facade.rb +131 -0
  88. data/lib/julewire/core/integration/fork_hooks.rb +79 -0
  89. data/lib/julewire/core/integration/health.rb +41 -0
  90. data/lib/julewire/core/integration/ivar_state.rb +38 -0
  91. data/lib/julewire/core/integration/lifecycle.rb +22 -0
  92. data/lib/julewire/core/integration/scoped.rb +34 -0
  93. data/lib/julewire/core/integration/settings.rb +92 -0
  94. data/lib/julewire/core/integration/subscriber_install.rb +39 -0
  95. data/lib/julewire/core/integration/subscription.rb +29 -0
  96. data/lib/julewire/core/integration/values.rb +192 -0
  97. data/lib/julewire/core/lifecycle_error.rb +7 -0
  98. data/lib/julewire/core/local_storage.rb +91 -0
  99. data/lib/julewire/core/processing/level_threshold.rb +53 -0
  100. data/lib/julewire/core/processing/match.rb +74 -0
  101. data/lib/julewire/core/processing/pipeline.rb +360 -0
  102. data/lib/julewire/core/processing/processor_chain.rb +69 -0
  103. data/lib/julewire/core/processing/processor_registry.rb +115 -0
  104. data/lib/julewire/core/processing/processor_wrapper.rb +44 -0
  105. data/lib/julewire/core/processing/record_field_transform.rb +124 -0
  106. data/lib/julewire/core/processing/sampling.rb +109 -0
  107. data/lib/julewire/core/processing.rb +41 -0
  108. data/lib/julewire/core/propagation/carrier.rb +93 -0
  109. data/lib/julewire/core/propagation.rb +50 -0
  110. data/lib/julewire/core/records/console_formatter.rb +24 -0
  111. data/lib/julewire/core/records/deconstruct.rb +19 -0
  112. data/lib/julewire/core/records/display_message.rb +166 -0
  113. data/lib/julewire/core/records/draft.rb +576 -0
  114. data/lib/julewire/core/records/formatter.rb +14 -0
  115. data/lib/julewire/core/records/lazy_emit_input.rb +99 -0
  116. data/lib/julewire/core/records/metadata.rb +23 -0
  117. data/lib/julewire/core/records/public_projection.rb +51 -0
  118. data/lib/julewire/core/records/raw_input.rb +41 -0
  119. data/lib/julewire/core/records/record.rb +175 -0
  120. data/lib/julewire/core/records/severity.rb +44 -0
  121. data/lib/julewire/core/runtime.rb +515 -0
  122. data/lib/julewire/core/runtime_locator.rb +20 -0
  123. data/lib/julewire/core/runtime_registry.rb +48 -0
  124. data/lib/julewire/core/runtime_state.rb +39 -0
  125. data/lib/julewire/core/scheduling/deadline.rb +24 -0
  126. data/lib/julewire/core/scheduling/deadline_scheduler.rb +207 -0
  127. data/lib/julewire/core/scheduling/shared_scheduler.rb +48 -0
  128. data/lib/julewire/core/sentinel.rb +18 -0
  129. data/lib/julewire/core/serialization/backtrace_limiter.rb +50 -0
  130. data/lib/julewire/core/serialization/bounded_transform.rb +55 -0
  131. data/lib/julewire/core/serialization/bounded_traversal.rb +274 -0
  132. data/lib/julewire/core/serialization/deep_compact_empty.rb +67 -0
  133. data/lib/julewire/core/serialization/deep_freeze.rb +63 -0
  134. data/lib/julewire/core/serialization/encoding_sanitizer.rb +40 -0
  135. data/lib/julewire/core/serialization/exception_shape.rb +88 -0
  136. data/lib/julewire/core/serialization/json_encoder.rb +69 -0
  137. data/lib/julewire/core/serialization/serializer.rb +233 -0
  138. data/lib/julewire/core/serialization/serializer_pool.rb +21 -0
  139. data/lib/julewire/core/serialization/text_encoder.rb +147 -0
  140. data/lib/julewire/core/serialization/value_copy.rb +209 -0
  141. data/lib/julewire/core/serialization/value_traversal.rb +150 -0
  142. data/lib/julewire/core/testing/chaos/catalog.rb +72 -0
  143. data/lib/julewire/core/testing/chaos/core_runtime.rb +120 -0
  144. data/lib/julewire/core/testing/chaos/destination.rb +55 -0
  145. data/lib/julewire/core/testing/chaos/emitter.rb +20 -0
  146. data/lib/julewire/core/testing/chaos/raising_output.rb +42 -0
  147. data/lib/julewire/core/testing/chaos.rb +80 -0
  148. data/lib/julewire/core/testing/contracts/component.rb +162 -0
  149. data/lib/julewire/core/testing/contracts/deadline_scheduler.rb +59 -0
  150. data/lib/julewire/core/testing/contracts/integration.rb +166 -0
  151. data/lib/julewire/core/testing/contracts/integration_fields.rb +36 -0
  152. data/lib/julewire/core/testing/contracts/record_draft.rb +37 -0
  153. data/lib/julewire/core/testing/contracts/runtime.rb +178 -0
  154. data/lib/julewire/core/testing/contracts/wire.rb +60 -0
  155. data/lib/julewire/core/testing/contracts.rb +24 -0
  156. data/lib/julewire/core/testing/coverage.rb +58 -0
  157. data/lib/julewire/core/testing/test_reports.rb +78 -0
  158. data/lib/julewire/core/testing.rb +122 -0
  159. data/lib/julewire/core/validation.rb +69 -0
  160. data/lib/julewire/core/version.rb +7 -0
  161. data/lib/julewire/core.rb +80 -0
  162. data/lib/julewire/error.rb +5 -0
  163. data/lib/julewire-core.rb +3 -0
  164. metadata +237 -0
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ module Diagnostics
6
+ class Tail
7
+ DEFAULT_CAPACITY = 200
8
+ DEFAULT_NAME = :tail
9
+ COUNTER_KEYS = %i[captured failures].freeze
10
+ Entry = Data.define(:sequence, :at, :record)
11
+
12
+ class << self
13
+ def attach!(runtime = Julewire, **)
14
+ destination = new(**)
15
+ runtime.configure { it.destinations.add(destination) }
16
+ destination
17
+ end
18
+ end
19
+
20
+ attr_reader :capacity, :name
21
+
22
+ def initialize(
23
+ name: DEFAULT_NAME,
24
+ capacity: DEFAULT_CAPACITY,
25
+ formatter: Records::Formatter.new,
26
+ renderer: Renderer.new,
27
+ serializer: nil
28
+ )
29
+ @name = Core.normalize_name(name)
30
+ @capacity = Validation.validate_integer_limit!(capacity, name: :capacity, positive: true)
31
+ Validation.validate_callable!(formatter, name: :formatter)
32
+ Validation.validate_callable!(renderer, name: :renderer)
33
+ @formatter = formatter
34
+ @renderer = renderer
35
+ @serializer = serializer
36
+ @serializer_pool_key = :"julewire_core_tail_serializers_#{object_id}"
37
+ initialize_state
38
+ end
39
+
40
+ def emit(record)
41
+ 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
+ end
48
+ @health.increment(:captured)
49
+ nil
50
+ rescue StandardError => e
51
+ record_failure(e, record)
52
+ nil
53
+ end
54
+
55
+ def entries(limit: nil)
56
+ limit = normalize_limit(limit)
57
+ snapshot = @mutex.synchronize { @entries.dup }
58
+ limit ? snapshot.last(limit) : snapshot
59
+ end
60
+
61
+ def records(limit: nil)
62
+ entries(limit: limit).map(&:record)
63
+ end
64
+
65
+ def render(limit: nil, color: false)
66
+ @renderer.call(entries(limit: limit), color: color)
67
+ end
68
+
69
+ def write(io = $stdout, limit: nil, color: nil)
70
+ io.write(render(limit: limit, color: color.nil? ? io.tty? : color))
71
+ io
72
+ end
73
+
74
+ def clear
75
+ @mutex.synchronize do
76
+ @entries.clear
77
+ end
78
+ @health.clear_degraded!
79
+ self
80
+ end
81
+
82
+ def flush(*) = self
83
+
84
+ def close(*) = self
85
+
86
+ def after_fork!
87
+ initialize_state
88
+ self
89
+ end
90
+
91
+ def health
92
+ size = @mutex.synchronize { @entries.length }
93
+ @health.snapshot(capacity: @capacity, size: size)
94
+ end
95
+
96
+ private
97
+
98
+ def initialize_state
99
+ @mutex = Mutex.new
100
+ @entries = []
101
+ @health = Integration::DestinationHealth.new(counter_keys: COUNTER_KEYS)
102
+ @sequence = 0
103
+ @serializer_mutex = Mutex.new
104
+ end
105
+
106
+ def normalize_limit(value)
107
+ return if value.nil?
108
+
109
+ Validation.validate_integer_limit!(value, name: :limit, positive: true)
110
+ end
111
+
112
+ def snapshot_record(record)
113
+ payload = @formatter.call(record)
114
+ raise TypeError, "formatter must return a payload object" if payload.nil?
115
+
116
+ Serialization::DeepFreeze.call(with_display_message(serialize_payload(payload), record))
117
+ end
118
+
119
+ def with_display_message(payload, record)
120
+ return payload unless payload.is_a?(Hash)
121
+ return payload unless blank?(payload["message"]) && blank?(payload[:message])
122
+
123
+ message = Records::DisplayMessage.call(record)
124
+ return payload if blank?(message)
125
+
126
+ payload = payload.dup if payload.frozen?
127
+ payload["message"] = message
128
+ payload
129
+ end
130
+
131
+ def serialize_payload(payload)
132
+ return serialize_custom_payload(payload) if @serializer
133
+
134
+ serializer = cached_serializer
135
+ return build_serializer.serialize(payload) if serializer.in_use?
136
+
137
+ serializer.serialize(payload)
138
+ end
139
+
140
+ def serialize_custom_payload(payload)
141
+ @serializer_mutex.synchronize { @serializer.serialize(payload) }
142
+ end
143
+
144
+ def cached_serializer
145
+ Serialization::SerializerPool.serializer(@serializer_pool_key, :default) { build_serializer }
146
+ end
147
+
148
+ def build_serializer
149
+ Serialization::Serializer.new(compact_empty: true)
150
+ end
151
+
152
+ def blank?(value)
153
+ value.nil? || (value.respond_to?(:empty?) && value.empty?)
154
+ end
155
+
156
+ def record_failure(error, record)
157
+ @health.record_failure(
158
+ error,
159
+ action: :emit,
160
+ destination: @name,
161
+ phase: :tail,
162
+ record_metadata: Records::Metadata.call(record)
163
+ )
164
+ end
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ module Diagnostics
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ class Error < Julewire::Error; end
6
+ end
7
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ module Execution
6
+ class Boundary
7
+ EMPTY_HASH = {}.freeze
8
+ EXECUTION_OPTION_DEFAULTS = {
9
+ id: nil,
10
+ fields: EMPTY_HASH,
11
+ attributes: EMPTY_HASH,
12
+ neutral: EMPTY_HASH,
13
+ labels: EMPTY_HASH,
14
+ owned: false,
15
+ inherit_attributes: true,
16
+ emit_summary: true,
17
+ summary_event: nil,
18
+ summary_severity: nil,
19
+ summary_source: nil
20
+ }.freeze
21
+ EXECUTION_OPTION_KEYS = EXECUTION_OPTION_DEFAULTS.keys.freeze
22
+ private_constant :EXECUTION_OPTION_DEFAULTS, :EXECUTION_OPTION_KEYS, :EMPTY_HASH
23
+
24
+ def initialize(emit_summary_record:, summary_finalizer_failure:, emit_non_standard_exception_summaries:,
25
+ before_call: nil)
26
+ @before_call = before_call
27
+ @emit_summary_record = emit_summary_record
28
+ @summary_finalizer_failure = summary_finalizer_failure
29
+ @emit_non_standard_exception_summaries = emit_non_standard_exception_summaries
30
+ end
31
+
32
+ def with_execution(type:, **options, &)
33
+ before_call!(:with_execution)
34
+ raise ArgumentError, "block required" unless block_given?
35
+
36
+ open_context_execution(:with_execution, type: type, options: options, &)
37
+ end
38
+
39
+ def start_execution(type:, **options)
40
+ before_call!(:start_execution)
41
+ open_context_execution(:start_execution, type: type, options: options)
42
+ end
43
+
44
+ private
45
+
46
+ def before_call!(action)
47
+ @before_call&.call(action)
48
+ end
49
+
50
+ def open_context_execution(method_name, type:, options:, &)
51
+ options = normalized_execution_options(options)
52
+ ContextStore.current.public_send(
53
+ method_name,
54
+ type: type,
55
+ id: options.fetch(:id),
56
+ execution: options.fetch(:execution),
57
+ attributes: options.fetch(:attributes),
58
+ neutral: options.fetch(:neutral),
59
+ labels: options.fetch(:labels),
60
+ owned: options.fetch(:owned),
61
+ inherit_attributes: options.fetch(:inherit_attributes),
62
+ on_finish: summary_finalizer(emit_summary_enabled: options.fetch(:emit_summary)),
63
+ on_finish_failure: @summary_finalizer_failure,
64
+ summary_event: options.fetch(:summary_event),
65
+ summary_severity: options.fetch(:summary_severity),
66
+ summary_source: options.fetch(:summary_source),
67
+ &
68
+ )
69
+ end
70
+
71
+ def normalized_execution_options(options)
72
+ unknown = options.keys - EXECUTION_OPTION_KEYS
73
+ raise ArgumentError, "unknown execution options: #{unknown.join(", ")}" unless unknown.empty?
74
+
75
+ EXECUTION_OPTION_DEFAULTS.merge(options).tap do |normalized|
76
+ normalized[:execution] = execution_fields(normalized.delete(:fields))
77
+ normalized[:attributes] ||= EMPTY_HASH
78
+ normalized[:neutral] ||= EMPTY_HASH
79
+ normalized[:labels] ||= EMPTY_HASH
80
+ end
81
+ end
82
+
83
+ def execution_fields(fields)
84
+ return {} if fields.nil?
85
+ raise ArgumentError, "execution fields must be a Hash" unless fields.is_a?(Hash)
86
+
87
+ fields
88
+ end
89
+
90
+ def summary_finalizer(emit_summary_enabled:)
91
+ return unless emit_summary_enabled
92
+
93
+ @summary_finalizer ||= lambda do |scope|
94
+ next if suppress_summary_for_non_standard_exception?(scope)
95
+
96
+ @emit_summary_record.call(scope)
97
+ end
98
+ end
99
+
100
+ def suppress_summary_for_non_standard_exception?(scope)
101
+ scope.non_standard_exception? && !@emit_non_standard_exception_summaries.call
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ module Execution
6
+ class Handle
7
+ def initialize(scope:, on_finish:, on_finish_failure:)
8
+ @scope = scope
9
+ @on_finish = on_finish
10
+ @on_finish_failure = on_finish_failure
11
+ @mutex = Mutex.new
12
+ @finished = false
13
+ end
14
+
15
+ attr_reader :scope
16
+
17
+ def snapshot = View.new(@scope)
18
+
19
+ def run
20
+ active_exception = nil
21
+ ContextStore.current.with_scope(@scope) do
22
+ yield self
23
+ end
24
+ rescue Exception => e # rubocop:disable Lint/RescueException
25
+ active_exception = e
26
+ raise
27
+ ensure
28
+ finish(reason: :error, error: active_exception) if active_exception
29
+ end
30
+
31
+ def with_context(&)
32
+ ContextStore.current.with_scope(@scope, &)
33
+ end
34
+
35
+ def finish(reason: :closed, fields: {}, attributes: {}, error: nil, severity: nil)
36
+ return false unless mark_finished
37
+
38
+ add_completion_attributes(reason)
39
+ @scope.add_summary(fields) unless fields.empty?
40
+ @scope.add_summary_attributes(attributes) unless attributes.empty?
41
+ @scope.finish_owned(error: error, severity: severity)
42
+ call_finish
43
+ true
44
+ rescue StandardError => e
45
+ report_finish_failure(e)
46
+ false
47
+ end
48
+
49
+ private
50
+
51
+ def mark_finished
52
+ @mutex.synchronize do
53
+ return false if @finished
54
+
55
+ @finished = true
56
+ end
57
+ end
58
+
59
+ def add_completion_attributes(reason)
60
+ @scope.add_summary_attributes({ "julewire.completion": reason.to_s }, owned: true)
61
+ end
62
+
63
+ def call_finish
64
+ @on_finish&.call(@scope)
65
+ rescue StandardError => e
66
+ report_finish_failure(e)
67
+ end
68
+
69
+ def report_finish_failure(error)
70
+ @on_finish_failure&.call(error)
71
+ rescue StandardError
72
+ nil
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,192 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ module Execution
6
+ class Lineage
7
+ # Bounded to cap summary growth; also the Answer to the Ultimate Question.
8
+ MAX_ANCESTORS = 42
9
+ RELATIONSHIP_KEYS = %i[depth root parent ancestors ancestors_truncated].freeze
10
+ LAZY_RELATIONSHIP_KEYS = %i[ancestors ancestors_truncated].freeze
11
+ attr_reader :depth, :parent_reference, :root_reference
12
+
13
+ class << self
14
+ def clean_execution_hash(execution)
15
+ clean_hash(execution, RELATIONSHIP_KEYS)
16
+ end
17
+
18
+ def clean_owned_execution_hash(execution)
19
+ clean_hash!(execution, RELATIONSHIP_KEYS)
20
+ end
21
+
22
+ def clean_lazy_relationship_hash(execution)
23
+ clean_hash(execution, LAZY_RELATIONSHIP_KEYS)
24
+ end
25
+
26
+ def from_execution_hash(execution)
27
+ execution = {} unless execution.is_a?(Hash)
28
+ new(
29
+ reference: execution_reference(execution),
30
+ root_reference: relationship_value(execution, :root),
31
+ parent_reference: relationship_value(execution, :parent),
32
+ depth: relationship_value(execution, :depth),
33
+ ancestors: relationship_value(execution, :ancestors),
34
+ ancestors_truncated: relationship_value(execution, :ancestors_truncated)
35
+ )
36
+ end
37
+
38
+ private
39
+
40
+ def relationship_value(execution, key)
41
+ Fields::FieldSet.value_for(execution, key)
42
+ end
43
+
44
+ def reference_value(execution, key)
45
+ Fields::FieldSet.value_for(execution, key, default: MISSING)
46
+ end
47
+
48
+ def clean_hash(execution, keys)
49
+ copy = execution.is_a?(Hash) ? Fields::FieldSet.deep_symbolize_keys(execution) : {}
50
+ clean_hash!(copy, keys)
51
+ end
52
+
53
+ def clean_hash!(copy, keys)
54
+ keys.each do |key|
55
+ Fields::Internal.delete_key!(copy, key)
56
+ end
57
+ copy
58
+ end
59
+
60
+ def execution_reference(execution)
61
+ reference = {}
62
+ type = reference_value(execution, :type)
63
+ id = reference_value(execution, :id)
64
+ reference[:type] = type unless type.equal?(MISSING)
65
+ reference[:id] = id unless id.equal?(MISSING)
66
+ reference.empty? ? nil : reference
67
+ end
68
+ end
69
+
70
+ def initialize(
71
+ reference: nil,
72
+ parent_lineage: nil,
73
+ parent_reference: nil,
74
+ root_reference: nil,
75
+ depth: nil,
76
+ ancestors: nil,
77
+ ancestors_truncated: false
78
+ )
79
+ @parent_lineage = parent_lineage
80
+ @depth = depth_value(depth, parent_lineage)
81
+ @root_reference = freeze_reference(root_reference || root_reference_for(reference, parent_lineage))
82
+ @parent_reference = freeze_reference(parent_reference)
83
+ @ancestor_references = freeze_ancestors(ancestors)
84
+ @ancestors_truncated = ancestors_truncated ? true : false
85
+ @ancestor_references_for_child = nil
86
+ @truncated = nil
87
+ @truncated_computed = false
88
+ end
89
+
90
+ def merge_into_frozen(execution)
91
+ hash = frozen_hash_copy(execution)
92
+ hash[:depth] = depth
93
+ hash[:root] = @root_reference
94
+ hash[:parent] = @parent_reference if @parent_reference
95
+ hash.freeze
96
+ end
97
+
98
+ def ancestors
99
+ references = @ancestor_references_for_child
100
+ return references if references
101
+
102
+ materialize_ancestor_references_for_child
103
+ end
104
+
105
+ def truncated?
106
+ return @truncated if @truncated_computed
107
+
108
+ @truncated = @ancestors_truncated || ancestor_count_exceeds_limit?
109
+ @truncated_computed = true
110
+ @truncated
111
+ end
112
+
113
+ def freeze
114
+ return self if frozen?
115
+
116
+ ancestors
117
+ truncated?
118
+ @parent_lineage = nil
119
+ super
120
+ end
121
+
122
+ protected
123
+
124
+ def ancestor_references_for_child
125
+ ancestors
126
+ end
127
+
128
+ def root_reference_for_child
129
+ @root_reference
130
+ end
131
+
132
+ private
133
+
134
+ attr_reader :parent_lineage
135
+
136
+ def materialize_ancestor_references_for_child
137
+ if @ancestor_references
138
+ @ancestor_references_for_child = @ancestor_references.freeze
139
+ else
140
+ references = build_ancestor_references
141
+ @truncated = references.length > MAX_ANCESTORS
142
+ @truncated_computed = true
143
+ @ancestor_references_for_child = references.last(MAX_ANCESTORS).freeze
144
+ end
145
+ end
146
+
147
+ def ancestor_count_exceeds_limit?
148
+ return false if @ancestor_references_for_child
149
+
150
+ build_ancestor_references.length > MAX_ANCESTORS
151
+ end
152
+
153
+ def depth_value(depth, parent_lineage)
154
+ return depth if depth.is_a?(Integer) && depth.positive?
155
+ return parent_lineage.depth + 1 if parent_lineage
156
+
157
+ 1
158
+ end
159
+
160
+ def root_reference_for(reference, parent_lineage)
161
+ parent_lineage ? parent_lineage.root_reference_for_child : reference
162
+ end
163
+
164
+ def build_ancestor_references
165
+ return [] unless parent_lineage && @parent_reference
166
+
167
+ parent_lineage.ancestor_references_for_child + [@parent_reference]
168
+ end
169
+
170
+ def freeze_ancestors(ancestors)
171
+ return unless ancestors.is_a?(Array)
172
+
173
+ Serialization::ValueCopy.call(ancestors, freeze_values: true)
174
+ end
175
+
176
+ def freeze_reference(reference)
177
+ return unless reference
178
+ return reference if reference.frozen?
179
+
180
+ Serialization::ValueCopy.call(reference, freeze_values: true)
181
+ end
182
+
183
+ def frozen_hash_copy(value)
184
+ value.each_with_object({}) do |(key, field_value), copy|
185
+ copy[Fields::Internal.normalize_key(key)] =
186
+ Serialization::ValueCopy.call(field_value, freeze_values: true)
187
+ end
188
+ end
189
+ end
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ module Execution
6
+ class MeasurementHandle
7
+ def initialize(&finish)
8
+ @finish = finish
9
+ @finished = false
10
+ @mutex = Mutex.new
11
+ end
12
+
13
+ def finish
14
+ @mutex.synchronize do
15
+ return if @finished
16
+
17
+ @finished = true
18
+ @finish.call
19
+ end
20
+ end
21
+
22
+ def finished?
23
+ @mutex.synchronize { @finished }
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Julewire
4
+ module Core
5
+ module Execution
6
+ class NoCurrentError < Error; end
7
+ end
8
+ end
9
+ end