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,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "adapter"
|
|
4
|
+
require_relative "external_operation"
|
|
5
|
+
require "active_support/core_ext/string/inflections"
|
|
6
|
+
|
|
7
|
+
module ActiveJob
|
|
8
|
+
module Temporal
|
|
9
|
+
module JobPayloadChainBuilder
|
|
10
|
+
ChainStepRoutingJob = Struct.new(:queue_name, :priority)
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def apply_chain(payload, job)
|
|
15
|
+
chain = chain_payloads_for(job)
|
|
16
|
+
payload[:chain] = chain if chain.any?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def chain_payloads_for(job)
|
|
20
|
+
Array(job.respond_to?(:temporal_chain) ? job.temporal_chain : nil).each_with_index.map do |chain_step, index|
|
|
21
|
+
chain_payload_for(job, chain_step, index + 1)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def chain_payload_for(root_job, chain_step, position)
|
|
26
|
+
external_operation = ExternalOperation.normalize(chain_step)
|
|
27
|
+
return external_chain_payload(external_operation) if external_operation
|
|
28
|
+
|
|
29
|
+
job_class = chain_step_job_class(chain_step)
|
|
30
|
+
options = chain_step_options(chain_step)
|
|
31
|
+
queue_name = chain_step_queue_name(job_class, options)
|
|
32
|
+
job_id = "#{root_job.job_id}:chain:#{position}"
|
|
33
|
+
payload = base_chain_payload(job_class, job_id, queue_name, options)
|
|
34
|
+
|
|
35
|
+
apply_chain_step_retry_policy(payload, job_class, job_id, queue_name)
|
|
36
|
+
apply_temporal_options(payload, job_class)
|
|
37
|
+
apply_rate_limits_for_class(payload, job_class)
|
|
38
|
+
apply_workflow_interactions(payload, job_class)
|
|
39
|
+
payload
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def external_chain_payload(external_operation)
|
|
43
|
+
{
|
|
44
|
+
temporal_operation: external_operation.fetch(:temporal_operation),
|
|
45
|
+
temporal_type: external_operation.fetch(:temporal_type),
|
|
46
|
+
options: external_operation.fetch(:options)
|
|
47
|
+
}
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def base_chain_payload(job_class, job_id, queue_name, options)
|
|
51
|
+
{
|
|
52
|
+
job_class: job_class.name,
|
|
53
|
+
job_id: job_id,
|
|
54
|
+
queue_name: queue_name,
|
|
55
|
+
arguments: [],
|
|
56
|
+
executions: 0,
|
|
57
|
+
exception_executions: {},
|
|
58
|
+
default_activity_options: default_activity_options,
|
|
59
|
+
activity_task_queue: chain_step_activity_task_queue(queue_name, options)
|
|
60
|
+
}
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def chain_step_job_class(chain_step)
|
|
64
|
+
job_class_name = chain_step[:job_class] || chain_step["job_class"]
|
|
65
|
+
job_class = job_class_name.constantize
|
|
66
|
+
return job_class if job_class < ActiveJob::Base
|
|
67
|
+
|
|
68
|
+
raise ArgumentError, "chain entries must be ActiveJob classes or configured jobs"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def chain_step_options(chain_step)
|
|
72
|
+
chain_step[:options] || chain_step["options"] || {}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def chain_step_queue_name(job_class, options)
|
|
76
|
+
queue_name = options[:queue] || options["queue"] || job_class.queue_name
|
|
77
|
+
queue_name.to_s
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def chain_step_activity_task_queue(queue_name, options)
|
|
81
|
+
priority = options[:priority] || options["priority"]
|
|
82
|
+
routing_job = ChainStepRoutingJob.new(queue_name, priority)
|
|
83
|
+
Adapter.resolve_task_queue(routing_job, config: @config)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def apply_chain_step_retry_policy(payload, job_class, job_id, queue_name)
|
|
87
|
+
retry_policy = retry_policy_for(job_class)
|
|
88
|
+
payload[:retry_policy] = retry_policy
|
|
89
|
+
return unless dead_letter_enabled?
|
|
90
|
+
|
|
91
|
+
payload[:dead_letter] = dead_letter_metadata(
|
|
92
|
+
job_class.name,
|
|
93
|
+
job_id,
|
|
94
|
+
queue_name,
|
|
95
|
+
retry_policy,
|
|
96
|
+
task_queue: payload.fetch(:activity_task_queue)
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def apply_rate_limits_for_class(payload, job_class)
|
|
101
|
+
rate_limits = rate_limits_for(job_class)
|
|
102
|
+
payload[:rate_limits] = rate_limits if rate_limits.any?
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/core_ext/string/inflections"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
require_relative "adapter"
|
|
7
|
+
require_relative "external_operation"
|
|
8
|
+
require_relative "workflow_id_builder"
|
|
9
|
+
|
|
10
|
+
module ActiveJob
|
|
11
|
+
module Temporal
|
|
12
|
+
module JobPayloadChildWorkflows
|
|
13
|
+
ChildWorkflowRoutingJob = Struct.new(:queue_name, :priority)
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def apply_child_workflows(payload, job)
|
|
18
|
+
child_workflows = child_workflow_payloads_for(job)
|
|
19
|
+
payload[:child_workflows] = child_workflows if child_workflows.any?
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def child_workflow_payloads_for(job)
|
|
23
|
+
child_workflows = job.respond_to?(:temporal_child_workflows) ? job.temporal_child_workflows : nil
|
|
24
|
+
Array(child_workflows).each_with_index.map do |child_workflow, index|
|
|
25
|
+
child_workflow_payload_for(job, child_workflow, index + 1)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def child_workflow_payload_for(root_job, child_workflow, position)
|
|
30
|
+
external_operation = ExternalOperation.normalize(child_workflow)
|
|
31
|
+
return external_child_workflow_payload(external_operation) if external_operation
|
|
32
|
+
|
|
33
|
+
job_class = child_workflow_job_class(child_workflow)
|
|
34
|
+
options = child_workflow_options(child_workflow)
|
|
35
|
+
queue_name = child_workflow_queue_name(job_class, options)
|
|
36
|
+
job_id = "#{root_job.job_id}:child:#{position}"
|
|
37
|
+
payload = base_child_workflow_payload(job_class, job_id, queue_name, options)
|
|
38
|
+
|
|
39
|
+
apply_child_workflow_retry_policy(payload, job_class, job_id, queue_name)
|
|
40
|
+
apply_temporal_options(payload, job_class)
|
|
41
|
+
apply_workflow_identity(payload, job_class)
|
|
42
|
+
apply_rate_limits_for_class(payload, job_class)
|
|
43
|
+
apply_workflow_interactions(payload, job_class)
|
|
44
|
+
apply_child_workflow_search_attributes(payload, job_class, job_id, queue_name, options)
|
|
45
|
+
payload
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def external_child_workflow_payload(external_operation)
|
|
49
|
+
{
|
|
50
|
+
temporal_operation: external_operation.fetch(:temporal_operation),
|
|
51
|
+
temporal_type: external_operation.fetch(:temporal_type),
|
|
52
|
+
options: external_operation.fetch(:options)
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def base_child_workflow_payload(job_class, job_id, queue_name, options)
|
|
57
|
+
task_queue = child_workflow_task_queue(queue_name, options)
|
|
58
|
+
{
|
|
59
|
+
job_class: job_class.name,
|
|
60
|
+
job_id: job_id,
|
|
61
|
+
workflow_id: child_workflow_id(job_class, job_id),
|
|
62
|
+
queue_name: queue_name,
|
|
63
|
+
arguments: [],
|
|
64
|
+
executions: 0,
|
|
65
|
+
exception_executions: {},
|
|
66
|
+
default_activity_options: default_activity_options,
|
|
67
|
+
activity_task_queue: task_queue,
|
|
68
|
+
workflow_task_queue: task_queue
|
|
69
|
+
}
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def child_workflow_job_class(child_workflow)
|
|
73
|
+
job_class_name = child_workflow[:job_class] || child_workflow["job_class"]
|
|
74
|
+
job_class = job_class_name.constantize
|
|
75
|
+
return job_class if job_class < ActiveJob::Base
|
|
76
|
+
|
|
77
|
+
raise ArgumentError, "child_workflows entries must be ActiveJob classes or configured jobs"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def child_workflow_options(child_workflow)
|
|
81
|
+
child_workflow[:options] || child_workflow["options"] || {}
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def child_workflow_queue_name(job_class, options)
|
|
85
|
+
queue_name = options[:queue] || options["queue"] || job_class.queue_name
|
|
86
|
+
queue_name.to_s
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def child_workflow_task_queue(queue_name, options)
|
|
90
|
+
priority = options[:priority] || options["priority"]
|
|
91
|
+
routing_job = ChildWorkflowRoutingJob.new(queue_name, priority)
|
|
92
|
+
Adapter.resolve_task_queue(routing_job, config: @config)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def child_workflow_id(job_class, job_id)
|
|
96
|
+
WorkflowIdBuilder.new.build_from_job_class(job_class, job_id)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def apply_child_workflow_retry_policy(payload, job_class, job_id, queue_name)
|
|
100
|
+
retry_policy = retry_policy_for(job_class)
|
|
101
|
+
payload[:retry_policy] = retry_policy
|
|
102
|
+
return unless dead_letter_enabled?
|
|
103
|
+
|
|
104
|
+
payload[:dead_letter] = dead_letter_metadata(
|
|
105
|
+
job_class.name,
|
|
106
|
+
job_id,
|
|
107
|
+
queue_name,
|
|
108
|
+
retry_policy,
|
|
109
|
+
task_queue: payload.fetch(:activity_task_queue)
|
|
110
|
+
)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def apply_child_workflow_search_attributes(payload, job_class, job_id, queue_name, options)
|
|
114
|
+
return unless @config.respond_to?(:enable_search_attributes) && @config.enable_search_attributes
|
|
115
|
+
|
|
116
|
+
tags = options[:tags] || options["tags"] || []
|
|
117
|
+
payload[:search_attributes] = {
|
|
118
|
+
job_class: job_class.name,
|
|
119
|
+
job_id: job_id,
|
|
120
|
+
queue_name: queue_name,
|
|
121
|
+
enqueued_at: Time.now.utc.iso8601,
|
|
122
|
+
tags: tags
|
|
123
|
+
}
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "workflow_id_builder"
|
|
4
|
+
|
|
5
|
+
module ActiveJob
|
|
6
|
+
module Temporal
|
|
7
|
+
module JobPayloadDependencies
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def apply_dependencies(payload, job)
|
|
11
|
+
return unless job.respond_to?(:temporal_dependencies)
|
|
12
|
+
|
|
13
|
+
dependencies = Array(job.temporal_dependencies)
|
|
14
|
+
return if dependencies.empty?
|
|
15
|
+
|
|
16
|
+
payload[:dependencies] = dependencies.map { |dependency| enrich_dependency(dependency) }
|
|
17
|
+
policy = job.respond_to?(:temporal_dependency_failure_policy) ? job.temporal_dependency_failure_policy : :fail
|
|
18
|
+
payload[:dependency_failure_policy] = policy.to_s
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def enrich_dependency(dependency)
|
|
22
|
+
normalized = dependency.each_with_object({}) do |(key, value), enriched_dependency|
|
|
23
|
+
enriched_dependency[key.to_sym] = value
|
|
24
|
+
end
|
|
25
|
+
normalized[:workflow_id] ||= default_dependency_workflow_id(normalized)
|
|
26
|
+
normalized.compact
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def default_dependency_workflow_id(dependency)
|
|
30
|
+
job_class = dependency[:job_class]
|
|
31
|
+
job_id = dependency[:job_id]
|
|
32
|
+
return unless job_class && job_id
|
|
33
|
+
|
|
34
|
+
workflow_id = "#{WorkflowIdBuilder::DEFAULT_PREFIX}:#{job_class}:#{job_id}"
|
|
35
|
+
WorkflowIdBuilder.validate!(workflow_id)
|
|
36
|
+
workflow_id
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "rate_limit_options"
|
|
4
|
+
|
|
5
|
+
module ActiveJob
|
|
6
|
+
module Temporal
|
|
7
|
+
module JobPayloadRateLimits
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def apply_rate_limits(payload, job)
|
|
11
|
+
rate_limits = rate_limits_for(job.class)
|
|
12
|
+
payload[:rate_limits] = rate_limits if rate_limits.any?
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def rate_limits_for(job_class)
|
|
16
|
+
rate_limits = [
|
|
17
|
+
configured_global_rate_limit,
|
|
18
|
+
configured_job_rate_limit(job_class)
|
|
19
|
+
].compact
|
|
20
|
+
validate_rate_limiter!(rate_limits)
|
|
21
|
+
rate_limits
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def configured_global_rate_limit
|
|
25
|
+
return unless @config.respond_to?(:global_rate_limit) && @config.global_rate_limit
|
|
26
|
+
|
|
27
|
+
normalize_rate_limit(@config.global_rate_limit, default_key: "activejob-temporal:global")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def configured_job_rate_limit(job_class)
|
|
31
|
+
return unless job_class.respond_to?(:rate_limit)
|
|
32
|
+
|
|
33
|
+
rate_limit = job_class.rate_limit
|
|
34
|
+
return if rate_limit.empty?
|
|
35
|
+
|
|
36
|
+
normalize_rate_limit(rate_limit, default_key: "activejob-temporal:job:#{job_class.name}")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def normalize_rate_limit(rate_limit, default_key:)
|
|
40
|
+
normalized = RateLimitOptions.normalize_hash(rate_limit)
|
|
41
|
+
normalized[:key] ||= default_key
|
|
42
|
+
normalized
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def validate_rate_limiter!(rate_limits)
|
|
46
|
+
return if rate_limits.empty?
|
|
47
|
+
return if @config.respond_to?(:rate_limiter) && @config.rate_limiter
|
|
48
|
+
|
|
49
|
+
raise ConfigurationError, "rate_limiter is required when rate limits are configured"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveJob
|
|
4
|
+
module Temporal
|
|
5
|
+
module JobPayloadWorkflowInteractions
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def apply_workflow_interactions(payload, job_class)
|
|
9
|
+
workflow_interactions = workflow_interactions_for(job_class)
|
|
10
|
+
payload[:workflow_interactions] = workflow_interactions if workflow_interactions
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def workflow_interactions_for(job_class)
|
|
14
|
+
handlers = {
|
|
15
|
+
signals: handler_names_for(job_class, :temporal_signal_handler_names),
|
|
16
|
+
queries: handler_names_for(job_class, :temporal_query_handler_names),
|
|
17
|
+
updates: handler_names_for(job_class, :temporal_update_handler_names)
|
|
18
|
+
}
|
|
19
|
+
return if handlers.values.all?(&:empty?)
|
|
20
|
+
|
|
21
|
+
{ job_class: job_class.name, **handlers }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def handler_names_for(job_class, method_name)
|
|
25
|
+
return [] unless job_class.respond_to?(method_name)
|
|
26
|
+
|
|
27
|
+
job_class.public_send(method_name).map(&:to_s).sort
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_job"
|
|
4
|
+
|
|
5
|
+
module ActiveJob
|
|
6
|
+
module Temporal
|
|
7
|
+
# Captures Temporal search tags passed through ActiveJob's set options.
|
|
8
|
+
module JobTags
|
|
9
|
+
attr_reader :temporal_tags
|
|
10
|
+
|
|
11
|
+
def self.normalize(tags)
|
|
12
|
+
return [] if tags.nil?
|
|
13
|
+
|
|
14
|
+
raise ArgumentError, "tags must be an Array of Strings or Symbols" unless tags.is_a?(Array)
|
|
15
|
+
|
|
16
|
+
tags.map do |tag|
|
|
17
|
+
normalize_tag(tag)
|
|
18
|
+
end.uniq
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.normalize_tag(tag)
|
|
22
|
+
return tag if tag.is_a?(String)
|
|
23
|
+
return tag.to_s if tag.is_a?(Symbol)
|
|
24
|
+
|
|
25
|
+
raise ArgumentError, "tags must contain only Strings or Symbols"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def set(options = {})
|
|
29
|
+
enqueue_options = options.dup
|
|
30
|
+
normalized_tags = JobTags.normalize(enqueue_options.delete(:tags)) if enqueue_options.key?(:tags)
|
|
31
|
+
|
|
32
|
+
super(enqueue_options).tap do
|
|
33
|
+
@temporal_tags = normalized_tags if options.key?(:tags)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
ActiveJob::Base.prepend(ActiveJob::Temporal::JobTags) if defined?(ActiveJob::Base)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
en:
|
|
2
|
+
activemodel:
|
|
3
|
+
errors:
|
|
4
|
+
models:
|
|
5
|
+
active_job/temporal/config_validator:
|
|
6
|
+
attributes:
|
|
7
|
+
target:
|
|
8
|
+
target_required: "Target host is required (set ACTIVEJOB_TEMPORAL_TARGET or config.target)"
|
|
9
|
+
invalid_format: "must be in format 'host:port' with DNS-style host labels and TCP port 1-65535 (e.g., 'localhost:7233' or 'temporal.example.com:7233'), got: %{target}"
|
|
10
|
+
|
|
11
|
+
namespace:
|
|
12
|
+
namespace_required: "Namespace is required (set ACTIVEJOB_TEMPORAL_NAMESPACE or config.namespace)"
|
|
13
|
+
invalid_format: "must be 1-1000 characters, start and end with an alphanumeric character, and contain only alphanumeric characters, hyphens, and underscores (got: %{namespace})"
|
|
14
|
+
|
|
15
|
+
default_activity_timeout:
|
|
16
|
+
not_a_duration: "must be a duration (e.g., 10.minutes or 30.seconds), got: %{value}"
|
|
17
|
+
duration_not_positive: "must be positive (got: %{seconds} seconds). Use values like 1.second or 10.minutes"
|
|
18
|
+
|
|
19
|
+
default_retry_initial_interval:
|
|
20
|
+
not_a_duration: "must be a duration (e.g., 10.minutes or 30.seconds), got: %{value}"
|
|
21
|
+
duration_not_positive: "must be positive (got: %{seconds} seconds). Use values like 1.second or 10.minutes"
|
|
22
|
+
|
|
23
|
+
default_retry_backoff:
|
|
24
|
+
retry_backoff_too_small: "must be >= 1.0 (got: %{value}). Use 2.0 for exponential backoff"
|
|
25
|
+
|
|
26
|
+
default_retry_max_attempts:
|
|
27
|
+
retry_max_attempts_negative: "must be >= 0 (got: %{value}). Use 0 for unlimited retries, or positive number for max attempts"
|
|
28
|
+
|
|
29
|
+
max_payload_size_kb:
|
|
30
|
+
payload_size_invalid: "must be between 1 and 2,097,152 KB (got: %{value}). Typical values: 250 KB (default), 500 KB (large), or 1000 KB (very large)"
|
|
31
|
+
|
|
32
|
+
payload_serializer:
|
|
33
|
+
unsupported_serializer: "is not supported. Use one of: json, message_pack, msgpack, marshal"
|
|
34
|
+
|
|
35
|
+
payload_storage_adapter:
|
|
36
|
+
invalid: "must respond to #dump and #load, got: %{value}"
|
|
37
|
+
|
|
38
|
+
payload_storage_threshold_kb:
|
|
39
|
+
required: "is required when payload_storage_adapter is configured"
|
|
40
|
+
requires_adapter: "requires payload_storage_adapter"
|
|
41
|
+
invalid: "must be a positive integer, got: %{value}"
|
|
42
|
+
|
|
43
|
+
max_concurrent_activities:
|
|
44
|
+
concurrent_activities_invalid: "must be positive (got: %{value}). Typical values: 50 (low), 100 (default), 200+ (high throughput)"
|
|
45
|
+
|
|
46
|
+
max_concurrent_workflow_tasks:
|
|
47
|
+
concurrent_workflow_tasks_invalid: "must be positive (got: %{value}). Typical values: 5 (default), 10-50 (medium), 100+ (high throughput)"
|
|
48
|
+
|
|
49
|
+
validation_level:
|
|
50
|
+
invalid_level: "must be one of: strict, warn, none (got: %{value})"
|
|
51
|
+
|
|
52
|
+
observability:
|
|
53
|
+
invalid: "must be an observability configuration, got: %{value}"
|
|
54
|
+
|
|
55
|
+
audit_log:
|
|
56
|
+
not_boolean: "must be true or false, got: %{value}"
|
|
57
|
+
|
|
58
|
+
audit_logger:
|
|
59
|
+
invalid: "must respond to #info, got: %{value}"
|
|
60
|
+
|
|
61
|
+
encrypt_payload:
|
|
62
|
+
not_boolean: "must be true or false, got: %{value}"
|
|
63
|
+
|
|
64
|
+
encryption_key:
|
|
65
|
+
required: "is required when payload encryption is enabled"
|
|
66
|
+
invalid: "must be a Base64-encoded %{bytes}-byte AES-256-GCM key or key metadata with a safe id"
|
|
67
|
+
|
|
68
|
+
encryption_old_keys:
|
|
69
|
+
not_an_array: "must be an array of Base64-encoded keys or key metadata, got: %{value}"
|
|
70
|
+
invalid: "must contain only Base64-encoded %{bytes}-byte AES-256-GCM keys or key metadata with safe ids"
|
|
71
|
+
|
|
72
|
+
workflow_id_generator:
|
|
73
|
+
not_callable: "must respond to #call (for example, ->(job) { \"custom-#{job.job_id}\" }), got: %{value}"
|
|
74
|
+
wrong_arity: "must accept one positional ActiveJob argument (for example, ->(job) { \"custom-#{job.job_id}\" }), got: %{value}"
|
|
75
|
+
|
|
76
|
+
middleware_chain:
|
|
77
|
+
invalid: "must respond to #add and #call, got: %{value}"
|
|
78
|
+
|
|
79
|
+
priority_task_queues:
|
|
80
|
+
not_a_hash: "must be a hash mapping ActiveJob priority values to task queue names, got: %{value}"
|
|
81
|
+
non_integer_priority: "priority keys must be integers because ActiveJob priorities are numeric, got: %{value}"
|
|
82
|
+
blank_queue: "task queue names must be present, got: %{value}"
|
|
83
|
+
|
|
84
|
+
rate_limiter:
|
|
85
|
+
invalid: "must respond to #wait_time_for or #call, got: %{value}"
|
|
86
|
+
wrong_arity: "must accept one rate_limits argument, got: %{value}"
|
|
87
|
+
|
|
88
|
+
global_rate_limit:
|
|
89
|
+
requires_rate_limiter: "requires rate_limiter"
|
|
90
|
+
invalid: "must be a hash like { limit: 100, per: :second }, got: %{value}"
|
|
91
|
+
|
|
92
|
+
dead_letter_queue:
|
|
93
|
+
blank: "must be present when configured"
|
|
94
|
+
|
|
95
|
+
dead_letter_after_attempts:
|
|
96
|
+
requires_queue: "requires dead_letter_queue"
|
|
97
|
+
invalid: "must be greater than 0, got: %{value}"
|
|
98
|
+
|
|
99
|
+
dead_letter_auto_discard_after:
|
|
100
|
+
requires_queue: "requires dead_letter_queue"
|
|
101
|
+
not_a_duration: "must be a duration (e.g., 1.day or 6.hours), got: %{value}"
|
|
102
|
+
duration_not_positive: "must be positive (got: %{seconds} seconds). Use values like 1.hour or 7.days"
|
|
103
|
+
|
|
104
|
+
tls_cert_path:
|
|
105
|
+
requires_key_path: "requires tls_key_path so the client certificate and private key rotate together"
|
|
106
|
+
invalid_path: "must be a non-empty file path, got: %{value}"
|
|
107
|
+
unreadable_path: "must point to a readable, non-symlink file, got: %{value}"
|
|
108
|
+
|
|
109
|
+
tls_key_path:
|
|
110
|
+
invalid_path: "must be a non-empty file path, got: %{value}"
|
|
111
|
+
unreadable_path: "must point to a readable, non-symlink file, got: %{value}"
|
|
112
|
+
|
|
113
|
+
tls_server_root_ca_cert_path:
|
|
114
|
+
invalid_path: "must be a non-empty file path, got: %{value}"
|
|
115
|
+
unreadable_path: "must point to a readable, non-symlink file, got: %{value}"
|
|
116
|
+
|
|
117
|
+
tls_domain:
|
|
118
|
+
blank: "must be present when configured"
|
|
119
|
+
|
|
120
|
+
tls_cert_watch:
|
|
121
|
+
not_boolean: "must be true or false, got: %{value}"
|
|
122
|
+
requires_paths: "requires at least one TLS certificate path to watch"
|
|
123
|
+
|
|
124
|
+
tls_reload_signal:
|
|
125
|
+
blank: "must be present when configured"
|
|
126
|
+
invalid: "must be a signal name like HUP or USR1, got: %{value}"
|