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.
Files changed (99) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +130 -0
  3. data/LICENSE +21 -0
  4. data/README.md +198 -0
  5. data/activejob-temporal.gemspec +58 -0
  6. data/api/job_payload_schema.json +318 -0
  7. data/bin/temporal-worker +295 -0
  8. data/lib/activejob/temporal/active_job_handler_source.rb +84 -0
  9. data/lib/activejob/temporal/activities/aj_runner_activity.rb +454 -0
  10. data/lib/activejob/temporal/activities/best_effort_side_effects.rb +49 -0
  11. data/lib/activejob/temporal/activities/dependency_status_activity.rb +160 -0
  12. data/lib/activejob/temporal/activities/rate_limit_activity.rb +41 -0
  13. data/lib/activejob/temporal/adapter.rb +257 -0
  14. data/lib/activejob/temporal/audit_log.rb +118 -0
  15. data/lib/activejob/temporal/batch_enqueue_result.rb +110 -0
  16. data/lib/activejob/temporal/batch_enqueuer.rb +141 -0
  17. data/lib/activejob/temporal/bind_policy.rb +44 -0
  18. data/lib/activejob/temporal/cancel/batch_canceller.rb +154 -0
  19. data/lib/activejob/temporal/cancel/batch_summary.rb +45 -0
  20. data/lib/activejob/temporal/cancel.rb +236 -0
  21. data/lib/activejob/temporal/certificate_watcher.rb +76 -0
  22. data/lib/activejob/temporal/chain_options.rb +83 -0
  23. data/lib/activejob/temporal/child_workflow_options.rb +102 -0
  24. data/lib/activejob/temporal/client.rb +215 -0
  25. data/lib/activejob/temporal/conditional_enqueue.rb +56 -0
  26. data/lib/activejob/temporal/configurable.rb +55 -0
  27. data/lib/activejob/temporal/configuration.rb +981 -0
  28. data/lib/activejob/temporal/configured_job_compatibility.rb +44 -0
  29. data/lib/activejob/temporal/connection_worker_pool.rb +88 -0
  30. data/lib/activejob/temporal/dead_letter_payload_validation.rb +34 -0
  31. data/lib/activejob/temporal/dead_letter_queue.rb +163 -0
  32. data/lib/activejob/temporal/dependency_options.rb +134 -0
  33. data/lib/activejob/temporal/external_operation.rb +193 -0
  34. data/lib/activejob/temporal/health_check_server.rb +159 -0
  35. data/lib/activejob/temporal/http_line_reader.rb +36 -0
  36. data/lib/activejob/temporal/inspect.rb +184 -0
  37. data/lib/activejob/temporal/job_descriptor.rb +37 -0
  38. data/lib/activejob/temporal/job_payload_builder.rb +209 -0
  39. data/lib/activejob/temporal/job_payload_chain_builder.rb +106 -0
  40. data/lib/activejob/temporal/job_payload_child_workflows.rb +127 -0
  41. data/lib/activejob/temporal/job_payload_dependencies.rb +40 -0
  42. data/lib/activejob/temporal/job_payload_rate_limits.rb +53 -0
  43. data/lib/activejob/temporal/job_payload_workflow_interactions.rb +31 -0
  44. data/lib/activejob/temporal/job_tags.rb +40 -0
  45. data/lib/activejob/temporal/locales/en.yml +126 -0
  46. data/lib/activejob/temporal/logger.rb +214 -0
  47. data/lib/activejob/temporal/metrics_server.rb +150 -0
  48. data/lib/activejob/temporal/middleware/chain.rb +106 -0
  49. data/lib/activejob/temporal/middleware.rb +11 -0
  50. data/lib/activejob/temporal/observability/datadog.rb +167 -0
  51. data/lib/activejob/temporal/observability/opentelemetry.rb +107 -0
  52. data/lib/activejob/temporal/observability/prometheus.rb +271 -0
  53. data/lib/activejob/temporal/observability.rb +260 -0
  54. data/lib/activejob/temporal/payload.rb +415 -0
  55. data/lib/activejob/temporal/payload_encryption.rb +215 -0
  56. data/lib/activejob/temporal/payload_serializers/json.rb +23 -0
  57. data/lib/activejob/temporal/payload_serializers/marshal.rb +53 -0
  58. data/lib/activejob/temporal/payload_serializers/message_pack.rb +59 -0
  59. data/lib/activejob/temporal/payload_serializers.rb +37 -0
  60. data/lib/activejob/temporal/payload_storage.rb +103 -0
  61. data/lib/activejob/temporal/rails_environment_loader.rb +143 -0
  62. data/lib/activejob/temporal/rate_limit_options.rb +94 -0
  63. data/lib/activejob/temporal/rate_limiters/memory.rb +198 -0
  64. data/lib/activejob/temporal/reload_signal_queue.rb +40 -0
  65. data/lib/activejob/temporal/retry_handler_extractor.rb +361 -0
  66. data/lib/activejob/temporal/retry_mapper.rb +264 -0
  67. data/lib/activejob/temporal/schedulable.rb +60 -0
  68. data/lib/activejob/temporal/schedule.rb +181 -0
  69. data/lib/activejob/temporal/schedule_options.rb +105 -0
  70. data/lib/activejob/temporal/search_attributes.rb +173 -0
  71. data/lib/activejob/temporal/signal_query.rb +161 -0
  72. data/lib/activejob/temporal/signal_query_options.rb +106 -0
  73. data/lib/activejob/temporal/temporal_options.rb +114 -0
  74. data/lib/activejob/temporal/tls_file.rb +45 -0
  75. data/lib/activejob/temporal/transaction_safety.rb +39 -0
  76. data/lib/activejob/temporal/version.rb +7 -0
  77. data/lib/activejob/temporal/visibility_query.rb +13 -0
  78. data/lib/activejob/temporal/worker_client_reloader.rb +34 -0
  79. data/lib/activejob/temporal/worker_health.rb +117 -0
  80. data/lib/activejob/temporal/worker_pool.rb +408 -0
  81. data/lib/activejob/temporal/workflow_enqueuer.rb +271 -0
  82. data/lib/activejob/temporal/workflow_enqueuer_batch.rb +17 -0
  83. data/lib/activejob/temporal/workflow_id_builder.rb +155 -0
  84. data/lib/activejob/temporal/workflow_identity.rb +62 -0
  85. data/lib/activejob/temporal/workflows/aj_workflow.rb +282 -0
  86. data/lib/activejob/temporal/workflows/dead_letter_support.rb +134 -0
  87. data/lib/activejob/temporal/workflows/dead_letter_workflow.rb +114 -0
  88. data/lib/activejob/temporal/workflows/workflow_chaining.rb +194 -0
  89. data/lib/activejob/temporal/workflows/workflow_child_workflows.rb +140 -0
  90. data/lib/activejob/temporal/workflows/workflow_continue_as_new.rb +44 -0
  91. data/lib/activejob/temporal/workflows/workflow_dependencies.rb +115 -0
  92. data/lib/activejob/temporal/workflows/workflow_execution_steps.rb +22 -0
  93. data/lib/activejob/temporal/workflows/workflow_interactions.rb +215 -0
  94. data/lib/activejob/temporal/workflows/workflow_local_activities.rb +29 -0
  95. data/lib/activejob/temporal/workflows/workflow_nexus.rb +15 -0
  96. data/lib/activejob/temporal/workflows/workflow_versioning.rb +21 -0
  97. data/lib/activejob/temporal.rb +297 -0
  98. data/lib/activejob-temporal.rb +3 -0
  99. 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