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,215 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveJob
4
+ module Temporal
5
+ module Workflows
6
+ module WorkflowInteractions
7
+ HANDLER_NAME_PATTERN = /\A[a-zA-Z_]\w*\z/
8
+
9
+ private
10
+
11
+ def pause_workflow(args)
12
+ workflow_state["paused"] = true
13
+ workflow_state["pause_reason"] = args.first if args.any?
14
+ record_signal("pause", args)
15
+ nil
16
+ end
17
+
18
+ def resume_workflow(args)
19
+ workflow_state["paused"] = false
20
+ workflow_state.delete("pause_reason")
21
+ record_signal("resume", args)
22
+ nil
23
+ end
24
+
25
+ def dispatch_custom_signal(handler_name, args)
26
+ unless workflow_interactions_configured?
27
+ buffered_custom_signals << [handler_name, deep_copy(args)]
28
+ return nil
29
+ end
30
+
31
+ dispatch_configured_custom_signal(handler_name, args)
32
+ end
33
+
34
+ def dispatch_configured_custom_signal(handler_name, args)
35
+ handler = workflow_signal_handlers[handler_name]
36
+ raise ArgumentError, "Unknown workflow signal: #{handler_name}" unless handler
37
+
38
+ handler.call(workflow_state["custom"], *args)
39
+ record_signal(handler_name, args)
40
+ nil
41
+ end
42
+
43
+ def dispatch_custom_query(handler_name, args)
44
+ handler = workflow_query_handlers[handler_name]
45
+ raise ArgumentError, "Unknown workflow query: #{handler_name}" unless handler
46
+
47
+ handler.call(deep_copy(workflow_state["custom"]), *args)
48
+ end
49
+
50
+ def dispatch_custom_update(handler_name, args)
51
+ handler = workflow_update_handlers[handler_name]
52
+ raise ArgumentError, "Unknown workflow update: #{handler_name}" unless handler
53
+
54
+ result = handler.call(workflow_state["custom"], *args)
55
+ record_update(handler_name, args)
56
+ result
57
+ end
58
+
59
+ def record_signal(handler_name, args)
60
+ workflow_state["signals"][handler_name] = {
61
+ "args" => deep_copy(args),
62
+ "received_at" => Temporalio::Workflow.now.iso8601
63
+ }
64
+ end
65
+
66
+ def record_update(handler_name, args)
67
+ workflow_state["updates"][handler_name] = {
68
+ "args" => deep_copy(args),
69
+ "accepted_at" => Temporalio::Workflow.now.iso8601
70
+ }
71
+ end
72
+
73
+ def configure_workflow_state(payload)
74
+ restore_workflow_state(payload)
75
+ workflow_state["job_class"] = payload[:job_class] || payload["job_class"]
76
+ workflow_state["job_id"] = payload[:job_id] || payload["job_id"]
77
+ workflow_state["queue_name"] = payload[:queue_name] || payload["queue_name"]
78
+ workflow_state["phase"] = "initialized"
79
+ end
80
+
81
+ def restore_workflow_state(payload)
82
+ return unless workflow_patch_enabled?(:workflow_state)
83
+
84
+ state = payload[:workflow_state] || payload["workflow_state"]
85
+ return unless state.is_a?(Hash)
86
+
87
+ @workflow_state = deep_copy(state)
88
+ workflow_state["phase"] ||= "initialized"
89
+ workflow_state["paused"] = false unless workflow_state.key?("paused")
90
+ workflow_state["signals"] ||= {}
91
+ workflow_state["updates"] ||= {}
92
+ workflow_state["custom"] ||= {}
93
+ end
94
+
95
+ def configure_workflow_interactions(payload)
96
+ reset_workflow_interactions
97
+ load_workflow_interaction_handlers(payload)
98
+ @workflow_interactions_configured = true
99
+ flush_buffered_custom_signals
100
+ end
101
+
102
+ def reset_workflow_interactions
103
+ @workflow_signal_handlers = {}
104
+ @workflow_query_handlers = {}
105
+ @workflow_update_handlers = {}
106
+ end
107
+
108
+ def load_workflow_interaction_handlers(payload)
109
+ metadata = payload[:workflow_interactions] || payload["workflow_interactions"]
110
+ return unless metadata
111
+
112
+ job_class = constant_from_name(metadata[:job_class] || metadata["job_class"])
113
+ return unless job_class
114
+
115
+ @workflow_signal_handlers = filtered_handlers(
116
+ job_class,
117
+ :temporal_signal_handlers,
118
+ metadata[:signals] || metadata["signals"]
119
+ )
120
+ @workflow_query_handlers = filtered_handlers(
121
+ job_class,
122
+ :temporal_query_handlers,
123
+ metadata[:queries] || metadata["queries"]
124
+ )
125
+ @workflow_update_handlers = filtered_handlers(
126
+ job_class,
127
+ :temporal_update_handlers,
128
+ metadata[:updates] || metadata["updates"]
129
+ )
130
+ end
131
+
132
+ def workflow_state
133
+ @workflow_state ||= {
134
+ "phase" => "initialized",
135
+ "paused" => false,
136
+ "signals" => {},
137
+ "updates" => {},
138
+ "custom" => {}
139
+ }
140
+ end
141
+
142
+ def workflow_signal_handlers
143
+ @workflow_signal_handlers ||= {}
144
+ end
145
+
146
+ def workflow_query_handlers
147
+ @workflow_query_handlers ||= {}
148
+ end
149
+
150
+ def workflow_update_handlers
151
+ @workflow_update_handlers ||= {}
152
+ end
153
+
154
+ def workflow_interactions_configured?
155
+ @workflow_interactions_configured == true
156
+ end
157
+
158
+ def buffered_custom_signals
159
+ @buffered_custom_signals ||= []
160
+ end
161
+
162
+ def flush_buffered_custom_signals
163
+ buffered_custom_signals.shift(buffered_custom_signals.length).each do |handler_name, args|
164
+ dispatch_configured_custom_signal(handler_name, args)
165
+ end
166
+ end
167
+
168
+ def filtered_handlers(job_class, method_name, handler_names)
169
+ return {} unless job_class.respond_to?(method_name)
170
+
171
+ allowed_names = Array(handler_names).map(&:to_s)
172
+ job_class.public_send(method_name).each_with_object({}) do |(name, handler), handlers|
173
+ handler_name = name.to_s
174
+ handlers[handler_name] = handler if allowed_names.include?(handler_name)
175
+ end
176
+ end
177
+
178
+ def constant_from_name(class_name)
179
+ class_name.to_s.split("::").reject(&:empty?).reduce(Object) do |namespace, constant_name|
180
+ namespace.const_get(constant_name, false)
181
+ end
182
+ rescue NameError
183
+ nil
184
+ end
185
+
186
+ def normalize_handler_name!(name, handler_type)
187
+ handler_name = name.to_s
188
+ return handler_name if handler_name.match?(HANDLER_NAME_PATTERN)
189
+
190
+ raise ArgumentError, "#{handler_type} names must start with a letter or underscore and contain word chars"
191
+ end
192
+
193
+ def wait_while_paused
194
+ return unless workflow_state["paused"]
195
+
196
+ workflow_state["phase"] = "paused"
197
+ Temporalio::Workflow.wait_condition { !workflow_state["paused"] }
198
+ end
199
+
200
+ def deep_copy(value)
201
+ case value
202
+ when Hash
203
+ value.each_with_object({}) do |(key, entry_value), hash|
204
+ hash[key.to_s] = deep_copy(entry_value)
205
+ end
206
+ when Array
207
+ value.map { |entry_value| deep_copy(entry_value) }
208
+ else
209
+ value
210
+ end
211
+ end
212
+ end
213
+ end
214
+ end
215
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveJob
4
+ module Temporal
5
+ module Workflows
6
+ module WorkflowLocalActivities
7
+ private
8
+
9
+ def execute_helper_activity(payload, helper_name, activity, *, **)
10
+ if local_activity_helper?(payload, helper_name)
11
+ Temporalio::Workflow.execute_local_activity(activity, *, **)
12
+ else
13
+ Temporalio::Workflow.execute_activity(activity, *, **)
14
+ end
15
+ end
16
+
17
+ def local_activity_helper?(payload, helper_name)
18
+ return false unless workflow_patch_enabled?(:local_activity_helpers)
19
+
20
+ local_activity_helper_names(payload).include?(helper_name.to_s)
21
+ end
22
+
23
+ def local_activity_helper_names(payload)
24
+ Array(payload[:local_activity_helpers] || payload["local_activity_helpers"]).map(&:to_s)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveJob
4
+ module Temporal
5
+ module Workflows
6
+ module WorkflowNexus
7
+ private
8
+
9
+ def nexus_client_for(endpoint:, service:)
10
+ Temporalio::Workflow.create_nexus_client(endpoint: endpoint, service: service)
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveJob
4
+ module Temporal
5
+ module Workflows
6
+ module WorkflowVersioning
7
+ PATCHES = {
8
+ continue_as_new: "activejob-temporal.continue-as-new-v1",
9
+ local_activity_helpers: "activejob-temporal.local-activity-helpers-v1",
10
+ workflow_state: "activejob-temporal.workflow-state-v1"
11
+ }.freeze
12
+
13
+ private
14
+
15
+ def workflow_patch_enabled?(patch_name)
16
+ Temporalio::Workflow.patched(PATCHES.fetch(patch_name))
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,297 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+ require "active_support/core_ext/numeric/time"
5
+
6
+ require_relative "temporal/version"
7
+ require_relative "temporal/configuration"
8
+ require_relative "temporal/configurable"
9
+ require_relative "temporal/client"
10
+ require_relative "temporal/logger"
11
+ require_relative "temporal/bind_policy"
12
+ require_relative "temporal/rails_environment_loader"
13
+ require_relative "temporal/tls_file"
14
+ require_relative "temporal/certificate_watcher"
15
+ require_relative "temporal/reload_signal_queue"
16
+ require_relative "temporal/worker_client_reloader"
17
+ require_relative "temporal/worker_pool"
18
+ require_relative "temporal/audit_log"
19
+ require_relative "temporal/metrics_server"
20
+ require_relative "temporal/observability"
21
+ require_relative "temporal/worker_health"
22
+ require_relative "temporal/health_check_server"
23
+ require_relative "temporal/middleware"
24
+ require_relative "temporal/payload_serializers"
25
+ require_relative "temporal/rate_limit_options"
26
+ require_relative "temporal/rate_limiters/memory"
27
+ require_relative "temporal/payload_encryption"
28
+ require_relative "temporal/payload_storage"
29
+ require_relative "temporal/payload"
30
+ require_relative "temporal/search_attributes"
31
+ require_relative "temporal/retry_mapper"
32
+ require_relative "temporal/job_descriptor"
33
+ require_relative "temporal/external_operation"
34
+ require_relative "temporal/signal_query_options"
35
+ require_relative "temporal/child_workflow_options"
36
+ require_relative "temporal/chain_options"
37
+ require_relative "temporal/dependency_options"
38
+ require_relative "temporal/workflow_identity"
39
+ require_relative "temporal/job_payload_builder"
40
+ require_relative "temporal/schedule_options"
41
+ require_relative "temporal/conditional_enqueue"
42
+ require_relative "temporal/job_tags"
43
+ require_relative "temporal/temporal_options"
44
+ require_relative "temporal/schedule"
45
+ require_relative "temporal/schedulable"
46
+ require_relative "temporal/workflow_id_builder"
47
+ require_relative "temporal/batch_enqueue_result"
48
+ require_relative "temporal/batch_enqueuer"
49
+ require_relative "temporal/workflow_enqueuer_batch"
50
+ require_relative "temporal/workflow_enqueuer"
51
+ require_relative "temporal/adapter"
52
+ require_relative "temporal/transaction_safety"
53
+ require_relative "temporal/workflows/aj_workflow"
54
+ require_relative "temporal/workflows/dead_letter_workflow"
55
+ require_relative "temporal/activities/rate_limit_activity"
56
+ require_relative "temporal/activities/dependency_status_activity"
57
+ require_relative "temporal/activities/aj_runner_activity"
58
+ require_relative "temporal/cancel"
59
+ require_relative "temporal/inspect"
60
+ require_relative "temporal/signal_query"
61
+ require_relative "temporal/dead_letter_queue"
62
+
63
+ module ActiveJob
64
+ # ActiveJob adapter for Temporal workflow orchestration.
65
+ #
66
+ # This gem provides a durable, fault-tolerant execution backend for Rails ActiveJob
67
+ # by leveraging Temporal's workflow engine. Jobs are executed as Temporal workflows
68
+ # with automatic retries, scheduling, and observability.
69
+ #
70
+ # @example Basic configuration
71
+ # ActiveJob::Temporal.configure do |config|
72
+ # config.target = "temporal.example.com:7233"
73
+ # config.namespace = "production"
74
+ # config.task_queue_prefix = "my-app"
75
+ # end
76
+ #
77
+ # @example Using the adapter in a job
78
+ # class MyJob < ApplicationJob
79
+ # self.queue_adapter = :temporal
80
+ #
81
+ # def perform(arg1, arg2)
82
+ # # Job logic here
83
+ # end
84
+ # end
85
+ #
86
+ # @example Complete configuration with error handling
87
+ # begin
88
+ # ActiveJob::Temporal.configure do |config|
89
+ # config.target = "temporal.example.com:7233"
90
+ # config.namespace = "production"
91
+ # config.default_activity_timeout = 10.minutes
92
+ # config.max_payload_size_kb = 250
93
+ # end
94
+ # ActiveJob::Temporal.config.validate!
95
+ # rescue ActiveJob::Temporal::ConfigurationError => e
96
+ # Rails.logger.error("Temporal configuration invalid: #{e.message}")
97
+ # raise
98
+ # end
99
+ #
100
+ # @see https://github.com/temporalio/sdk-ruby Temporal Ruby SDK
101
+ module Temporal
102
+ # Raised when attempting to cancel a job that does not exist.
103
+ #
104
+ # @see Cancel.cancel
105
+ class WorkflowNotFoundError < Error; end
106
+
107
+ # Raised when Temporal cluster is unreachable.
108
+ #
109
+ # @see Client.build
110
+ # @see Cancel.cancel
111
+ class TemporalConnectionError < Error; end
112
+
113
+ extend Configurable
114
+
115
+ class << self
116
+ # Returns the memoized Temporal client connection for the process.
117
+ #
118
+ # The client is connected to the Temporal server specified in the configuration.
119
+ # TLS options can be provided via configuration attributes or environment variables:
120
+ # - TEMPORAL_TLS_CERT: TLS certificate
121
+ # - TEMPORAL_TLS_KEY: TLS private key
122
+ # - TEMPORAL_TLS_SERVER_NAME: TLS server name
123
+ #
124
+ # @return [Temporalio::Client] the connected Temporal client
125
+ # @raise [ActiveJob::Temporal::Error] if connection fails due to network or authentication issues
126
+ # @raise [ActiveJob::Temporal::TemporalConnectionError] if Temporal cluster is unreachable
127
+ # @raise [Errno::ECONNREFUSED] if Temporal cluster is not accepting connections
128
+ # @raise [SocketError] if Temporal hostname cannot be resolved
129
+ # @raise [OpenSSL::SSL::SSLError] if TLS configuration is invalid
130
+ # @example Get the client
131
+ # client = ActiveJob::Temporal.client
132
+ # client.list_workflows("ajQueue='default'")
133
+ #
134
+ # @example Using client for workflow queries
135
+ # client = ActiveJob::Temporal.client
136
+ # workflows = client.list_workflows("ajClass='MyJob'")
137
+ # workflows.each { |wf| puts wf.id }
138
+ #
139
+ # @example Accessing workflow handles
140
+ # client = ActiveJob::Temporal.client
141
+ # handle = client.workflow_handle("ajwf:MyJob:abc-123")
142
+ # result = handle.result
143
+ #
144
+ # @see Client.build
145
+ def client
146
+ client_mutex.synchronize do
147
+ @client ||= Client.build(config)
148
+ end
149
+ end
150
+
151
+ def reload_client!
152
+ fresh_client = Client.build(config)
153
+ begin
154
+ yield fresh_client if block_given?
155
+ rescue StandardError
156
+ close_client(fresh_client)
157
+ raise
158
+ end
159
+
160
+ previous_client = nil
161
+
162
+ client_mutex.synchronize do
163
+ previous_client = @client
164
+ @client = fresh_client
165
+ end
166
+
167
+ close_client(previous_client) unless previous_client.equal?(fresh_client)
168
+ fresh_client
169
+ end
170
+
171
+ # Cancels a running or scheduled job by job ID.
172
+ #
173
+ # This method requests cancellation for the Temporal workflow associated with the job.
174
+ # Cancellation is asynchronous and best-effort: the job will stop only if it is actively
175
+ # heartbeating. See Cancel module documentation for details.
176
+ #
177
+ # @param job_class [Class] the ActiveJob class (used to determine task queue)
178
+ # @param job_id [String] the unique job identifier
179
+ # @return [Boolean, nil] false if workflow already completed, nil if cancellation requested
180
+ # @raise [ActiveJob::Temporal::WorkflowNotFoundError] if job never existed or already removed from history
181
+ # @raise [ActiveJob::Temporal::TemporalConnectionError] if Temporal cluster is unreachable
182
+ # @example Cancel a scheduled job
183
+ # ActiveJob::Temporal.cancel(MyJob, "job-123-abc")
184
+ # @example Handle cancellation outcomes
185
+ # result = ActiveJob::Temporal.cancel(MyJob, "abc-123")
186
+ # case result
187
+ # when false
188
+ # puts "Job already completed"
189
+ # when nil
190
+ # puts "Cancellation requested"
191
+ # end
192
+ #
193
+ # @example Cancel with error handling
194
+ # begin
195
+ # ActiveJob::Temporal.cancel(MyJob, "unknown-id")
196
+ # rescue ActiveJob::Temporal::WorkflowNotFoundError
197
+ # puts "Job does not exist"
198
+ # end
199
+ #
200
+ # @note Cancellation Requires Heartbeating
201
+ # For jobs to respond to cancellation, they must check for cancellation by heartbeating
202
+ # or polling Temporalio::Activity::Context.current.cancelled?. Without heartbeating,
203
+ # long-running activities will complete before they detect the cancellation signal.
204
+ #
205
+ # @see Cancel.cancel
206
+ def cancel(job_class, job_id)
207
+ Cancel.cancel(job_class, job_id)
208
+ end
209
+
210
+ def cancel_all(job_class)
211
+ Cancel.cancel_all(job_class)
212
+ end
213
+
214
+ def cancel_where(filters)
215
+ Cancel.cancel_where(filters)
216
+ end
217
+
218
+ def enqueue_batch(items, concurrency: 1)
219
+ WorkflowEnqueuer.new(
220
+ -> { client },
221
+ config,
222
+ config.logger
223
+ ).enqueue_batch(items, concurrency: concurrency)
224
+ end
225
+
226
+ def activity(temporal_type, **)
227
+ ExternalOperation.activity(temporal_type, **)
228
+ end
229
+
230
+ def workflow(temporal_type, **)
231
+ ExternalOperation.workflow(temporal_type, **)
232
+ end
233
+
234
+ def job(job_class, **options)
235
+ JobDescriptor.new(job_class, options)
236
+ end
237
+
238
+ def status(job_class, job_id)
239
+ Inspect.status(job_class, job_id)
240
+ end
241
+
242
+ def signal(job_class, job_id, signal_name, *)
243
+ SignalQuery.signal(job_class, job_id, signal_name, *)
244
+ end
245
+
246
+ def query(job_class, job_id, query_name, *, reject_condition: SignalQuery::DEFAULT_REJECT_CONDITION)
247
+ SignalQuery.query(job_class, job_id, query_name, *, reject_condition: reject_condition)
248
+ end
249
+
250
+ def update(job_class, job_id, update_name, *)
251
+ SignalQuery.update(job_class, job_id, update_name, *)
252
+ end
253
+
254
+ def running?(job_class, job_id)
255
+ Inspect.running?(job_class, job_id)
256
+ end
257
+
258
+ def completed?(job_class, job_id)
259
+ Inspect.completed?(job_class, job_id)
260
+ end
261
+
262
+ def failed?(job_class, job_id)
263
+ Inspect.failed?(job_class, job_id)
264
+ end
265
+
266
+ def dead_letter_entry(job_class, job_id)
267
+ DeadLetterQueue.entry(job_class, job_id)
268
+ end
269
+
270
+ def dead_letter_entries(queue: nil, limit: DeadLetterQueue::DEFAULT_ENTRIES_LIMIT)
271
+ DeadLetterQueue.entries(queue: queue, limit: limit)
272
+ end
273
+
274
+ def retry_dead_letter(job_class, job_id, queue: nil)
275
+ DeadLetterQueue.retry(job_class, job_id, queue: queue)
276
+ end
277
+
278
+ def discard_dead_letter(job_class, job_id, reason: nil)
279
+ DeadLetterQueue.discard(job_class, job_id, reason: reason)
280
+ end
281
+
282
+ private
283
+
284
+ def client_mutex
285
+ @client_mutex ||= Mutex.new
286
+ end
287
+
288
+ def close_client(client)
289
+ return unless client.respond_to?(:close)
290
+
291
+ client.close
292
+ rescue StandardError
293
+ nil
294
+ end
295
+ end
296
+ end
297
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "activejob/temporal"