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,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_job"
|
|
4
|
+
require_relative "configured_job_compatibility"
|
|
5
|
+
require_relative "external_operation"
|
|
6
|
+
require_relative "job_descriptor"
|
|
7
|
+
|
|
8
|
+
module ActiveJob
|
|
9
|
+
module Temporal
|
|
10
|
+
module ChainOptions
|
|
11
|
+
SUPPORTED_CONFIGURED_OPTIONS = %i[priority queue].freeze
|
|
12
|
+
|
|
13
|
+
attr_reader :temporal_chain
|
|
14
|
+
|
|
15
|
+
def self.normalize(chain)
|
|
16
|
+
return nil if chain.nil?
|
|
17
|
+
|
|
18
|
+
raise ArgumentError, "chain must be an Array of ActiveJob classes or configured jobs" unless chain.is_a?(Array)
|
|
19
|
+
raise ArgumentError, "chain must contain at least one ActiveJob class or configured job" if chain.empty?
|
|
20
|
+
|
|
21
|
+
chain.map { |job_class| normalize_job_class(job_class) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.normalize_job_class(job_class)
|
|
25
|
+
ExternalOperation.normalize(job_class) ||
|
|
26
|
+
job_descriptor_payload(job_class) ||
|
|
27
|
+
active_job_class_payload(job_class) ||
|
|
28
|
+
configured_job_payload(job_class, feature: "chain") ||
|
|
29
|
+
raise(ArgumentError, "chain entries must be ActiveJob classes or configured jobs")
|
|
30
|
+
end
|
|
31
|
+
private_class_method :normalize_job_class
|
|
32
|
+
|
|
33
|
+
def self.job_descriptor_payload(job_class)
|
|
34
|
+
payload = JobDescriptor.normalize(job_class)
|
|
35
|
+
return unless payload
|
|
36
|
+
|
|
37
|
+
payload.merge(options: normalize_configured_options(payload.fetch(:options)))
|
|
38
|
+
end
|
|
39
|
+
private_class_method :job_descriptor_payload
|
|
40
|
+
|
|
41
|
+
def self.active_job_class_payload(job_class)
|
|
42
|
+
return unless job_class.is_a?(Class) && job_class < ActiveJob::Base && job_class.name
|
|
43
|
+
|
|
44
|
+
{ job_class: job_class.name, options: {} }
|
|
45
|
+
end
|
|
46
|
+
private_class_method :active_job_class_payload
|
|
47
|
+
|
|
48
|
+
def self.configured_job_payload(job_class, feature:)
|
|
49
|
+
ConfiguredJobCompatibility.payload(
|
|
50
|
+
job_class,
|
|
51
|
+
feature: feature,
|
|
52
|
+
normalize_options: method(:normalize_configured_options)
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
private_class_method :configured_job_payload
|
|
56
|
+
|
|
57
|
+
def self.normalize_configured_options(options)
|
|
58
|
+
unsupported_options = options.keys.reject do |key|
|
|
59
|
+
SUPPORTED_CONFIGURED_OPTIONS.include?(key.to_sym)
|
|
60
|
+
end
|
|
61
|
+
unless unsupported_options.empty?
|
|
62
|
+
raise ArgumentError, "chain configured jobs only support queue and priority options"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
options.each_with_object({}) do |(key, value), normalized|
|
|
66
|
+
normalized[key.to_sym] = value
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
private_class_method :normalize_configured_options
|
|
70
|
+
|
|
71
|
+
def set(options = {})
|
|
72
|
+
enqueue_options = options.dup
|
|
73
|
+
normalized_chain = ChainOptions.normalize(enqueue_options.delete(:chain)) if enqueue_options.key?(:chain)
|
|
74
|
+
|
|
75
|
+
super(enqueue_options).tap do
|
|
76
|
+
@temporal_chain = normalized_chain if options.key?(:chain)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
ActiveJob::Base.prepend(ActiveJob::Temporal::ChainOptions) if defined?(ActiveJob::Base)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_job"
|
|
4
|
+
require_relative "configured_job_compatibility"
|
|
5
|
+
require_relative "external_operation"
|
|
6
|
+
require_relative "job_descriptor"
|
|
7
|
+
require_relative "job_tags"
|
|
8
|
+
|
|
9
|
+
module ActiveJob
|
|
10
|
+
module Temporal
|
|
11
|
+
module ChildWorkflowOptions
|
|
12
|
+
SUPPORTED_CONFIGURED_OPTIONS = %i[priority queue tags].freeze
|
|
13
|
+
|
|
14
|
+
attr_reader :temporal_child_workflows
|
|
15
|
+
|
|
16
|
+
def self.normalize(child_workflows)
|
|
17
|
+
return nil if child_workflows.nil?
|
|
18
|
+
|
|
19
|
+
unless child_workflows.is_a?(Array)
|
|
20
|
+
raise ArgumentError, "child_workflows must be an Array of ActiveJob classes or configured jobs"
|
|
21
|
+
end
|
|
22
|
+
if child_workflows.empty?
|
|
23
|
+
raise ArgumentError, "child_workflows must contain at least one ActiveJob class or configured job"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
child_workflows.map { |child_workflow| normalize_job_class(child_workflow) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.normalize_job_class(child_workflow)
|
|
30
|
+
external_operation = ExternalOperation.normalize(child_workflow)
|
|
31
|
+
return normalize_external_operation(external_operation) if external_operation
|
|
32
|
+
|
|
33
|
+
job_descriptor = job_descriptor_payload(child_workflow)
|
|
34
|
+
return job_descriptor if job_descriptor
|
|
35
|
+
|
|
36
|
+
active_job_class_payload(child_workflow) ||
|
|
37
|
+
configured_job_payload(child_workflow, feature: "child_workflows") ||
|
|
38
|
+
raise(ArgumentError, "child_workflows entries must be ActiveJob classes or configured jobs")
|
|
39
|
+
end
|
|
40
|
+
private_class_method :normalize_job_class
|
|
41
|
+
|
|
42
|
+
def self.job_descriptor_payload(child_workflow)
|
|
43
|
+
payload = JobDescriptor.normalize(child_workflow)
|
|
44
|
+
return unless payload
|
|
45
|
+
|
|
46
|
+
payload.merge(options: normalize_configured_options(payload.fetch(:options)))
|
|
47
|
+
end
|
|
48
|
+
private_class_method :job_descriptor_payload
|
|
49
|
+
|
|
50
|
+
def self.normalize_external_operation(external_operation)
|
|
51
|
+
return external_operation if external_operation[:temporal_operation] == ExternalOperation::WORKFLOW
|
|
52
|
+
|
|
53
|
+
raise ArgumentError, "child_workflows entries must be ActiveJob classes or configured jobs; " \
|
|
54
|
+
"external refs must be workflows"
|
|
55
|
+
end
|
|
56
|
+
private_class_method :normalize_external_operation
|
|
57
|
+
|
|
58
|
+
def self.active_job_class_payload(job_class)
|
|
59
|
+
return unless job_class.is_a?(Class) && job_class < ActiveJob::Base && job_class.name
|
|
60
|
+
|
|
61
|
+
{ job_class: job_class.name, options: {} }
|
|
62
|
+
end
|
|
63
|
+
private_class_method :active_job_class_payload
|
|
64
|
+
|
|
65
|
+
def self.configured_job_payload(child_workflow, feature:)
|
|
66
|
+
ConfiguredJobCompatibility.payload(
|
|
67
|
+
child_workflow,
|
|
68
|
+
feature: feature,
|
|
69
|
+
normalize_options: method(:normalize_configured_options)
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
private_class_method :configured_job_payload
|
|
73
|
+
|
|
74
|
+
def self.normalize_configured_options(options)
|
|
75
|
+
unsupported_options = options.keys.reject do |key|
|
|
76
|
+
SUPPORTED_CONFIGURED_OPTIONS.include?(key.to_sym)
|
|
77
|
+
end
|
|
78
|
+
unless unsupported_options.empty?
|
|
79
|
+
raise ArgumentError, "child_workflows configured jobs only support queue, priority, and tags options"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
options.each_with_object({}) do |(key, value), normalized|
|
|
83
|
+
normalized[key.to_sym] = key.to_sym == :tags ? JobTags.normalize(value) : value
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
private_class_method :normalize_configured_options
|
|
87
|
+
|
|
88
|
+
def set(options = {})
|
|
89
|
+
enqueue_options = options.dup
|
|
90
|
+
normalized_children = if enqueue_options.key?(:child_workflows)
|
|
91
|
+
ChildWorkflowOptions.normalize(enqueue_options.delete(:child_workflows))
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
super(enqueue_options).tap do
|
|
95
|
+
@temporal_child_workflows = normalized_children if options.key?(:child_workflows)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
ActiveJob::Base.prepend(ActiveJob::Temporal::ChildWorkflowOptions) if defined?(ActiveJob::Base)
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
begin
|
|
4
|
+
require "temporalio/client"
|
|
5
|
+
rescue LoadError
|
|
6
|
+
# The Temporal Ruby SDK is not present in development/test by default.
|
|
7
|
+
# Tests stub Temporalio::Client, and production users must include the SDK.
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
require_relative "tls_file"
|
|
11
|
+
|
|
12
|
+
module ActiveJob
|
|
13
|
+
module Temporal
|
|
14
|
+
# Builds Temporal client connections.
|
|
15
|
+
#
|
|
16
|
+
# This module encapsulates the logic for connecting to a Temporal cluster
|
|
17
|
+
# with optional TLS configuration. TLS options can be provided via configuration
|
|
18
|
+
# attributes or environment variables.
|
|
19
|
+
#
|
|
20
|
+
# @note TLS Configuration Precedence
|
|
21
|
+
# `config.tls` takes precedence, followed by configured certificate file paths,
|
|
22
|
+
# then legacy environment variables.
|
|
23
|
+
#
|
|
24
|
+
# @note Environment Variables
|
|
25
|
+
# - TEMPORAL_TLS_CERT: TLS certificate (PEM format, full content)
|
|
26
|
+
# - TEMPORAL_TLS_KEY: TLS private key (PEM format, full content)
|
|
27
|
+
# - TEMPORAL_TLS_SERVER_NAME: TLS server name for verification
|
|
28
|
+
#
|
|
29
|
+
# @example Basic connection
|
|
30
|
+
# config = ActiveJob::Temporal.config
|
|
31
|
+
# client = Client.build(config)
|
|
32
|
+
#
|
|
33
|
+
# @example TLS via environment variables
|
|
34
|
+
# ENV["TEMPORAL_TLS_CERT"] = File.read("cert.pem")
|
|
35
|
+
# ENV["TEMPORAL_TLS_KEY"] = File.read("key.pem")
|
|
36
|
+
# ENV["TEMPORAL_TLS_SERVER_NAME"] = "temporal.example.com"
|
|
37
|
+
# client = Client.build(config)
|
|
38
|
+
#
|
|
39
|
+
# @example TLS via configuration object
|
|
40
|
+
# ActiveJob::Temporal.configure do |config|
|
|
41
|
+
# config.tls = {
|
|
42
|
+
# client_cert: File.read("cert.pem"),
|
|
43
|
+
# client_private_key: File.read("key.pem"),
|
|
44
|
+
# domain: "temporal.example.com"
|
|
45
|
+
# }
|
|
46
|
+
# end
|
|
47
|
+
# client = ActiveJob::Temporal.client
|
|
48
|
+
module Client
|
|
49
|
+
TLS_OPTIONS_CLASS = if defined?(Temporalio::Client::Connection::TLSOptions)
|
|
50
|
+
Temporalio::Client::Connection::TLSOptions
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Environment variable name for TLS certificate
|
|
54
|
+
TLS_CERT_ENV = "TEMPORAL_TLS_CERT"
|
|
55
|
+
# Environment variable name for TLS private key
|
|
56
|
+
TLS_KEY_ENV = "TEMPORAL_TLS_KEY"
|
|
57
|
+
# Environment variable name for TLS server name
|
|
58
|
+
TLS_SERVER_NAME_ENV = "TEMPORAL_TLS_SERVER_NAME"
|
|
59
|
+
# Environment variable name for TLS root CA certificate
|
|
60
|
+
TLS_SERVER_ROOT_CA_CERT_ENV = "TEMPORAL_TLS_SERVER_ROOT_CA_CERT"
|
|
61
|
+
|
|
62
|
+
module_function
|
|
63
|
+
|
|
64
|
+
# Builds and connects a Temporal client.
|
|
65
|
+
#
|
|
66
|
+
# Creates a new Temporalio::Client instance connected to the configured
|
|
67
|
+
# Temporal cluster. TLS options are automatically included if present in
|
|
68
|
+
# configuration or environment variables.
|
|
69
|
+
#
|
|
70
|
+
# @param configuration [Configuration] Gem configuration object with target/namespace
|
|
71
|
+
#
|
|
72
|
+
# @return [Temporalio::Client] Connected Temporal client
|
|
73
|
+
#
|
|
74
|
+
# @raise [ActiveJob::Temporal::Error] if connection fails (includes target, namespace, and error message)
|
|
75
|
+
# @raise [OpenSSL::SSL::SSLError] if TLS certificate validation fails
|
|
76
|
+
# @raise [OpenSSL::PKey::RSAError] if TLS private key is invalid
|
|
77
|
+
# @raise [OpenSSL::X509::CertificateError] if TLS certificate is malformed
|
|
78
|
+
# @raise [SocketError] if target hostname cannot be resolved
|
|
79
|
+
# @raise [Errno::ECONNREFUSED] if target port is not accepting connections
|
|
80
|
+
# @raise [Errno::ETIMEDOUT] if connection times out
|
|
81
|
+
#
|
|
82
|
+
# @example Basic usage
|
|
83
|
+
# config = ActiveJob::Temporal::Configuration.new
|
|
84
|
+
# config.target = "temporal.example.com:7233"
|
|
85
|
+
# config.namespace = "production"
|
|
86
|
+
# client = Client.build(config)
|
|
87
|
+
#
|
|
88
|
+
# @example With TLS via environment variables
|
|
89
|
+
# ENV["TEMPORAL_TLS_CERT"] = File.read("client.pem")
|
|
90
|
+
# ENV["TEMPORAL_TLS_KEY"] = File.read("client-key.pem")
|
|
91
|
+
# ENV["TEMPORAL_TLS_SERVER_NAME"] = "temporal.prod.example.com"
|
|
92
|
+
# client = Client.build(config)
|
|
93
|
+
# # Client will connect using mutual TLS
|
|
94
|
+
#
|
|
95
|
+
# @example Handling connection failures
|
|
96
|
+
# begin
|
|
97
|
+
# client = Client.build(config)
|
|
98
|
+
# rescue ActiveJob::Temporal::Error => e
|
|
99
|
+
# Rails.logger.fatal("Cannot connect to Temporal: #{e.message}")
|
|
100
|
+
# # Fall back to different adapter or alert operations team
|
|
101
|
+
# end
|
|
102
|
+
def build(configuration)
|
|
103
|
+
Temporalio::Client.connect(
|
|
104
|
+
configuration.target,
|
|
105
|
+
configuration.namespace,
|
|
106
|
+
**connection_kwargs(configuration)
|
|
107
|
+
)
|
|
108
|
+
rescue StandardError => e
|
|
109
|
+
raise ActiveJob::Temporal::Error,
|
|
110
|
+
format(
|
|
111
|
+
"Unable to connect to Temporal at %<target>s (namespace: %<namespace>s): %<error>s",
|
|
112
|
+
target: configuration.target,
|
|
113
|
+
namespace: configuration.namespace,
|
|
114
|
+
error: e.message
|
|
115
|
+
)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Builds connection keyword arguments (including TLS options).
|
|
119
|
+
# @api private
|
|
120
|
+
def connection_kwargs(configuration)
|
|
121
|
+
tls = tls_options(configuration)
|
|
122
|
+
return {} if tls.nil?
|
|
123
|
+
|
|
124
|
+
{ tls: tls }
|
|
125
|
+
end
|
|
126
|
+
private_class_method :connection_kwargs
|
|
127
|
+
|
|
128
|
+
# Extracts TLS options from config or environment variables.
|
|
129
|
+
# @api private
|
|
130
|
+
def tls_options(configuration)
|
|
131
|
+
configured_tls = configuration.tls if configuration.respond_to?(:tls)
|
|
132
|
+
return normalize_tls_options(configured_tls) unless configured_tls.nil?
|
|
133
|
+
|
|
134
|
+
path_options = tls_path_options(configuration)
|
|
135
|
+
return path_options if path_options
|
|
136
|
+
|
|
137
|
+
cert = ENV.fetch(TLS_CERT_ENV, nil)
|
|
138
|
+
key = ENV.fetch(TLS_KEY_ENV, nil)
|
|
139
|
+
server_name = ENV.fetch(TLS_SERVER_NAME_ENV, nil)
|
|
140
|
+
server_root_ca_cert = ENV.fetch(TLS_SERVER_ROOT_CA_CERT_ENV, nil)
|
|
141
|
+
return nil unless cert || key || server_name || server_root_ca_cert
|
|
142
|
+
|
|
143
|
+
build_tls_options(
|
|
144
|
+
client_cert: cert,
|
|
145
|
+
client_private_key: key,
|
|
146
|
+
server_root_ca_cert: server_root_ca_cert,
|
|
147
|
+
domain: server_name
|
|
148
|
+
)
|
|
149
|
+
end
|
|
150
|
+
private_class_method :tls_options
|
|
151
|
+
|
|
152
|
+
def tls_path_options(configuration)
|
|
153
|
+
return unless configuration.respond_to?(:tls_cert_path)
|
|
154
|
+
|
|
155
|
+
cert = read_tls_file(configuration.tls_cert_path)
|
|
156
|
+
key = read_tls_file(configuration.tls_key_path)
|
|
157
|
+
server_root_ca_cert = read_tls_file(configuration.tls_server_root_ca_cert_path)
|
|
158
|
+
domain = configuration.tls_domain
|
|
159
|
+
return nil unless cert || key || server_root_ca_cert || domain
|
|
160
|
+
|
|
161
|
+
build_tls_options(
|
|
162
|
+
client_cert: cert,
|
|
163
|
+
client_private_key: key,
|
|
164
|
+
server_root_ca_cert: server_root_ca_cert,
|
|
165
|
+
domain: domain
|
|
166
|
+
)
|
|
167
|
+
end
|
|
168
|
+
private_class_method :tls_path_options
|
|
169
|
+
|
|
170
|
+
def read_tls_file(path)
|
|
171
|
+
TLSFile.read(path)
|
|
172
|
+
end
|
|
173
|
+
private_class_method :read_tls_file
|
|
174
|
+
|
|
175
|
+
def normalize_tls_options(tls)
|
|
176
|
+
return tls if [true, false].include?(tls)
|
|
177
|
+
return tls if TLS_OPTIONS_CLASS && tls.is_a?(TLS_OPTIONS_CLASS)
|
|
178
|
+
return normalize_tls_hash(tls) if tls.is_a?(Hash)
|
|
179
|
+
|
|
180
|
+
tls
|
|
181
|
+
end
|
|
182
|
+
private_class_method :normalize_tls_options
|
|
183
|
+
|
|
184
|
+
def normalize_tls_hash(tls)
|
|
185
|
+
build_tls_options(
|
|
186
|
+
client_cert: tls_hash_value(tls, :client_cert, :certificate),
|
|
187
|
+
client_private_key: tls_hash_value(tls, :client_private_key, :private_key),
|
|
188
|
+
server_root_ca_cert: tls_hash_value(tls, :server_root_ca_cert),
|
|
189
|
+
domain: tls_hash_value(tls, :domain, :server_name)
|
|
190
|
+
)
|
|
191
|
+
end
|
|
192
|
+
private_class_method :normalize_tls_hash
|
|
193
|
+
|
|
194
|
+
def tls_hash_value(hash, *keys)
|
|
195
|
+
keys.each do |key|
|
|
196
|
+
return hash[key] if hash.key?(key)
|
|
197
|
+
|
|
198
|
+
string_key = key.to_s
|
|
199
|
+
return hash[string_key] if hash.key?(string_key)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
nil
|
|
203
|
+
end
|
|
204
|
+
private_class_method :tls_hash_value
|
|
205
|
+
|
|
206
|
+
def build_tls_options(**attributes)
|
|
207
|
+
attributes = attributes.compact
|
|
208
|
+
return attributes unless TLS_OPTIONS_CLASS
|
|
209
|
+
|
|
210
|
+
TLS_OPTIONS_CLASS.new(**attributes)
|
|
211
|
+
end
|
|
212
|
+
private_class_method :build_tls_options
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_job"
|
|
4
|
+
require "active_support/concern"
|
|
5
|
+
|
|
6
|
+
module ActiveJob
|
|
7
|
+
module Temporal
|
|
8
|
+
# Adds conditional enqueue helpers to ActiveJob classes.
|
|
9
|
+
module ConditionalEnqueue
|
|
10
|
+
extend ActiveSupport::Concern
|
|
11
|
+
|
|
12
|
+
def self.job_arguments(arguments, keyword_arguments)
|
|
13
|
+
return arguments if keyword_arguments.empty?
|
|
14
|
+
|
|
15
|
+
arguments + [keyword_arguments]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.condition_allows_enqueue?(receiver, condition, arguments)
|
|
19
|
+
!!evaluate_condition(receiver, condition, arguments)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.evaluate_condition(receiver, condition, arguments)
|
|
23
|
+
return receiver.public_send(condition, arguments) if condition.is_a?(Symbol) || condition.is_a?(String)
|
|
24
|
+
return condition.call(arguments) if condition.respond_to?(:call)
|
|
25
|
+
|
|
26
|
+
raise ArgumentError, "condition must be a Symbol, String, or respond to #call"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class_methods do
|
|
30
|
+
def perform_later_if(condition, *arguments, **keyword_arguments, &)
|
|
31
|
+
condition_arguments = ConditionalEnqueue.job_arguments(arguments, keyword_arguments)
|
|
32
|
+
return nil unless ConditionalEnqueue.condition_allows_enqueue?(self, condition, condition_arguments)
|
|
33
|
+
|
|
34
|
+
perform_later(*arguments, **keyword_arguments, &)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Adds conditional enqueue helpers to ActiveJob configured jobs.
|
|
40
|
+
module ConfiguredConditionalEnqueue
|
|
41
|
+
def perform_later_if(condition, *arguments, **keyword_arguments, &)
|
|
42
|
+
job_class = instance_variable_get(:@job_class)
|
|
43
|
+
condition_arguments = ConditionalEnqueue.job_arguments(arguments, keyword_arguments)
|
|
44
|
+
return nil unless ConditionalEnqueue.condition_allows_enqueue?(job_class, condition, condition_arguments)
|
|
45
|
+
|
|
46
|
+
perform_later(*arguments, **keyword_arguments, &)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
ActiveJob::Base.include(ActiveJob::Temporal::ConditionalEnqueue) if defined?(ActiveJob::Base)
|
|
53
|
+
|
|
54
|
+
if defined?(ActiveJob::ConfiguredJob)
|
|
55
|
+
ActiveJob::ConfiguredJob.include(ActiveJob::Temporal::ConfiguredConditionalEnqueue)
|
|
56
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "concurrent/mvar"
|
|
4
|
+
|
|
5
|
+
require_relative "configuration"
|
|
6
|
+
|
|
7
|
+
module ActiveJob
|
|
8
|
+
module Temporal
|
|
9
|
+
# Module-level configuration API for ActiveJob::Temporal.
|
|
10
|
+
#
|
|
11
|
+
# @api private
|
|
12
|
+
module Configurable
|
|
13
|
+
# Returns the global configuration object.
|
|
14
|
+
#
|
|
15
|
+
# @return [Configuration] the gem configuration
|
|
16
|
+
def config
|
|
17
|
+
@config_mvar ||= Concurrent::MVar.new(Configuration.new)
|
|
18
|
+
@config_mvar.value
|
|
19
|
+
end
|
|
20
|
+
alias configuration config
|
|
21
|
+
|
|
22
|
+
# Configures the gem with a block and validates after mutation.
|
|
23
|
+
#
|
|
24
|
+
# @yield [config] Gives the configuration object to the block
|
|
25
|
+
# @yieldparam config [Configuration] the configuration to modify
|
|
26
|
+
# @return [Configuration] the configuration object
|
|
27
|
+
# @raise [ConfigurationError] if validation fails after configuration
|
|
28
|
+
def configure
|
|
29
|
+
return config unless block_given?
|
|
30
|
+
|
|
31
|
+
@config_mvar ||= Concurrent::MVar.new(Configuration.new)
|
|
32
|
+
@config_mvar.borrow do |configuration|
|
|
33
|
+
configuration.in_configure_block = true
|
|
34
|
+
|
|
35
|
+
begin
|
|
36
|
+
yield(configuration)
|
|
37
|
+
ensure
|
|
38
|
+
configuration.in_configure_block = false
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
configuration.validate!
|
|
42
|
+
configuration
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Validates the current configuration.
|
|
47
|
+
#
|
|
48
|
+
# @return [void]
|
|
49
|
+
# @raise [ConfigurationError] if validation fails
|
|
50
|
+
def validate!
|
|
51
|
+
config.validate!
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|