activejob-temporal 0.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +130 -0
- data/LICENSE +21 -0
- data/README.md +198 -0
- data/activejob-temporal.gemspec +58 -0
- data/api/job_payload_schema.json +318 -0
- data/bin/temporal-worker +295 -0
- data/lib/activejob/temporal/active_job_handler_source.rb +84 -0
- data/lib/activejob/temporal/activities/aj_runner_activity.rb +454 -0
- data/lib/activejob/temporal/activities/best_effort_side_effects.rb +49 -0
- data/lib/activejob/temporal/activities/dependency_status_activity.rb +160 -0
- data/lib/activejob/temporal/activities/rate_limit_activity.rb +41 -0
- data/lib/activejob/temporal/adapter.rb +257 -0
- data/lib/activejob/temporal/audit_log.rb +118 -0
- data/lib/activejob/temporal/batch_enqueue_result.rb +110 -0
- data/lib/activejob/temporal/batch_enqueuer.rb +141 -0
- data/lib/activejob/temporal/bind_policy.rb +44 -0
- data/lib/activejob/temporal/cancel/batch_canceller.rb +154 -0
- data/lib/activejob/temporal/cancel/batch_summary.rb +45 -0
- data/lib/activejob/temporal/cancel.rb +236 -0
- data/lib/activejob/temporal/certificate_watcher.rb +76 -0
- data/lib/activejob/temporal/chain_options.rb +83 -0
- data/lib/activejob/temporal/child_workflow_options.rb +102 -0
- data/lib/activejob/temporal/client.rb +215 -0
- data/lib/activejob/temporal/conditional_enqueue.rb +56 -0
- data/lib/activejob/temporal/configurable.rb +55 -0
- data/lib/activejob/temporal/configuration.rb +981 -0
- data/lib/activejob/temporal/configured_job_compatibility.rb +44 -0
- data/lib/activejob/temporal/connection_worker_pool.rb +88 -0
- data/lib/activejob/temporal/dead_letter_payload_validation.rb +34 -0
- data/lib/activejob/temporal/dead_letter_queue.rb +163 -0
- data/lib/activejob/temporal/dependency_options.rb +134 -0
- data/lib/activejob/temporal/external_operation.rb +193 -0
- data/lib/activejob/temporal/health_check_server.rb +159 -0
- data/lib/activejob/temporal/http_line_reader.rb +36 -0
- data/lib/activejob/temporal/inspect.rb +184 -0
- data/lib/activejob/temporal/job_descriptor.rb +37 -0
- data/lib/activejob/temporal/job_payload_builder.rb +209 -0
- data/lib/activejob/temporal/job_payload_chain_builder.rb +106 -0
- data/lib/activejob/temporal/job_payload_child_workflows.rb +127 -0
- data/lib/activejob/temporal/job_payload_dependencies.rb +40 -0
- data/lib/activejob/temporal/job_payload_rate_limits.rb +53 -0
- data/lib/activejob/temporal/job_payload_workflow_interactions.rb +31 -0
- data/lib/activejob/temporal/job_tags.rb +40 -0
- data/lib/activejob/temporal/locales/en.yml +126 -0
- data/lib/activejob/temporal/logger.rb +214 -0
- data/lib/activejob/temporal/metrics_server.rb +150 -0
- data/lib/activejob/temporal/middleware/chain.rb +106 -0
- data/lib/activejob/temporal/middleware.rb +11 -0
- data/lib/activejob/temporal/observability/datadog.rb +167 -0
- data/lib/activejob/temporal/observability/opentelemetry.rb +107 -0
- data/lib/activejob/temporal/observability/prometheus.rb +271 -0
- data/lib/activejob/temporal/observability.rb +260 -0
- data/lib/activejob/temporal/payload.rb +415 -0
- data/lib/activejob/temporal/payload_encryption.rb +215 -0
- data/lib/activejob/temporal/payload_serializers/json.rb +23 -0
- data/lib/activejob/temporal/payload_serializers/marshal.rb +53 -0
- data/lib/activejob/temporal/payload_serializers/message_pack.rb +59 -0
- data/lib/activejob/temporal/payload_serializers.rb +37 -0
- data/lib/activejob/temporal/payload_storage.rb +103 -0
- data/lib/activejob/temporal/rails_environment_loader.rb +143 -0
- data/lib/activejob/temporal/rate_limit_options.rb +94 -0
- data/lib/activejob/temporal/rate_limiters/memory.rb +198 -0
- data/lib/activejob/temporal/reload_signal_queue.rb +40 -0
- data/lib/activejob/temporal/retry_handler_extractor.rb +361 -0
- data/lib/activejob/temporal/retry_mapper.rb +264 -0
- data/lib/activejob/temporal/schedulable.rb +60 -0
- data/lib/activejob/temporal/schedule.rb +181 -0
- data/lib/activejob/temporal/schedule_options.rb +105 -0
- data/lib/activejob/temporal/search_attributes.rb +173 -0
- data/lib/activejob/temporal/signal_query.rb +161 -0
- data/lib/activejob/temporal/signal_query_options.rb +106 -0
- data/lib/activejob/temporal/temporal_options.rb +114 -0
- data/lib/activejob/temporal/tls_file.rb +45 -0
- data/lib/activejob/temporal/transaction_safety.rb +39 -0
- data/lib/activejob/temporal/version.rb +7 -0
- data/lib/activejob/temporal/visibility_query.rb +13 -0
- data/lib/activejob/temporal/worker_client_reloader.rb +34 -0
- data/lib/activejob/temporal/worker_health.rb +117 -0
- data/lib/activejob/temporal/worker_pool.rb +408 -0
- data/lib/activejob/temporal/workflow_enqueuer.rb +271 -0
- data/lib/activejob/temporal/workflow_enqueuer_batch.rb +17 -0
- data/lib/activejob/temporal/workflow_id_builder.rb +155 -0
- data/lib/activejob/temporal/workflow_identity.rb +62 -0
- data/lib/activejob/temporal/workflows/aj_workflow.rb +282 -0
- data/lib/activejob/temporal/workflows/dead_letter_support.rb +134 -0
- data/lib/activejob/temporal/workflows/dead_letter_workflow.rb +114 -0
- data/lib/activejob/temporal/workflows/workflow_chaining.rb +194 -0
- data/lib/activejob/temporal/workflows/workflow_child_workflows.rb +140 -0
- data/lib/activejob/temporal/workflows/workflow_continue_as_new.rb +44 -0
- data/lib/activejob/temporal/workflows/workflow_dependencies.rb +115 -0
- data/lib/activejob/temporal/workflows/workflow_execution_steps.rb +22 -0
- data/lib/activejob/temporal/workflows/workflow_interactions.rb +215 -0
- data/lib/activejob/temporal/workflows/workflow_local_activities.rb +29 -0
- data/lib/activejob/temporal/workflows/workflow_nexus.rb +15 -0
- data/lib/activejob/temporal/workflows/workflow_versioning.rb +21 -0
- data/lib/activejob/temporal.rb +297 -0
- data/lib/activejob-temporal.rb +3 -0
- metadata +423 -0
|
@@ -0,0 +1,981 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "logger"
|
|
4
|
+
require "active_support/core_ext/numeric/time"
|
|
5
|
+
require "active_model"
|
|
6
|
+
|
|
7
|
+
require_relative "middleware"
|
|
8
|
+
require_relative "observability"
|
|
9
|
+
require_relative "payload_encryption"
|
|
10
|
+
require_relative "payload_serializers"
|
|
11
|
+
require_relative "rate_limit_options"
|
|
12
|
+
require_relative "tls_file"
|
|
13
|
+
|
|
14
|
+
# rubocop:disable Metrics/ModuleLength
|
|
15
|
+
module ActiveJob
|
|
16
|
+
module Temporal
|
|
17
|
+
LOCALE_PATH = File.expand_path("locales/en.yml", __dir__)
|
|
18
|
+
I18n.load_path << LOCALE_PATH unless I18n.load_path.include?(LOCALE_PATH)
|
|
19
|
+
|
|
20
|
+
# Base error class for all activejob-temporal errors.
|
|
21
|
+
class Error < StandardError; end
|
|
22
|
+
|
|
23
|
+
# Raised when configuration is invalid.
|
|
24
|
+
#
|
|
25
|
+
# @see Configuration#validate!
|
|
26
|
+
class ConfigurationError < Error; end
|
|
27
|
+
|
|
28
|
+
# Raised when Temporal rejects a workflow start because the job was already enqueued.
|
|
29
|
+
class DuplicateEnqueueError < ActiveJob::EnqueueError; end
|
|
30
|
+
|
|
31
|
+
VALIDATION_LEVELS = %i[strict warn none].freeze
|
|
32
|
+
PAYLOAD_SERIALIZERS = PayloadSerializers::SUPPORTED
|
|
33
|
+
LOCAL_ACTIVITY_HELPERS = %i[rate_limit].freeze
|
|
34
|
+
UNTRAPPABLE_SIGNALS = %w[CHLD INT KILL PIPE QUIT STOP TERM].freeze
|
|
35
|
+
POSITIONAL_PARAMETER_TYPES = %i[req opt rest].freeze
|
|
36
|
+
MAX_TARGET_HOST_LENGTH = 253
|
|
37
|
+
MAX_NAMESPACE_LENGTH = 1000
|
|
38
|
+
TARGET_HOST_LABEL_PATTERN = /\A[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\z/
|
|
39
|
+
TARGET_PORT_PATTERN = /\A[1-9]\d{0,4}\z/
|
|
40
|
+
NAMESPACE_PATTERN = /\A[A-Za-z0-9](?:[A-Za-z0-9_-]*[A-Za-z0-9])?\z/
|
|
41
|
+
|
|
42
|
+
# Central registry of all configuration attributes.
|
|
43
|
+
#
|
|
44
|
+
# This is the single source of truth for attribute names, types, defaults, and env vars.
|
|
45
|
+
# Provides metadata for:
|
|
46
|
+
# - Automatic attribute accessor generation
|
|
47
|
+
# - Default value initialization (with lazy evaluation via Proc)
|
|
48
|
+
# - Environment variable mapping (e.g., TEMPORAL_TARGET)
|
|
49
|
+
# - Type information for validation and type coercion
|
|
50
|
+
#
|
|
51
|
+
# @example Accessing configuration metadata
|
|
52
|
+
# metadata = CONFIGURATION_ATTRIBUTES[:target]
|
|
53
|
+
# metadata[:default] # => "127.0.0.1:7233"
|
|
54
|
+
# metadata[:env_var] # => "TEMPORAL_TARGET"
|
|
55
|
+
# metadata[:type] # => :string
|
|
56
|
+
# metadata[:description] # => "Temporal server host:port"
|
|
57
|
+
#
|
|
58
|
+
# @api private
|
|
59
|
+
CONFIGURATION_ATTRIBUTES = {
|
|
60
|
+
# Connection Settings
|
|
61
|
+
target: {
|
|
62
|
+
default: "127.0.0.1:7233",
|
|
63
|
+
env_var: "ACTIVEJOB_TEMPORAL_TARGET",
|
|
64
|
+
type: :string,
|
|
65
|
+
description: "Temporal server host:port"
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
namespace: {
|
|
69
|
+
default: "default",
|
|
70
|
+
env_var: "ACTIVEJOB_TEMPORAL_NAMESPACE",
|
|
71
|
+
type: :string,
|
|
72
|
+
description: "Temporal namespace"
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
task_queue_prefix: {
|
|
76
|
+
default: nil,
|
|
77
|
+
env_var: "ACTIVEJOB_TEMPORAL_TASK_QUEUE_PREFIX",
|
|
78
|
+
type: :string,
|
|
79
|
+
description: "Optional prefix for task queue names"
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
task_queue: {
|
|
83
|
+
default: "default",
|
|
84
|
+
env_var: "ACTIVEJOB_TEMPORAL_TASK_QUEUE",
|
|
85
|
+
type: :string,
|
|
86
|
+
description: "Default task queue name for workers"
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
tls: {
|
|
90
|
+
default: nil,
|
|
91
|
+
type: :object,
|
|
92
|
+
description: "Optional SDK-native TLS options or hash-compatible TLS settings"
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
tls_cert_path: {
|
|
96
|
+
default: nil,
|
|
97
|
+
env_var: "ACTIVEJOB_TEMPORAL_TLS_CERT_PATH",
|
|
98
|
+
type: :string,
|
|
99
|
+
description: "Optional client certificate file path for mTLS"
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
tls_key_path: {
|
|
103
|
+
default: nil,
|
|
104
|
+
env_var: "ACTIVEJOB_TEMPORAL_TLS_KEY_PATH",
|
|
105
|
+
type: :string,
|
|
106
|
+
description: "Optional client private key file path for mTLS"
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
tls_server_root_ca_cert_path: {
|
|
110
|
+
default: nil,
|
|
111
|
+
env_var: "ACTIVEJOB_TEMPORAL_TLS_SERVER_ROOT_CA_CERT_PATH",
|
|
112
|
+
type: :string,
|
|
113
|
+
description: "Optional server root CA certificate file path for TLS verification"
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
tls_domain: {
|
|
117
|
+
default: nil,
|
|
118
|
+
env_var: "ACTIVEJOB_TEMPORAL_TLS_DOMAIN",
|
|
119
|
+
type: :string,
|
|
120
|
+
description: "Optional TLS SNI domain override"
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
tls_cert_watch: {
|
|
124
|
+
default: false,
|
|
125
|
+
env_var: "ACTIVEJOB_TEMPORAL_TLS_CERT_WATCH",
|
|
126
|
+
type: :boolean,
|
|
127
|
+
description: "Watch TLS certificate files and reload worker clients when they change"
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
tls_reload_signal: {
|
|
131
|
+
default: "HUP",
|
|
132
|
+
env_var: "ACTIVEJOB_TEMPORAL_TLS_RELOAD_SIGNAL",
|
|
133
|
+
type: :string,
|
|
134
|
+
description: "Signal used by workers to reload TLS certificates manually"
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
priority_task_queues: {
|
|
138
|
+
default: -> { {} },
|
|
139
|
+
type: :hash,
|
|
140
|
+
description: "Optional mapping from numeric ActiveJob priority values to Temporal task queues"
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
workflow_id_generator: {
|
|
144
|
+
default: nil,
|
|
145
|
+
type: :callable,
|
|
146
|
+
description: "Optional callable for custom Temporal workflow IDs"
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
rate_limiter: {
|
|
150
|
+
default: nil,
|
|
151
|
+
type: :object,
|
|
152
|
+
description: "Optional limiter backend responding to #wait_time_for or #call"
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
global_rate_limit: {
|
|
156
|
+
default: nil,
|
|
157
|
+
type: :hash,
|
|
158
|
+
description: "Optional global rate limit hash, for example { limit: 1000, per: :minute }"
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
# Timeouts (use Proc for lazy evaluation of ActiveSupport::Duration)
|
|
162
|
+
default_activity_timeout: {
|
|
163
|
+
default: -> { 15.minutes },
|
|
164
|
+
type: :duration,
|
|
165
|
+
description: "Default start_to_close_timeout for activity execution"
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
default_heartbeat_timeout: {
|
|
169
|
+
default: nil,
|
|
170
|
+
type: :duration,
|
|
171
|
+
description: "Default heartbeat_timeout for activity execution (optional)"
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
default_schedule_to_start_timeout: {
|
|
175
|
+
default: nil,
|
|
176
|
+
type: :duration,
|
|
177
|
+
description: "Default schedule_to_start_timeout for activity execution (optional)"
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
default_schedule_to_close_timeout: {
|
|
181
|
+
default: nil,
|
|
182
|
+
type: :duration,
|
|
183
|
+
description: "Default schedule_to_close_timeout for activity execution (optional)"
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
default_retry_initial_interval: {
|
|
187
|
+
default: -> { 30.seconds },
|
|
188
|
+
type: :duration,
|
|
189
|
+
description: "Initial retry interval"
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
# Retry Settings
|
|
193
|
+
default_retry_backoff: {
|
|
194
|
+
default: 2.0,
|
|
195
|
+
type: :float,
|
|
196
|
+
description: "Backoff coefficient for exponential retry"
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
default_retry_max_attempts: {
|
|
200
|
+
default: 1,
|
|
201
|
+
type: :integer,
|
|
202
|
+
description: "Maximum retry attempts for activities"
|
|
203
|
+
},
|
|
204
|
+
|
|
205
|
+
dead_letter_queue: {
|
|
206
|
+
default: nil,
|
|
207
|
+
env_var: "ACTIVEJOB_TEMPORAL_DEAD_LETTER_QUEUE",
|
|
208
|
+
type: :string,
|
|
209
|
+
description: "Optional Temporal task queue for failed job dead letter workflows"
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
dead_letter_after_attempts: {
|
|
213
|
+
default: nil,
|
|
214
|
+
env_var: "ACTIVEJOB_TEMPORAL_DEAD_LETTER_AFTER_ATTEMPTS",
|
|
215
|
+
type: :integer,
|
|
216
|
+
description: "Optional retry attempt limit before routing jobs to the dead letter queue"
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
dead_letter_auto_discard_after: {
|
|
220
|
+
default: nil,
|
|
221
|
+
env_var: "ACTIVEJOB_TEMPORAL_DEAD_LETTER_AUTO_DISCARD_AFTER_SECONDS",
|
|
222
|
+
type: :duration,
|
|
223
|
+
description: "Optional time to keep dead letter workflows queryable before auto-discarding them"
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
# Observability
|
|
227
|
+
logger: {
|
|
228
|
+
default: lambda {
|
|
229
|
+
(defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger) ||
|
|
230
|
+
::Logger.new($stdout)
|
|
231
|
+
},
|
|
232
|
+
type: :object,
|
|
233
|
+
description: "Logger instance for gem output"
|
|
234
|
+
},
|
|
235
|
+
|
|
236
|
+
audit_log: {
|
|
237
|
+
default: false,
|
|
238
|
+
env_var: "ACTIVEJOB_TEMPORAL_AUDIT_LOG",
|
|
239
|
+
type: :boolean,
|
|
240
|
+
description: "Enable structured audit events for job lifecycle changes"
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
audit_logger: {
|
|
244
|
+
default: nil,
|
|
245
|
+
type: :object,
|
|
246
|
+
description: "Optional logger instance for audit events; falls back to logger"
|
|
247
|
+
},
|
|
248
|
+
|
|
249
|
+
validation_level: {
|
|
250
|
+
default: :strict,
|
|
251
|
+
type: :symbol,
|
|
252
|
+
description: "Configuration validation behavior: :strict, :warn, or :none"
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
observability: {
|
|
256
|
+
default: -> { Observability::Configuration.new },
|
|
257
|
+
type: :object,
|
|
258
|
+
description: "Optional observability adapters and trace propagation settings"
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
middleware_chain: {
|
|
262
|
+
default: -> { Middleware::Chain.new },
|
|
263
|
+
type: :object,
|
|
264
|
+
description: "Ordered middleware chain for activity job execution"
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
identity: {
|
|
268
|
+
default: nil,
|
|
269
|
+
type: :string,
|
|
270
|
+
description: "Optional worker identity for observability"
|
|
271
|
+
},
|
|
272
|
+
|
|
273
|
+
# Payload & Performance
|
|
274
|
+
max_payload_size_kb: {
|
|
275
|
+
default: 250,
|
|
276
|
+
env_var: "ACTIVEJOB_TEMPORAL_MAX_PAYLOAD_SIZE_KB",
|
|
277
|
+
type: :integer,
|
|
278
|
+
description: "Maximum job payload size in kilobytes"
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
payload_serializer: {
|
|
282
|
+
default: :json,
|
|
283
|
+
env_var: "ACTIVEJOB_TEMPORAL_PAYLOAD_SERIALIZER",
|
|
284
|
+
type: :symbol,
|
|
285
|
+
description: "Payload serializer for job execution data: :json, :message_pack, :msgpack, or :marshal"
|
|
286
|
+
},
|
|
287
|
+
|
|
288
|
+
payload_storage_adapter: {
|
|
289
|
+
default: nil,
|
|
290
|
+
type: :object,
|
|
291
|
+
description: "Optional external payload storage adapter responding to #dump and #load"
|
|
292
|
+
},
|
|
293
|
+
|
|
294
|
+
payload_storage_threshold_kb: {
|
|
295
|
+
default: nil,
|
|
296
|
+
type: :integer,
|
|
297
|
+
description: "Optional payload size threshold in kilobytes for external payload storage"
|
|
298
|
+
},
|
|
299
|
+
|
|
300
|
+
encrypt_payload: {
|
|
301
|
+
default: false,
|
|
302
|
+
env_var: "ACTIVEJOB_TEMPORAL_ENCRYPT_PAYLOAD",
|
|
303
|
+
type: :boolean,
|
|
304
|
+
description: "Encrypt serialized job execution payloads before sending them to Temporal"
|
|
305
|
+
},
|
|
306
|
+
|
|
307
|
+
encryption_key: {
|
|
308
|
+
default: nil,
|
|
309
|
+
env_var: "ACTIVEJOB_TEMPORAL_ENCRYPTION_KEY",
|
|
310
|
+
type: :object,
|
|
311
|
+
description: "Base64-encoded 32-byte AES-256-GCM payload encryption key or key metadata"
|
|
312
|
+
},
|
|
313
|
+
|
|
314
|
+
encryption_old_keys: {
|
|
315
|
+
default: -> { [] },
|
|
316
|
+
type: :array,
|
|
317
|
+
description: "Previous payload encryption keys or key metadata accepted for decryption"
|
|
318
|
+
},
|
|
319
|
+
|
|
320
|
+
enable_search_attributes: {
|
|
321
|
+
default: true,
|
|
322
|
+
type: :boolean,
|
|
323
|
+
description: "Enable Temporal search attributes for job metadata"
|
|
324
|
+
},
|
|
325
|
+
|
|
326
|
+
continue_as_new_history_event_threshold: {
|
|
327
|
+
default: nil,
|
|
328
|
+
env_var: "ACTIVEJOB_TEMPORAL_CONTINUE_AS_NEW_HISTORY_EVENT_THRESHOLD",
|
|
329
|
+
type: :integer,
|
|
330
|
+
description: "Optional workflow history event threshold for continuing ActiveJob workflows as new"
|
|
331
|
+
},
|
|
332
|
+
|
|
333
|
+
local_activity_helpers: {
|
|
334
|
+
default: -> { [] },
|
|
335
|
+
type: :array,
|
|
336
|
+
description: "Internal helper activity names that should run as Temporal local activities"
|
|
337
|
+
},
|
|
338
|
+
|
|
339
|
+
max_concurrent_activities: {
|
|
340
|
+
default: 100,
|
|
341
|
+
env_var: "ACTIVEJOB_TEMPORAL_MAX_CONCURRENT_ACTIVITIES",
|
|
342
|
+
type: :integer,
|
|
343
|
+
description: "Maximum concurrent activities per worker"
|
|
344
|
+
},
|
|
345
|
+
|
|
346
|
+
max_concurrent_workflow_tasks: {
|
|
347
|
+
default: 5,
|
|
348
|
+
env_var: "ACTIVEJOB_TEMPORAL_MAX_CONCURRENT_WORKFLOW_TASKS",
|
|
349
|
+
type: :integer,
|
|
350
|
+
description: "Maximum concurrent workflow tasks per worker"
|
|
351
|
+
}
|
|
352
|
+
}.freeze
|
|
353
|
+
|
|
354
|
+
# Configuration object for the activejob-temporal gem.
|
|
355
|
+
#
|
|
356
|
+
# Holds connection settings, timeouts, retry policies, and operational flags.
|
|
357
|
+
# Use {ActiveJob::Temporal.configure} to set values with automatic validation.
|
|
358
|
+
#
|
|
359
|
+
# @note Thread Safety
|
|
360
|
+
# The global configuration object is synchronized by {Configurable} while
|
|
361
|
+
# configure blocks mutate it. Complete configuration during application boot.
|
|
362
|
+
#
|
|
363
|
+
# @note Environment Variable Defaults
|
|
364
|
+
# ACTIVEJOB_TEMPORAL_TARGET, ACTIVEJOB_TEMPORAL_NAMESPACE,
|
|
365
|
+
# ACTIVEJOB_TEMPORAL_TASK_QUEUE_PREFIX, ACTIVEJOB_TEMPORAL_TASK_QUEUE,
|
|
366
|
+
# ACTIVEJOB_TEMPORAL_MAX_PAYLOAD_SIZE_KB,
|
|
367
|
+
# ACTIVEJOB_TEMPORAL_MAX_CONCURRENT_ACTIVITIES,
|
|
368
|
+
# ACTIVEJOB_TEMPORAL_MAX_CONCURRENT_WORKFLOW_TASKS,
|
|
369
|
+
# can provide defaults.
|
|
370
|
+
#
|
|
371
|
+
# @see ConfigValidator
|
|
372
|
+
# @see ActiveJob::Temporal.configure
|
|
373
|
+
# @api private
|
|
374
|
+
class Configuration
|
|
375
|
+
attr_accessor :in_configure_block
|
|
376
|
+
|
|
377
|
+
CONFIGURATION_ATTRIBUTES.each_key do |attribute|
|
|
378
|
+
define_method(attribute) do
|
|
379
|
+
@attributes[attribute]
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
define_method("#{attribute}=") do |value|
|
|
383
|
+
@attributes[attribute] = value
|
|
384
|
+
|
|
385
|
+
value
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def initialize
|
|
390
|
+
@attributes = {}
|
|
391
|
+
@in_configure_block = false
|
|
392
|
+
CONFIGURATION_ATTRIBUTES.each do |attribute, metadata|
|
|
393
|
+
@attributes[attribute] = resolve_default_value(metadata)
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def [](key)
|
|
398
|
+
@attributes[key]
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
def []=(key, value)
|
|
402
|
+
@attributes[key] = value
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def add_middleware(middleware, ...)
|
|
406
|
+
middleware_chain.add(middleware, ...)
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def validate!
|
|
410
|
+
return if validation_level == :none
|
|
411
|
+
|
|
412
|
+
validator = build_validator
|
|
413
|
+
return if validator.valid?
|
|
414
|
+
|
|
415
|
+
handle_validation_errors(validator.errors)
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
def self.format_validation_errors(errors)
|
|
419
|
+
messages = errors.full_messages
|
|
420
|
+
return messages.first if messages.size == 1
|
|
421
|
+
|
|
422
|
+
error_list = messages.each_with_index.map do |message, index|
|
|
423
|
+
" #{index + 1}. #{message}"
|
|
424
|
+
end.join("\n")
|
|
425
|
+
|
|
426
|
+
plural = "s" if messages.size > 1
|
|
427
|
+
"Configuration validation failed with #{messages.size} error#{plural}:\n#{error_list}"
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
private
|
|
431
|
+
|
|
432
|
+
def build_validator
|
|
433
|
+
validator = ConfigValidator.new
|
|
434
|
+
|
|
435
|
+
CONFIGURATION_ATTRIBUTES.each_key do |attribute|
|
|
436
|
+
validator.public_send("#{attribute}=", @attributes[attribute])
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
validator
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def handle_validation_errors(errors)
|
|
443
|
+
message = self.class.format_validation_errors(errors)
|
|
444
|
+
|
|
445
|
+
raise ConfigurationError, message unless validation_level == :warn
|
|
446
|
+
|
|
447
|
+
logger.warn(message)
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def resolve_default_value(metadata)
|
|
451
|
+
if metadata[:env_var] && ENV[metadata[:env_var]]
|
|
452
|
+
value = ENV[metadata[:env_var]]
|
|
453
|
+
return convert_env_value(value, metadata[:type])
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
default = metadata[:default]
|
|
457
|
+
default.is_a?(Proc) ? default.call : default
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def convert_env_value(value, type)
|
|
461
|
+
case type
|
|
462
|
+
when :integer then value.to_i
|
|
463
|
+
when :float, :duration then value.to_f
|
|
464
|
+
when :boolean then value == "true"
|
|
465
|
+
when :symbol then value.to_sym
|
|
466
|
+
else value
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
# Validates ActiveJob::Temporal configuration.
|
|
472
|
+
#
|
|
473
|
+
# This class uses ActiveModel::Validations to provide declarative validation
|
|
474
|
+
# with i18n support. Attributes are automatically synchronized from the
|
|
475
|
+
# configuration via metaprogramming.
|
|
476
|
+
#
|
|
477
|
+
# **Validation Rules:**
|
|
478
|
+
# - `target`: Presence + host:port format validation
|
|
479
|
+
# - `namespace`: Presence + alphanumeric/hyphens/underscores format
|
|
480
|
+
# - `default_activity_timeout`: Duration type + positive value
|
|
481
|
+
# - `default_retry_initial_interval`: Duration type + positive value
|
|
482
|
+
# - `default_retry_backoff`: Numericality >= 1.0
|
|
483
|
+
# - `default_retry_max_attempts`: Numericality >= 0
|
|
484
|
+
# - `max_payload_size_kb`: Numericality 1..2GB
|
|
485
|
+
# - `max_concurrent_activities`: Numericality > 0
|
|
486
|
+
# - `max_concurrent_workflow_tasks`: Numericality > 0
|
|
487
|
+
# - `workflow_id_generator`: Optional callable
|
|
488
|
+
# - `middleware_chain`: Callable chain with registration support
|
|
489
|
+
#
|
|
490
|
+
# @example Using ConfigValidator directly
|
|
491
|
+
# validator = ConfigValidator.new
|
|
492
|
+
# validator.target = "localhost:7233"
|
|
493
|
+
# validator.namespace = "production"
|
|
494
|
+
# if validator.valid?
|
|
495
|
+
# puts "Configuration is valid"
|
|
496
|
+
# else
|
|
497
|
+
# puts validator.errors.full_messages
|
|
498
|
+
# end
|
|
499
|
+
#
|
|
500
|
+
# @see ActiveJob::Temporal::Configuration.validate!
|
|
501
|
+
# @api private
|
|
502
|
+
# rubocop:disable Metrics/ClassLength
|
|
503
|
+
class ConfigValidator
|
|
504
|
+
include ActiveModel::Validations
|
|
505
|
+
|
|
506
|
+
# Generate attr_accessor for all configuration attributes
|
|
507
|
+
attr_accessor(*CONFIGURATION_ATTRIBUTES.keys)
|
|
508
|
+
|
|
509
|
+
#
|
|
510
|
+
# STANDARD VALIDATIONS
|
|
511
|
+
# Use ActiveModel's built-in validators where possible
|
|
512
|
+
#
|
|
513
|
+
|
|
514
|
+
# Connection Settings
|
|
515
|
+
validates :target,
|
|
516
|
+
presence: { message: :target_required }
|
|
517
|
+
|
|
518
|
+
validates :namespace,
|
|
519
|
+
presence: { message: :namespace_required }
|
|
520
|
+
|
|
521
|
+
# Retry Settings
|
|
522
|
+
validates :default_retry_backoff,
|
|
523
|
+
numericality: {
|
|
524
|
+
greater_than_or_equal_to: 1.0,
|
|
525
|
+
message: :retry_backoff_too_small,
|
|
526
|
+
allow_nil: false
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
validates :default_retry_max_attempts,
|
|
530
|
+
numericality: {
|
|
531
|
+
greater_than_or_equal_to: 0,
|
|
532
|
+
only_integer: true,
|
|
533
|
+
message: :retry_max_attempts_negative,
|
|
534
|
+
allow_nil: false
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
# Payload & Performance
|
|
538
|
+
validates :max_payload_size_kb,
|
|
539
|
+
numericality: {
|
|
540
|
+
greater_than: 0,
|
|
541
|
+
less_than_or_equal_to: 2_097_152, # 2 GB in KB
|
|
542
|
+
only_integer: true,
|
|
543
|
+
message: :payload_size_invalid,
|
|
544
|
+
allow_nil: false
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
validates :max_concurrent_activities,
|
|
548
|
+
numericality: {
|
|
549
|
+
greater_than: 0,
|
|
550
|
+
only_integer: true,
|
|
551
|
+
message: :concurrent_activities_invalid,
|
|
552
|
+
allow_nil: false
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
validates :max_concurrent_workflow_tasks,
|
|
556
|
+
numericality: {
|
|
557
|
+
greater_than: 0,
|
|
558
|
+
only_integer: true,
|
|
559
|
+
message: :concurrent_workflow_tasks_invalid,
|
|
560
|
+
allow_nil: false
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
validates :continue_as_new_history_event_threshold,
|
|
564
|
+
numericality: {
|
|
565
|
+
greater_than: 0,
|
|
566
|
+
only_integer: true,
|
|
567
|
+
allow_nil: true
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
validates :validation_level,
|
|
571
|
+
inclusion: {
|
|
572
|
+
in: VALIDATION_LEVELS,
|
|
573
|
+
message: :invalid_level
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
validates :payload_serializer,
|
|
577
|
+
inclusion: {
|
|
578
|
+
in: PAYLOAD_SERIALIZERS,
|
|
579
|
+
message: :unsupported_serializer
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
#
|
|
583
|
+
# CUSTOM VALIDATORS
|
|
584
|
+
# For complex logic that doesn't fit standard validators
|
|
585
|
+
#
|
|
586
|
+
|
|
587
|
+
validate :validate_target_format
|
|
588
|
+
validate :validate_namespace_format
|
|
589
|
+
validate :validate_duration_values
|
|
590
|
+
validate :validate_workflow_id_generator
|
|
591
|
+
validate :validate_middleware_chain
|
|
592
|
+
validate :validate_priority_task_queues
|
|
593
|
+
validate :validate_rate_limit_settings
|
|
594
|
+
validate :validate_dead_letter_settings
|
|
595
|
+
validate :validate_observability_settings
|
|
596
|
+
validate :validate_audit_settings
|
|
597
|
+
validate :validate_encryption_settings
|
|
598
|
+
validate :validate_payload_storage_settings
|
|
599
|
+
validate :validate_tls_settings
|
|
600
|
+
validate :validate_local_activity_helpers
|
|
601
|
+
|
|
602
|
+
private
|
|
603
|
+
|
|
604
|
+
def validate_target_format
|
|
605
|
+
return if target.blank? # presence validation handles nil/empty
|
|
606
|
+
|
|
607
|
+
host, port = split_target
|
|
608
|
+
return if host && port && valid_target_host?(host) && valid_target_port?(port)
|
|
609
|
+
|
|
610
|
+
errors.add(:target, :invalid_format, target: target)
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
def validate_namespace_format
|
|
614
|
+
return if namespace.blank?
|
|
615
|
+
return if namespace.length <= MAX_NAMESPACE_LENGTH && namespace.match?(NAMESPACE_PATTERN)
|
|
616
|
+
|
|
617
|
+
errors.add(:namespace, :invalid_format, namespace: namespace)
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
def split_target
|
|
621
|
+
return unless target.count(":") == 1
|
|
622
|
+
|
|
623
|
+
target.split(":", 2)
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
def valid_target_host?(host)
|
|
627
|
+
return false if host.length > MAX_TARGET_HOST_LENGTH
|
|
628
|
+
|
|
629
|
+
labels = host.split(".", -1)
|
|
630
|
+
labels.any? && labels.all? { |label| label.match?(TARGET_HOST_LABEL_PATTERN) }
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
def valid_target_port?(port)
|
|
634
|
+
port.match?(TARGET_PORT_PATTERN) && port.to_i.between?(1, 65_535)
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
# Validates duration attributes are positive durations
|
|
638
|
+
def validate_duration_values
|
|
639
|
+
# Required duration attributes
|
|
640
|
+
validate_duration_attribute(:default_activity_timeout, required: true)
|
|
641
|
+
validate_duration_attribute(:default_retry_initial_interval, required: true)
|
|
642
|
+
|
|
643
|
+
# Optional duration attributes
|
|
644
|
+
validate_duration_attribute(:default_heartbeat_timeout, required: false)
|
|
645
|
+
validate_duration_attribute(:default_schedule_to_start_timeout, required: false)
|
|
646
|
+
validate_duration_attribute(:default_schedule_to_close_timeout, required: false)
|
|
647
|
+
validate_duration_attribute(:dead_letter_auto_discard_after, required: false)
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
def validate_workflow_id_generator
|
|
651
|
+
return if workflow_id_generator.nil?
|
|
652
|
+
|
|
653
|
+
unless workflow_id_generator.respond_to?(:call)
|
|
654
|
+
errors.add(:workflow_id_generator, :not_callable, value: workflow_id_generator.inspect)
|
|
655
|
+
return
|
|
656
|
+
end
|
|
657
|
+
|
|
658
|
+
return if callable_accepts_positional_job?(workflow_id_generator)
|
|
659
|
+
|
|
660
|
+
errors.add(:workflow_id_generator, :wrong_arity, value: workflow_id_generator.inspect)
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
def validate_middleware_chain
|
|
664
|
+
return if middleware_chain.respond_to?(:add) && middleware_chain.respond_to?(:call)
|
|
665
|
+
|
|
666
|
+
errors.add(:middleware_chain, :invalid, value: middleware_chain.inspect)
|
|
667
|
+
end
|
|
668
|
+
|
|
669
|
+
def validate_priority_task_queues
|
|
670
|
+
unless priority_task_queues.is_a?(Hash)
|
|
671
|
+
errors.add(:priority_task_queues, :not_a_hash, value: priority_task_queues.inspect)
|
|
672
|
+
return
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
if non_integer_priority_key
|
|
676
|
+
errors.add(:priority_task_queues, :non_integer_priority, value: non_integer_priority_key.inspect)
|
|
677
|
+
return
|
|
678
|
+
end
|
|
679
|
+
|
|
680
|
+
return unless blank_priority_task_queue
|
|
681
|
+
|
|
682
|
+
errors.add(:priority_task_queues, :blank_queue, value: blank_priority_task_queue.inspect)
|
|
683
|
+
end
|
|
684
|
+
|
|
685
|
+
def non_integer_priority_key
|
|
686
|
+
priority_task_queues.keys.find { |priority| !priority.is_a?(Integer) }
|
|
687
|
+
end
|
|
688
|
+
|
|
689
|
+
def blank_priority_task_queue
|
|
690
|
+
priority_task_queues.values.find { |task_queue| task_queue.to_s.strip.empty? }
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
def validate_rate_limit_settings
|
|
694
|
+
validate_rate_limiter
|
|
695
|
+
validate_global_rate_limit
|
|
696
|
+
end
|
|
697
|
+
|
|
698
|
+
def validate_local_activity_helpers
|
|
699
|
+
unless local_activity_helpers.is_a?(Array)
|
|
700
|
+
errors.add(:local_activity_helpers, :invalid, value: local_activity_helpers.inspect)
|
|
701
|
+
return
|
|
702
|
+
end
|
|
703
|
+
|
|
704
|
+
unsupported_helper = local_activity_helpers.find do |helper|
|
|
705
|
+
!helper.respond_to?(:to_sym) || !LOCAL_ACTIVITY_HELPERS.include?(helper.to_sym)
|
|
706
|
+
end
|
|
707
|
+
errors.add(:local_activity_helpers, :invalid, value: unsupported_helper.inspect) if unsupported_helper
|
|
708
|
+
end
|
|
709
|
+
|
|
710
|
+
def validate_rate_limiter
|
|
711
|
+
return if rate_limiter.nil?
|
|
712
|
+
|
|
713
|
+
unless rate_limiter.respond_to?(:wait_time_for) || rate_limiter.respond_to?(:call)
|
|
714
|
+
errors.add(:rate_limiter, :invalid, value: rate_limiter.inspect)
|
|
715
|
+
return
|
|
716
|
+
end
|
|
717
|
+
|
|
718
|
+
return if rate_limiter_accepts_rate_limits_argument?
|
|
719
|
+
|
|
720
|
+
errors.add(:rate_limiter, :wrong_arity, value: rate_limiter.inspect)
|
|
721
|
+
end
|
|
722
|
+
|
|
723
|
+
def validate_global_rate_limit
|
|
724
|
+
return if global_rate_limit.nil?
|
|
725
|
+
|
|
726
|
+
if rate_limiter.nil?
|
|
727
|
+
errors.add(:global_rate_limit, :requires_rate_limiter)
|
|
728
|
+
return
|
|
729
|
+
end
|
|
730
|
+
|
|
731
|
+
RateLimitOptions.normalize_hash(global_rate_limit)
|
|
732
|
+
rescue ArgumentError
|
|
733
|
+
errors.add(:global_rate_limit, :invalid, value: global_rate_limit.inspect)
|
|
734
|
+
end
|
|
735
|
+
|
|
736
|
+
def validate_dead_letter_settings
|
|
737
|
+
validate_dead_letter_queue
|
|
738
|
+
validate_dead_letter_after_attempts
|
|
739
|
+
validate_dead_letter_auto_discard_after
|
|
740
|
+
end
|
|
741
|
+
|
|
742
|
+
def validate_dead_letter_queue
|
|
743
|
+
return if dead_letter_queue.nil? || dead_letter_queue.to_s.strip.present?
|
|
744
|
+
|
|
745
|
+
errors.add(:dead_letter_queue, :blank)
|
|
746
|
+
end
|
|
747
|
+
|
|
748
|
+
def validate_dead_letter_after_attempts
|
|
749
|
+
return if dead_letter_after_attempts.nil?
|
|
750
|
+
|
|
751
|
+
unless dead_letter_queue.to_s.strip.present?
|
|
752
|
+
errors.add(:dead_letter_after_attempts, :requires_queue)
|
|
753
|
+
return
|
|
754
|
+
end
|
|
755
|
+
|
|
756
|
+
return if dead_letter_after_attempts.is_a?(Integer) && dead_letter_after_attempts.positive?
|
|
757
|
+
|
|
758
|
+
errors.add(:dead_letter_after_attempts, :invalid, value: dead_letter_after_attempts.inspect)
|
|
759
|
+
end
|
|
760
|
+
|
|
761
|
+
def validate_dead_letter_auto_discard_after
|
|
762
|
+
return if dead_letter_auto_discard_after.nil?
|
|
763
|
+
return if dead_letter_queue.to_s.strip.present?
|
|
764
|
+
|
|
765
|
+
errors.add(:dead_letter_auto_discard_after, :requires_queue)
|
|
766
|
+
end
|
|
767
|
+
|
|
768
|
+
def validate_observability_settings
|
|
769
|
+
return if observability.respond_to?(:validate!)
|
|
770
|
+
|
|
771
|
+
errors.add(:observability, :invalid, value: observability.inspect)
|
|
772
|
+
end
|
|
773
|
+
|
|
774
|
+
def validate_audit_settings
|
|
775
|
+
validate_audit_log
|
|
776
|
+
validate_audit_logger
|
|
777
|
+
end
|
|
778
|
+
|
|
779
|
+
def validate_audit_log
|
|
780
|
+
return if [true, false].include?(audit_log)
|
|
781
|
+
|
|
782
|
+
errors.add(:audit_log, :not_boolean, value: audit_log.inspect)
|
|
783
|
+
end
|
|
784
|
+
|
|
785
|
+
def validate_audit_logger
|
|
786
|
+
return if audit_logger.nil? || audit_logger.respond_to?(:info)
|
|
787
|
+
|
|
788
|
+
errors.add(:audit_logger, :invalid, value: audit_logger.inspect)
|
|
789
|
+
end
|
|
790
|
+
|
|
791
|
+
def validate_encryption_settings
|
|
792
|
+
validate_encrypt_payload
|
|
793
|
+
validate_encryption_key
|
|
794
|
+
validate_encryption_old_keys
|
|
795
|
+
end
|
|
796
|
+
|
|
797
|
+
def validate_encrypt_payload
|
|
798
|
+
return if [true, false].include?(encrypt_payload)
|
|
799
|
+
|
|
800
|
+
errors.add(:encrypt_payload, :not_boolean, value: encrypt_payload.inspect)
|
|
801
|
+
end
|
|
802
|
+
|
|
803
|
+
def validate_encryption_key
|
|
804
|
+
if encrypt_payload && encryption_key.to_s.empty?
|
|
805
|
+
errors.add(:encryption_key, :required)
|
|
806
|
+
return
|
|
807
|
+
end
|
|
808
|
+
|
|
809
|
+
return if encryption_key.nil? || PayloadEncryption.valid_key?(encryption_key)
|
|
810
|
+
|
|
811
|
+
errors.add(:encryption_key, :invalid, bytes: PayloadEncryption.key_length, value: "[FILTERED]")
|
|
812
|
+
end
|
|
813
|
+
|
|
814
|
+
def validate_encryption_old_keys
|
|
815
|
+
unless encryption_old_keys.is_a?(Array)
|
|
816
|
+
errors.add(:encryption_old_keys, :not_an_array, value: encryption_old_keys.inspect)
|
|
817
|
+
return
|
|
818
|
+
end
|
|
819
|
+
|
|
820
|
+
return if encryption_old_keys.all? { |key| PayloadEncryption.valid_key?(key) }
|
|
821
|
+
|
|
822
|
+
errors.add(:encryption_old_keys, :invalid, bytes: PayloadEncryption.key_length, value: "[FILTERED]")
|
|
823
|
+
end
|
|
824
|
+
|
|
825
|
+
def validate_payload_storage_settings
|
|
826
|
+
validate_payload_storage_adapter
|
|
827
|
+
validate_payload_storage_threshold
|
|
828
|
+
end
|
|
829
|
+
|
|
830
|
+
def validate_payload_storage_adapter
|
|
831
|
+
return if payload_storage_adapter.nil?
|
|
832
|
+
return if payload_storage_adapter.respond_to?(:dump) && payload_storage_adapter.respond_to?(:load)
|
|
833
|
+
|
|
834
|
+
errors.add(:payload_storage_adapter, :invalid, value: payload_storage_adapter.inspect)
|
|
835
|
+
end
|
|
836
|
+
|
|
837
|
+
def validate_payload_storage_threshold
|
|
838
|
+
if payload_storage_adapter && payload_storage_threshold_kb.nil?
|
|
839
|
+
errors.add(:payload_storage_threshold_kb, :required)
|
|
840
|
+
return
|
|
841
|
+
end
|
|
842
|
+
|
|
843
|
+
if payload_storage_threshold_kb && payload_storage_adapter.nil?
|
|
844
|
+
errors.add(:payload_storage_threshold_kb, :requires_adapter)
|
|
845
|
+
return
|
|
846
|
+
end
|
|
847
|
+
|
|
848
|
+
return if payload_storage_threshold_kb.nil?
|
|
849
|
+
return if payload_storage_threshold_kb.is_a?(Integer) && payload_storage_threshold_kb.positive?
|
|
850
|
+
|
|
851
|
+
errors.add(:payload_storage_threshold_kb, :invalid, value: payload_storage_threshold_kb.inspect)
|
|
852
|
+
end
|
|
853
|
+
|
|
854
|
+
def validate_tls_settings
|
|
855
|
+
validate_tls_cert_key_pair
|
|
856
|
+
validate_tls_file_path(:tls_cert_path)
|
|
857
|
+
validate_tls_file_path(:tls_key_path)
|
|
858
|
+
validate_tls_file_path(:tls_server_root_ca_cert_path)
|
|
859
|
+
validate_tls_domain
|
|
860
|
+
validate_tls_cert_watch
|
|
861
|
+
validate_tls_reload_signal
|
|
862
|
+
end
|
|
863
|
+
|
|
864
|
+
def validate_tls_cert_key_pair
|
|
865
|
+
return if tls_cert_path.to_s.empty? == tls_key_path.to_s.empty?
|
|
866
|
+
|
|
867
|
+
errors.add(:tls_cert_path, :requires_key_path)
|
|
868
|
+
end
|
|
869
|
+
|
|
870
|
+
def validate_tls_file_path(attribute)
|
|
871
|
+
path = public_send(attribute)
|
|
872
|
+
return if path.nil?
|
|
873
|
+
|
|
874
|
+
unless path.is_a?(String) && path.strip.present?
|
|
875
|
+
errors.add(attribute, :invalid_path, value: path.inspect)
|
|
876
|
+
return
|
|
877
|
+
end
|
|
878
|
+
|
|
879
|
+
return if TLSFile.readable_regular_file?(path)
|
|
880
|
+
|
|
881
|
+
errors.add(attribute, :unreadable_path, value: path)
|
|
882
|
+
end
|
|
883
|
+
|
|
884
|
+
def validate_tls_domain
|
|
885
|
+
return if tls_domain.nil? || tls_domain.to_s.strip.present?
|
|
886
|
+
|
|
887
|
+
errors.add(:tls_domain, :blank)
|
|
888
|
+
end
|
|
889
|
+
|
|
890
|
+
def validate_tls_cert_watch
|
|
891
|
+
unless [true, false].include?(tls_cert_watch)
|
|
892
|
+
errors.add(:tls_cert_watch, :not_boolean, value: tls_cert_watch.inspect)
|
|
893
|
+
return
|
|
894
|
+
end
|
|
895
|
+
|
|
896
|
+
return unless tls_cert_watch && tls_watch_paths.empty?
|
|
897
|
+
|
|
898
|
+
errors.add(:tls_cert_watch, :requires_paths)
|
|
899
|
+
end
|
|
900
|
+
|
|
901
|
+
def validate_tls_reload_signal
|
|
902
|
+
unless tls_reload_signal.is_a?(String) && tls_reload_signal.strip.present?
|
|
903
|
+
errors.add(:tls_reload_signal, :blank)
|
|
904
|
+
return
|
|
905
|
+
end
|
|
906
|
+
|
|
907
|
+
normalized_signal = tls_reload_signal.sub(/\ASIG/i, "").upcase
|
|
908
|
+
return if Signal.list.key?(normalized_signal) && !UNTRAPPABLE_SIGNALS.include?(normalized_signal)
|
|
909
|
+
|
|
910
|
+
errors.add(:tls_reload_signal, :invalid, value: tls_reload_signal.inspect)
|
|
911
|
+
end
|
|
912
|
+
|
|
913
|
+
def tls_watch_paths
|
|
914
|
+
[tls_cert_path, tls_key_path, tls_server_root_ca_cert_path].compact.reject { |path| path.to_s.strip.empty? }
|
|
915
|
+
end
|
|
916
|
+
|
|
917
|
+
def callable_accepts_positional_job?(callable)
|
|
918
|
+
callable_accepts_one_positional_argument?(callable)
|
|
919
|
+
end
|
|
920
|
+
|
|
921
|
+
def rate_limiter_accepts_rate_limits_argument?
|
|
922
|
+
callable = if rate_limiter.respond_to?(:wait_time_for)
|
|
923
|
+
rate_limiter.method(:wait_time_for)
|
|
924
|
+
else
|
|
925
|
+
rate_limiter
|
|
926
|
+
end
|
|
927
|
+
|
|
928
|
+
callable_accepts_one_positional_argument?(callable)
|
|
929
|
+
end
|
|
930
|
+
|
|
931
|
+
def callable_accepts_one_positional_argument?(callable)
|
|
932
|
+
parameters = callable_parameters(callable)
|
|
933
|
+
return accepts_one_positional_argument_from_parameters?(parameters) if parameters
|
|
934
|
+
|
|
935
|
+
arity = callable.respond_to?(:arity) ? callable.arity : callable.method(:call).arity
|
|
936
|
+
|
|
937
|
+
arity == 1 || arity.negative?
|
|
938
|
+
end
|
|
939
|
+
|
|
940
|
+
def accepts_one_positional_argument_from_parameters?(parameters)
|
|
941
|
+
required_positional_count = parameters.count { |type, _name| type == :req }
|
|
942
|
+
|
|
943
|
+
required_positional_count <= 1 &&
|
|
944
|
+
parameters.any? { |type, _name| POSITIONAL_PARAMETER_TYPES.include?(type) }
|
|
945
|
+
end
|
|
946
|
+
|
|
947
|
+
def callable_parameters(callable)
|
|
948
|
+
if callable.respond_to?(:parameters)
|
|
949
|
+
callable.parameters
|
|
950
|
+
else
|
|
951
|
+
callable.method(:call).parameters
|
|
952
|
+
end
|
|
953
|
+
rescue NameError
|
|
954
|
+
nil
|
|
955
|
+
end
|
|
956
|
+
|
|
957
|
+
def validate_duration_attribute(attr_name, required: true)
|
|
958
|
+
value = public_send(attr_name)
|
|
959
|
+
|
|
960
|
+
# Allow nil for optional attributes
|
|
961
|
+
return if value.nil? && !required
|
|
962
|
+
|
|
963
|
+
# Check if it's a duration-like object (Numeric or ActiveSupport::Duration)
|
|
964
|
+
unless value.is_a?(Numeric) || value.is_a?(ActiveSupport::Duration)
|
|
965
|
+
errors.add(attr_name, :not_a_duration, value: value.inspect)
|
|
966
|
+
return
|
|
967
|
+
end
|
|
968
|
+
|
|
969
|
+
# Check if positive
|
|
970
|
+
seconds = value.to_f
|
|
971
|
+
return if seconds.positive?
|
|
972
|
+
|
|
973
|
+
errors.add(attr_name, :duration_not_positive,
|
|
974
|
+
seconds: seconds,
|
|
975
|
+
attribute: attr_name.to_s.humanize.downcase)
|
|
976
|
+
end
|
|
977
|
+
end
|
|
978
|
+
# rubocop:enable Metrics/ClassLength
|
|
979
|
+
end
|
|
980
|
+
end
|
|
981
|
+
# rubocop:enable Metrics/ModuleLength
|