activejob-temporal 0.1.0 → 0.3.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 +4 -4
- data/AGENTS.md +1 -0
- data/CHANGELOG.md +29 -0
- data/CLAUDE.md +274 -0
- data/CONTRIBUTING.md +69 -0
- data/README.md +34 -33
- data/activejob-temporal.gemspec +9 -13
- data/api/job_payload_schema.json +51 -6
- data/bin/temporal-worker +25 -17
- data/gemfiles/activejob_contract.gemfile +8 -0
- data/gemfiles/temporalio_contract.gemfile +7 -0
- data/lib/activejob/temporal/activities/aj_runner_activity.rb +75 -19
- data/lib/activejob/temporal/activities/dependency_status_activity.rb +36 -19
- data/lib/activejob/temporal/adapter.rb +4 -3
- data/lib/activejob/temporal/batch_enqueuer.rb +42 -33
- data/lib/activejob/temporal/bind_policy.rb +1 -1
- data/lib/activejob/temporal/cancel.rb +66 -29
- data/lib/activejob/temporal/certificate_watcher.rb +41 -6
- data/lib/activejob/temporal/client.rb +24 -6
- data/lib/activejob/temporal/conditional_enqueue.rb +2 -1
- data/lib/activejob/temporal/configurable.rb +56 -13
- data/lib/activejob/temporal/configuration.rb +218 -7
- data/lib/activejob/temporal/configured_job_compatibility.rb +91 -9
- data/lib/activejob/temporal/connection_worker_pool.rb +18 -1
- data/lib/activejob/temporal/dead_letter_queue.rb +64 -18
- data/lib/activejob/temporal/dependency_options.rb +126 -16
- data/lib/activejob/temporal/health_check_server.rb +14 -17
- data/lib/activejob/temporal/http_line_reader.rb +20 -2
- data/lib/activejob/temporal/http_request_failure_handling.rb +41 -0
- data/lib/activejob/temporal/inspect.rb +17 -7
- data/lib/activejob/temporal/job_id_validation.rb +41 -0
- data/lib/activejob/temporal/job_payload_dependencies.rb +23 -0
- data/lib/activejob/temporal/locales/en.yml +30 -3
- data/lib/activejob/temporal/metrics_server.rb +15 -18
- data/lib/activejob/temporal/middleware/chain.rb +7 -0
- data/lib/activejob/temporal/observability.rb +14 -2
- data/lib/activejob/temporal/payload.rb +64 -25
- data/lib/activejob/temporal/payload_encryption.rb +9 -1
- data/lib/activejob/temporal/payload_serializers.rb +3 -0
- data/lib/activejob/temporal/payload_storage.rb +4 -4
- data/lib/activejob/temporal/rails_environment_loader.rb +1 -8
- data/lib/activejob/temporal/reload_signal_queue.rb +19 -19
- data/lib/activejob/temporal/retry_handler_extractor.rb +18 -3
- data/lib/activejob/temporal/schedulable.rb +5 -7
- data/lib/activejob/temporal/schedule.rb +2 -2
- data/lib/activejob/temporal/signal_query.rb +35 -23
- data/lib/activejob/temporal/temporal_options.rb +26 -1
- data/lib/activejob/temporal/tls_file.rb +16 -16
- data/lib/activejob/temporal/transaction_safety.rb +102 -1
- data/lib/activejob/temporal/version.rb +1 -1
- data/lib/activejob/temporal/visibility_query.rb +16 -1
- data/lib/activejob/temporal/worker_pool.rb +13 -0
- data/lib/activejob/temporal/worker_registrations.rb +66 -0
- data/lib/activejob/temporal/worker_runtime.rb +17 -0
- data/lib/activejob/temporal/workflow_enqueuer.rb +2 -1
- data/lib/activejob/temporal/workflow_types.rb +10 -0
- data/lib/activejob/temporal/workflows/aj_workflow.rb +17 -4
- data/lib/activejob/temporal/workflows/workflow_dependencies.rb +148 -15
- data/lib/activejob/temporal.rb +21 -13
- data/test/mutant_unit_test.rb +13 -0
- metadata +46 -50
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "temporalio/error"
|
|
4
|
+
require_relative "job_id_validation"
|
|
3
5
|
require_relative "visibility_query"
|
|
4
6
|
require_relative "workflow_id_builder"
|
|
5
7
|
|
|
@@ -27,6 +29,48 @@ module ActiveJob
|
|
|
27
29
|
# @see https://docs.temporal.io/activities#heartbeat Temporal Activity Heartbeating
|
|
28
30
|
# @see https://docs.temporal.io/workflows#cancellation Temporal Cancellation Guide
|
|
29
31
|
module Cancel
|
|
32
|
+
class CancellationRequest
|
|
33
|
+
def initialize(client:, job_id:, workflow_id:, run_id: nil)
|
|
34
|
+
@client = client
|
|
35
|
+
@job_id = job_id
|
|
36
|
+
@workflow_id = workflow_id
|
|
37
|
+
@run_id = run_id
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def call
|
|
41
|
+
cancellation_handle.cancel
|
|
42
|
+
rescue StandardError => e
|
|
43
|
+
raise workflow_not_found_error, cause: e if rpc_not_found?(e)
|
|
44
|
+
|
|
45
|
+
raise ActiveJob::Temporal::TemporalConnectionError.new(
|
|
46
|
+
"Failed to cancel Temporal workflow for job_id #{@job_id}: #{e.message}"
|
|
47
|
+
), cause: e
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
attr_reader :client, :workflow_id, :run_id
|
|
53
|
+
|
|
54
|
+
def cancellation_handle
|
|
55
|
+
return client.workflow_handle(workflow_id) unless run_id
|
|
56
|
+
|
|
57
|
+
client.workflow_handle(workflow_id, run_id: run_id)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def workflow_not_found_error
|
|
61
|
+
ActiveJob::Temporal::WorkflowNotFoundError.new(
|
|
62
|
+
"No workflow found for job_id #{@job_id}. The job may have completed or never existed."
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def rpc_not_found?(error)
|
|
67
|
+
defined?(Temporalio::Error::RPCError) &&
|
|
68
|
+
error.is_a?(Temporalio::Error::RPCError) &&
|
|
69
|
+
error.code == Temporalio::Error::RPCError::Code::NOT_FOUND
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
private_constant :CancellationRequest
|
|
73
|
+
|
|
30
74
|
class << self
|
|
31
75
|
# Cancels a running Temporal workflow by sending a cancellation request.
|
|
32
76
|
#
|
|
@@ -38,7 +82,7 @@ module ActiveJob
|
|
|
38
82
|
# @return [Boolean, nil] Returns false if workflow already completed, nil if cancellation requested
|
|
39
83
|
#
|
|
40
84
|
# @raise [WorkflowNotFoundError] if no workflow exists for the given job_id
|
|
41
|
-
# @raise [TemporalConnectionError] if
|
|
85
|
+
# @raise [TemporalConnectionError] if Temporal lookup or cancellation RPCs fail
|
|
42
86
|
#
|
|
43
87
|
# @note Asynchronous Cancellation
|
|
44
88
|
# Cancellation requests are asynchronous. The method returns immediately after
|
|
@@ -67,6 +111,8 @@ module ActiveJob
|
|
|
67
111
|
def cancel(job_class, job_id)
|
|
68
112
|
validate_job_id!(job_id)
|
|
69
113
|
client = ActiveJob::Temporal.client
|
|
114
|
+
return nil if cancel_schedule_execution(client, job_class, job_id)
|
|
115
|
+
|
|
70
116
|
workflow_state = find_workflow(client, job_class, job_id)
|
|
71
117
|
workflow_id = workflow_state[:workflow_id]
|
|
72
118
|
|
|
@@ -78,7 +124,7 @@ module ActiveJob
|
|
|
78
124
|
raise ActiveJob::Temporal::WorkflowNotFoundError,
|
|
79
125
|
"No workflow found for job_id #{job_id}. The job may have never existed."
|
|
80
126
|
when :running
|
|
81
|
-
client
|
|
127
|
+
request_cancellation(client, job_id, workflow_id)
|
|
82
128
|
log_cancellation_requested(job_class, job_id, workflow_id)
|
|
83
129
|
log_audit_cancellation_requested(job_class, job_id, workflow_id)
|
|
84
130
|
nil
|
|
@@ -95,11 +141,6 @@ module ActiveJob
|
|
|
95
141
|
BatchCanceller.new(ActiveJob::Temporal.client).cancel_where(filters)
|
|
96
142
|
end
|
|
97
143
|
|
|
98
|
-
# UUID format regex (compliant with RFC 4122).
|
|
99
|
-
# Matches standard UUID format: 8-4-4-4-12 hexadecimal characters.
|
|
100
|
-
# @api private
|
|
101
|
-
UUID_REGEX = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i
|
|
102
|
-
|
|
103
144
|
private
|
|
104
145
|
|
|
105
146
|
def validate_job_class!(job_class)
|
|
@@ -108,22 +149,28 @@ module ActiveJob
|
|
|
108
149
|
raise ArgumentError, "job_class must be a named class"
|
|
109
150
|
end
|
|
110
151
|
|
|
111
|
-
# Validates that job_id is a valid UUID format.
|
|
112
|
-
#
|
|
113
|
-
# ActiveJob generates job IDs using SecureRandom.uuid, which produces RFC 4122
|
|
114
|
-
# compliant UUIDs. This validation prevents search query injection attacks by
|
|
115
|
-
# ensuring job_id contains only hexadecimal characters and hyphens, making it
|
|
116
|
-
# safe for direct use in Temporal queries.
|
|
117
|
-
#
|
|
118
152
|
# @param job_id [String] The job identifier to validate
|
|
119
|
-
# @raise [ArgumentError] if job_id is not a
|
|
153
|
+
# @raise [ArgumentError] if job_id is not a safe string identifier
|
|
120
154
|
# @api private
|
|
121
155
|
def validate_job_id!(job_id)
|
|
122
|
-
|
|
156
|
+
JobIdValidation.validate!(job_id)
|
|
157
|
+
end
|
|
123
158
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
159
|
+
def cancel_schedule_execution(client, job_class, job_id)
|
|
160
|
+
schedule_reference = JobIdValidation.schedule_execution_reference(job_id)
|
|
161
|
+
return false unless schedule_reference
|
|
162
|
+
|
|
163
|
+
workflow_id = schedule_reference.fetch(:workflow_id)
|
|
164
|
+
request_cancellation(client, job_id, workflow_id, run_id: schedule_reference.fetch(:run_id))
|
|
165
|
+
log_cancellation_requested(job_class, job_id, workflow_id)
|
|
166
|
+
log_audit_cancellation_requested(job_class, job_id, workflow_id)
|
|
167
|
+
true
|
|
168
|
+
rescue ActiveJob::Temporal::WorkflowNotFoundError
|
|
169
|
+
false
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def request_cancellation(client, job_id, workflow_id, run_id: nil)
|
|
173
|
+
CancellationRequest.new(client: client, job_id: job_id, workflow_id: workflow_id, run_id: run_id).call
|
|
127
174
|
end
|
|
128
175
|
|
|
129
176
|
# Builds deterministic workflow ID from job class and job ID.
|
|
@@ -167,21 +214,11 @@ module ActiveJob
|
|
|
167
214
|
{ status: status, workflow_id: workflow_id }
|
|
168
215
|
end
|
|
169
216
|
|
|
170
|
-
# Builds Temporal query for running workflows by job class and job_id.
|
|
171
|
-
#
|
|
172
|
-
# Note: job_id is validated as a UUID before reaching this method, ensuring it
|
|
173
|
-
# contains only safe characters ([0-9a-fA-F-]) for direct query interpolation.
|
|
174
|
-
#
|
|
175
217
|
# @api private
|
|
176
218
|
def running_workflows_query(job_class, job_id)
|
|
177
219
|
workflow_search_query(job_class, job_id, "ExecutionStatus='Running'")
|
|
178
220
|
end
|
|
179
221
|
|
|
180
|
-
# Builds Temporal query for closed workflows by job class and job_id.
|
|
181
|
-
#
|
|
182
|
-
# Note: job_id is validated as a UUID before reaching this method, ensuring it
|
|
183
|
-
# contains only safe characters ([0-9a-fA-F-]) for direct query interpolation.
|
|
184
|
-
#
|
|
185
222
|
# @api private
|
|
186
223
|
def closed_workflows_query(job_class, job_id)
|
|
187
224
|
workflow_search_query(
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "listen"
|
|
4
|
-
|
|
5
3
|
module ActiveJob
|
|
6
4
|
module Temporal
|
|
7
5
|
# Watches TLS certificate files and runs a reload callback when they change.
|
|
@@ -16,7 +14,7 @@ module ActiveJob
|
|
|
16
14
|
].compact.reject { |path| path.to_s.strip.empty? }
|
|
17
15
|
end
|
|
18
16
|
|
|
19
|
-
def initialize(paths:, reload_callback:, listener_factory:
|
|
17
|
+
def initialize(paths:, reload_callback:, listener_factory: nil, debounce_seconds: DEFAULT_DEBOUNCE_SECONDS)
|
|
20
18
|
@paths = paths.map { |path| File.expand_path(path) }.uniq
|
|
21
19
|
@reload_callback = reload_callback
|
|
22
20
|
@listener_factory = listener_factory
|
|
@@ -24,12 +22,13 @@ module ActiveJob
|
|
|
24
22
|
@mutex = Mutex.new
|
|
25
23
|
@last_reload_at = nil
|
|
26
24
|
@listener = nil
|
|
25
|
+
@trailing_reload = nil
|
|
27
26
|
end
|
|
28
27
|
|
|
29
28
|
def start
|
|
30
29
|
return self if @paths.empty? || @listener
|
|
31
30
|
|
|
32
|
-
@listener =
|
|
31
|
+
@listener = listener_factory.to(*directories) do |modified, added, removed|
|
|
33
32
|
handle_changes(modified + added + removed)
|
|
34
33
|
end
|
|
35
34
|
@listener.start
|
|
@@ -39,21 +38,57 @@ module ActiveJob
|
|
|
39
38
|
def stop
|
|
40
39
|
@listener&.stop
|
|
41
40
|
@listener = nil
|
|
41
|
+
@mutex.synchronize { @trailing_reload }&.kill
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
def handle_changes(changed_paths)
|
|
45
45
|
return unless relevant_change?(changed_paths)
|
|
46
|
-
return if debounced?
|
|
47
46
|
|
|
48
|
-
|
|
47
|
+
if debounced?
|
|
48
|
+
schedule_trailing_reload
|
|
49
|
+
else
|
|
50
|
+
reload(retry_on_failure: true)
|
|
51
|
+
end
|
|
49
52
|
end
|
|
50
53
|
|
|
51
54
|
private
|
|
52
55
|
|
|
56
|
+
# A failed reload clears the debounce stamp so the next change reloads
|
|
57
|
+
# immediately, and gets one retry in case no further change arrives.
|
|
58
|
+
def reload(retry_on_failure:)
|
|
59
|
+
@reload_callback.call
|
|
60
|
+
rescue StandardError
|
|
61
|
+
@mutex.synchronize { @last_reload_at = nil }
|
|
62
|
+
schedule_trailing_reload if retry_on_failure
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Cert and key rotate as separate writes, so the second one lands inside
|
|
66
|
+
# the debounce window. Flush it once the window closes instead of dropping it.
|
|
67
|
+
def schedule_trailing_reload
|
|
68
|
+
@mutex.synchronize do
|
|
69
|
+
return if @trailing_reload&.alive?
|
|
70
|
+
|
|
71
|
+
@trailing_reload = Thread.new do
|
|
72
|
+
sleep(@debounce_seconds)
|
|
73
|
+
@mutex.synchronize { @last_reload_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) }
|
|
74
|
+
reload(retry_on_failure: false)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
53
79
|
def directories
|
|
54
80
|
@directories ||= @paths.map { |path| File.dirname(path) }.uniq
|
|
55
81
|
end
|
|
56
82
|
|
|
83
|
+
def listener_factory
|
|
84
|
+
@listener_factory ||= begin
|
|
85
|
+
require "listen"
|
|
86
|
+
Listen
|
|
87
|
+
rescue LoadError => e
|
|
88
|
+
raise LoadError, "listen gem is required when tls_cert_watch is enabled: #{e.message}"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
57
92
|
def relevant_change?(changed_paths)
|
|
58
93
|
changed_paths.any? do |path|
|
|
59
94
|
@paths.include?(File.expand_path(path))
|
|
@@ -71,7 +71,8 @@ module ActiveJob
|
|
|
71
71
|
#
|
|
72
72
|
# @return [Temporalio::Client] Connected Temporal client
|
|
73
73
|
#
|
|
74
|
-
# @raise [ActiveJob::Temporal::
|
|
74
|
+
# @raise [ActiveJob::Temporal::TemporalConnectionError] if connection fails
|
|
75
|
+
# (includes target, namespace, and error message)
|
|
75
76
|
# @raise [OpenSSL::SSL::SSLError] if TLS certificate validation fails
|
|
76
77
|
# @raise [OpenSSL::PKey::RSAError] if TLS private key is invalid
|
|
77
78
|
# @raise [OpenSSL::X509::CertificateError] if TLS certificate is malformed
|
|
@@ -95,7 +96,7 @@ module ActiveJob
|
|
|
95
96
|
# @example Handling connection failures
|
|
96
97
|
# begin
|
|
97
98
|
# client = Client.build(config)
|
|
98
|
-
# rescue ActiveJob::Temporal::
|
|
99
|
+
# rescue ActiveJob::Temporal::TemporalConnectionError => e
|
|
99
100
|
# Rails.logger.fatal("Cannot connect to Temporal: #{e.message}")
|
|
100
101
|
# # Fall back to different adapter or alert operations team
|
|
101
102
|
# end
|
|
@@ -106,7 +107,7 @@ module ActiveJob
|
|
|
106
107
|
**connection_kwargs(configuration)
|
|
107
108
|
)
|
|
108
109
|
rescue StandardError => e
|
|
109
|
-
raise ActiveJob::Temporal::
|
|
110
|
+
raise ActiveJob::Temporal::TemporalConnectionError,
|
|
110
111
|
format(
|
|
111
112
|
"Unable to connect to Temporal at %<target>s (namespace: %<namespace>s): %<error>s",
|
|
112
113
|
target: configuration.target,
|
|
@@ -115,16 +116,33 @@ module ActiveJob
|
|
|
115
116
|
)
|
|
116
117
|
end
|
|
117
118
|
|
|
118
|
-
# Builds connection keyword arguments (
|
|
119
|
+
# Builds connection keyword arguments (TLS options and API key).
|
|
119
120
|
# @api private
|
|
120
121
|
def connection_kwargs(configuration)
|
|
122
|
+
kwargs = {}
|
|
123
|
+
|
|
121
124
|
tls = tls_options(configuration)
|
|
122
|
-
|
|
125
|
+
kwargs[:tls] = tls unless tls.nil?
|
|
126
|
+
|
|
127
|
+
api_key = resolve_api_key(configuration)
|
|
128
|
+
kwargs[:api_key] = api_key if api_key
|
|
123
129
|
|
|
124
|
-
|
|
130
|
+
kwargs
|
|
125
131
|
end
|
|
126
132
|
private_class_method :connection_kwargs
|
|
127
133
|
|
|
134
|
+
# Resolves the API key: an explicit api_key wins, otherwise api_key_file is read - at
|
|
135
|
+
# connection build time, and again by {ActiveJob::Temporal.refresh_api_key!} when the
|
|
136
|
+
# token file rotates.
|
|
137
|
+
# @api private
|
|
138
|
+
def resolve_api_key(configuration)
|
|
139
|
+
inline = configuration.api_key if configuration.respond_to?(:api_key)
|
|
140
|
+
return inline unless inline.nil? || inline.to_s.strip.empty?
|
|
141
|
+
|
|
142
|
+
path = configuration.api_key_file if configuration.respond_to?(:api_key_file)
|
|
143
|
+
read_tls_file(path)&.strip.presence
|
|
144
|
+
end
|
|
145
|
+
|
|
128
146
|
# Extracts TLS options from config or environment variables.
|
|
129
147
|
# @api private
|
|
130
148
|
def tls_options(configuration)
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "active_job"
|
|
4
4
|
require "active_support/concern"
|
|
5
|
+
require_relative "configured_job_compatibility"
|
|
5
6
|
|
|
6
7
|
module ActiveJob
|
|
7
8
|
module Temporal
|
|
@@ -39,7 +40,7 @@ module ActiveJob
|
|
|
39
40
|
# Adds conditional enqueue helpers to ActiveJob configured jobs.
|
|
40
41
|
module ConfiguredConditionalEnqueue
|
|
41
42
|
def perform_later_if(condition, *arguments, **keyword_arguments, &)
|
|
42
|
-
job_class =
|
|
43
|
+
job_class = ConfiguredJobCompatibility.job_class(self, feature: "conditional_enqueue")
|
|
43
44
|
condition_arguments = ConditionalEnqueue.job_arguments(arguments, keyword_arguments)
|
|
44
45
|
return nil unless ConditionalEnqueue.condition_allows_enqueue?(job_class, condition, condition_arguments)
|
|
45
46
|
|
|
@@ -19,28 +19,23 @@ module ActiveJob
|
|
|
19
19
|
end
|
|
20
20
|
alias configuration config
|
|
21
21
|
|
|
22
|
-
# Configures the gem with a block and validates
|
|
22
|
+
# Configures the gem with a block and validates before publishing changes.
|
|
23
23
|
#
|
|
24
24
|
# @yield [config] Gives the configuration object to the block
|
|
25
25
|
# @yieldparam config [Configuration] the configuration to modify
|
|
26
26
|
# @return [Configuration] the configuration object
|
|
27
27
|
# @raise [ConfigurationError] if validation fails after configuration
|
|
28
|
-
def configure
|
|
29
|
-
return config unless
|
|
28
|
+
def configure(&block)
|
|
29
|
+
return config unless block
|
|
30
30
|
|
|
31
31
|
@config_mvar ||= Concurrent::MVar.new(Configuration.new)
|
|
32
|
-
|
|
33
|
-
configuration.in_configure_block = true
|
|
32
|
+
applied_configuration = nil
|
|
34
33
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
ensure
|
|
38
|
-
configuration.in_configure_block = false
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
configuration.validate!
|
|
42
|
-
configuration
|
|
34
|
+
@config_mvar.modify do |current_configuration|
|
|
35
|
+
applied_configuration = build_configuration_candidate(current_configuration, &block)
|
|
43
36
|
end
|
|
37
|
+
|
|
38
|
+
applied_configuration
|
|
44
39
|
end
|
|
45
40
|
|
|
46
41
|
# Validates the current configuration.
|
|
@@ -50,6 +45,54 @@ module ActiveJob
|
|
|
50
45
|
def validate!
|
|
51
46
|
config.validate!
|
|
52
47
|
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def build_configuration_candidate(current_configuration)
|
|
52
|
+
candidate_configuration = current_configuration.dup
|
|
53
|
+
begin
|
|
54
|
+
candidate_configuration.in_configure_block = true
|
|
55
|
+
yield(candidate_configuration)
|
|
56
|
+
ensure
|
|
57
|
+
candidate_configuration.in_configure_block = false
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
candidate_configuration.validate!
|
|
61
|
+
deactivate_replaced_observability_adapters(current_configuration, candidate_configuration)
|
|
62
|
+
publish_configuration_candidate(current_configuration, candidate_configuration)
|
|
63
|
+
rescue StandardError
|
|
64
|
+
discard_configuration_candidate(current_configuration, candidate_configuration)
|
|
65
|
+
raise
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def publish_configuration_candidate(current_configuration, candidate_configuration)
|
|
69
|
+
current_configuration.instance_variable_set(
|
|
70
|
+
:@attributes,
|
|
71
|
+
candidate_configuration.instance_variable_get(:@attributes)
|
|
72
|
+
)
|
|
73
|
+
current_configuration.in_configure_block = false
|
|
74
|
+
current_configuration.finalize_configuration_copy!
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def deactivate_replaced_observability_adapters(previous_configuration, current_configuration)
|
|
78
|
+
previous_adapters = previous_configuration.observability.adapters
|
|
79
|
+
current_adapters = current_configuration.observability.adapters
|
|
80
|
+
|
|
81
|
+
previous_adapters.each do |adapter|
|
|
82
|
+
adapter.stop! unless current_adapters.include?(adapter)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def discard_configuration_candidate(previous_configuration, candidate_configuration)
|
|
87
|
+
previous_adapters = previous_configuration.observability.adapters
|
|
88
|
+
candidate_adapters = candidate_configuration.observability.adapters
|
|
89
|
+
|
|
90
|
+
candidate_adapters.each do |adapter|
|
|
91
|
+
adapter.stop! unless previous_adapters.include?(adapter)
|
|
92
|
+
end
|
|
93
|
+
rescue StandardError
|
|
94
|
+
nil
|
|
95
|
+
end
|
|
53
96
|
end
|
|
54
97
|
end
|
|
55
98
|
end
|