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,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
require "temporalio/workflow"
|
|
5
|
+
|
|
6
|
+
module ActiveJob
|
|
7
|
+
module Temporal
|
|
8
|
+
module Workflows
|
|
9
|
+
class DeadLetterWorkflow < Temporalio::Workflow::Definition
|
|
10
|
+
workflow_name :ActiveJobTemporalDeadLetterWorkflow
|
|
11
|
+
|
|
12
|
+
workflow_query_attr_reader :entry
|
|
13
|
+
|
|
14
|
+
workflow_signal
|
|
15
|
+
def mark_retried(retry_workflow_id)
|
|
16
|
+
if @entry
|
|
17
|
+
mark_retried_entry(retry_workflow_id)
|
|
18
|
+
else
|
|
19
|
+
@pending_action = [:retried, retry_workflow_id]
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
workflow_signal
|
|
24
|
+
def discard(reason = nil)
|
|
25
|
+
if @entry
|
|
26
|
+
mark_discarded_entry(reason)
|
|
27
|
+
else
|
|
28
|
+
@pending_action = [:discarded, reason]
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def execute(entry)
|
|
33
|
+
@entry = deep_stringify(entry)
|
|
34
|
+
apply_pending_action
|
|
35
|
+
wait_until_terminal_or_expired
|
|
36
|
+
@entry
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def wait_until_terminal_or_expired
|
|
42
|
+
auto_discard_after = auto_discard_after_seconds
|
|
43
|
+
return Temporalio::Workflow.wait_condition { terminal? } unless auto_discard_after
|
|
44
|
+
|
|
45
|
+
begin
|
|
46
|
+
Temporalio::Workflow.timeout(
|
|
47
|
+
auto_discard_after,
|
|
48
|
+
Timeout::Error,
|
|
49
|
+
"dead letter auto-discard expired",
|
|
50
|
+
summary: "Dead letter auto-discard"
|
|
51
|
+
) do
|
|
52
|
+
Temporalio::Workflow.wait_condition { terminal? }
|
|
53
|
+
end
|
|
54
|
+
rescue Timeout::Error
|
|
55
|
+
mark_discarded_entry("auto_discard_after_expired")
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def auto_discard_after_seconds
|
|
60
|
+
value = @entry.dig("metadata", "auto_discard_after_seconds")
|
|
61
|
+
return if value.nil?
|
|
62
|
+
|
|
63
|
+
seconds = Float(value)
|
|
64
|
+
seconds if seconds.positive?
|
|
65
|
+
rescue ArgumentError, TypeError
|
|
66
|
+
nil
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def apply_pending_action
|
|
70
|
+
action, value = @pending_action
|
|
71
|
+
@pending_action = nil
|
|
72
|
+
|
|
73
|
+
case action
|
|
74
|
+
when :retried then mark_retried_entry(value)
|
|
75
|
+
when :discarded then mark_discarded_entry(value)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def mark_retried_entry(retry_workflow_id)
|
|
80
|
+
return if terminal?
|
|
81
|
+
|
|
82
|
+
@entry["state"] = "retried"
|
|
83
|
+
@entry["retry_workflow_id"] = retry_workflow_id
|
|
84
|
+
@entry["retried_at"] = Temporalio::Workflow.now.iso8601
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def mark_discarded_entry(reason)
|
|
88
|
+
return if terminal?
|
|
89
|
+
|
|
90
|
+
@entry["state"] = "discarded"
|
|
91
|
+
@entry["discard_reason"] = reason if reason
|
|
92
|
+
@entry["discarded_at"] = Temporalio::Workflow.now.iso8601
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def terminal?
|
|
96
|
+
%w[retried discarded].include?(@entry&.fetch("state", nil))
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def deep_stringify(value)
|
|
100
|
+
case value
|
|
101
|
+
when Hash
|
|
102
|
+
value.each_with_object({}) do |(key, entry_value), hash|
|
|
103
|
+
hash[key.to_s] = deep_stringify(entry_value)
|
|
104
|
+
end
|
|
105
|
+
when Array
|
|
106
|
+
value.map { |entry_value| deep_stringify(entry_value) }
|
|
107
|
+
else
|
|
108
|
+
value
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../activities/aj_runner_activity"
|
|
4
|
+
|
|
5
|
+
module ActiveJob
|
|
6
|
+
module Temporal
|
|
7
|
+
module Workflows
|
|
8
|
+
module WorkflowChaining
|
|
9
|
+
WORKFLOW_CONTROL_FIELDS = %i[
|
|
10
|
+
default_activity_options
|
|
11
|
+
retry_policy
|
|
12
|
+
temporal_options
|
|
13
|
+
dead_letter
|
|
14
|
+
rate_limits
|
|
15
|
+
activity_task_queue
|
|
16
|
+
local_activity_helpers
|
|
17
|
+
].freeze
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def execute_activity_sequence(payload)
|
|
22
|
+
result = execute_activity_payload(payload)
|
|
23
|
+
execute_chain_sequence(payload, result) do |chain_payload|
|
|
24
|
+
yield chain_payload if block_given?
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def execute_chain_sequence(payload, result)
|
|
29
|
+
chain_payloads(payload).each_with_index do |chain_payload, index|
|
|
30
|
+
workflow_state["chain_index"] = index + 1
|
|
31
|
+
if external_operation_payload?(chain_payload)
|
|
32
|
+
yield chain_payload if block_given?
|
|
33
|
+
result = execute_external_operation(chain_payload, result)
|
|
34
|
+
else
|
|
35
|
+
raw_arguments = [result]
|
|
36
|
+
activity_payload = payload_with_raw_arguments(chain_payload, raw_arguments)
|
|
37
|
+
yield activity_payload if block_given?
|
|
38
|
+
result = execute_activity_payload(activity_payload, raw_arguments: raw_arguments)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
result
|
|
42
|
+
ensure
|
|
43
|
+
workflow_state.delete("chain_index")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def execute_activity_payload(payload, raw_arguments: nil)
|
|
47
|
+
wait_for_rate_limit(payload)
|
|
48
|
+
wait_while_paused
|
|
49
|
+
|
|
50
|
+
workflow_state["phase"] = "running_activity"
|
|
51
|
+
Temporalio::Workflow.execute_activity(
|
|
52
|
+
*activity_arguments(payload, raw_arguments),
|
|
53
|
+
**activity_options(payload)
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def activity_arguments(payload, raw_arguments)
|
|
58
|
+
arguments = [
|
|
59
|
+
ActiveJob::Temporal::Activities::AjRunnerActivity,
|
|
60
|
+
payload
|
|
61
|
+
]
|
|
62
|
+
arguments << raw_arguments unless raw_arguments.nil?
|
|
63
|
+
arguments
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def execute_external_operation(payload, input)
|
|
67
|
+
case payload_value(payload, :temporal_operation)
|
|
68
|
+
when "activity" then execute_external_activity(payload, input)
|
|
69
|
+
when "workflow" then execute_external_workflow(payload, input)
|
|
70
|
+
else raise ArgumentError, "external Temporal operation must be activity or workflow"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def execute_external_activity(payload, input)
|
|
75
|
+
wait_while_paused
|
|
76
|
+
workflow_state["phase"] = "running_activity"
|
|
77
|
+
Temporalio::Workflow.execute_activity(
|
|
78
|
+
payload_value(payload, :temporal_type),
|
|
79
|
+
input,
|
|
80
|
+
**external_operation_options(payload)
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def execute_external_workflow(payload, input)
|
|
85
|
+
wait_while_paused
|
|
86
|
+
workflow_state["phase"] = "running_child_workflow"
|
|
87
|
+
Temporalio::Workflow.execute_child_workflow(
|
|
88
|
+
payload_value(payload, :temporal_type),
|
|
89
|
+
input,
|
|
90
|
+
**external_operation_options(payload)
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def chain_payloads(payload)
|
|
95
|
+
Array(payload[:chain] || payload["chain"]).each_with_index.map do |chain_step, index|
|
|
96
|
+
chain_step_payload(payload, chain_step, index + 1)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def chain_step_payload(root_payload, chain_step, position)
|
|
101
|
+
return normalize_external_operation_payload(chain_step) if external_operation_payload?(chain_step)
|
|
102
|
+
return normalize_chain_step_payload(chain_step) if full_chain_step_payload?(chain_step)
|
|
103
|
+
|
|
104
|
+
options = chain_step_options(chain_step)
|
|
105
|
+
payload = base_chain_step_payload(root_payload, chain_step, options, position)
|
|
106
|
+
|
|
107
|
+
copy_workflow_control_fields(root_payload, payload)
|
|
108
|
+
payload.merge!(options)
|
|
109
|
+
payload
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def full_chain_step_payload?(chain_step)
|
|
113
|
+
payload_value(chain_step, :job_id) &&
|
|
114
|
+
payload_value(chain_step, :queue_name) &&
|
|
115
|
+
!payload_value(chain_step, :arguments).nil?
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def normalize_chain_step_payload(chain_step)
|
|
119
|
+
chain_step.each_with_object({}) do |(key, value), normalized|
|
|
120
|
+
normalized[key.to_s] = value
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def normalize_external_operation_payload(operation_payload)
|
|
125
|
+
{
|
|
126
|
+
"temporal_operation" => payload_value(operation_payload, :temporal_operation),
|
|
127
|
+
"temporal_type" => payload_value(operation_payload, :temporal_type),
|
|
128
|
+
"options" => external_operation_options(operation_payload)
|
|
129
|
+
}
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def base_chain_step_payload(root_payload, chain_step, options, position)
|
|
133
|
+
{
|
|
134
|
+
"job_class" => payload_value(chain_step, :job_class),
|
|
135
|
+
"job_id" => "#{payload_value(root_payload, :job_id)}:chain:#{position}",
|
|
136
|
+
"queue_name" => (options["queue"] || "default").to_s,
|
|
137
|
+
"arguments" => [],
|
|
138
|
+
"executions" => 0,
|
|
139
|
+
"exception_executions" => {}
|
|
140
|
+
}
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def chain_step_options(chain_step)
|
|
144
|
+
options = payload_value(chain_step, :options) || {}
|
|
145
|
+
options.each_with_object({}) do |(key, value), normalized|
|
|
146
|
+
normalized[key.to_s] = value
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def copy_workflow_control_fields(source_payload, target_payload)
|
|
151
|
+
WORKFLOW_CONTROL_FIELDS.each do |key|
|
|
152
|
+
value = payload_value(source_payload, key)
|
|
153
|
+
next unless value
|
|
154
|
+
|
|
155
|
+
target_payload[key.to_s] = key == :dead_letter ? chain_dead_letter_metadata(value, target_payload) : value
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def chain_dead_letter_metadata(metadata, payload)
|
|
160
|
+
metadata.transform_keys(&:to_s).merge(
|
|
161
|
+
"job_class" => payload.fetch("job_class"),
|
|
162
|
+
"job_id" => payload.fetch("job_id"),
|
|
163
|
+
"queue_name" => payload.fetch("queue_name")
|
|
164
|
+
)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def payload_value(payload, key)
|
|
168
|
+
payload[key] || payload[key.to_s]
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def payload_with_raw_arguments(payload, raw_arguments)
|
|
172
|
+
key = payload.key?(:arguments) ? :arguments : "arguments"
|
|
173
|
+
payload.merge(key => raw_arguments)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def external_operation_payload?(payload)
|
|
177
|
+
payload_value(payload, :temporal_operation) && payload_value(payload, :temporal_type)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def external_operation_options(payload)
|
|
181
|
+
options = payload_value(payload, :options) || {}
|
|
182
|
+
options.each_with_object({}) do |(key, value), normalized|
|
|
183
|
+
normalized_key = key.to_sym
|
|
184
|
+
normalized[normalized_key] = if normalized_key == :retry_policy && value.is_a?(Hash)
|
|
185
|
+
build_retry_policy(value)
|
|
186
|
+
else
|
|
187
|
+
value
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
require "temporalio/search_attributes"
|
|
5
|
+
require "temporalio/workflow"
|
|
6
|
+
|
|
7
|
+
require_relative "../search_attributes"
|
|
8
|
+
|
|
9
|
+
module ActiveJob
|
|
10
|
+
module Temporal
|
|
11
|
+
module Workflows
|
|
12
|
+
module WorkflowChildWorkflows
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def execute_child_workflow_sequence(payload, parent_result)
|
|
16
|
+
child_payloads = child_workflow_payloads(payload)
|
|
17
|
+
return parent_result if child_payloads.empty?
|
|
18
|
+
|
|
19
|
+
workflow_state["phase"] = "running_child_workflows"
|
|
20
|
+
handles = start_child_job_workflows(child_payloads, parent_result)
|
|
21
|
+
child_results = collect_child_workflow_results(child_payloads, handles)
|
|
22
|
+
|
|
23
|
+
{
|
|
24
|
+
"parent_result" => parent_result,
|
|
25
|
+
"child_results" => child_results
|
|
26
|
+
}
|
|
27
|
+
ensure
|
|
28
|
+
workflow_state.delete("child_index")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def child_workflow_payloads(payload)
|
|
32
|
+
Array(payload[:child_workflows] || payload["child_workflows"])
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def start_child_job_workflows(child_payloads, parent_result)
|
|
36
|
+
child_payloads.each_with_index.map do |child_payload, index|
|
|
37
|
+
workflow_state["child_index"] = index + 1
|
|
38
|
+
start_child_job_workflow(child_payload, parent_result)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def collect_child_workflow_results(child_payloads, handles)
|
|
43
|
+
handles.each_with_index.map do |handle, index|
|
|
44
|
+
workflow_state["child_index"] = index + 1
|
|
45
|
+
child_workflow_result(child_payloads.fetch(index), handle.result)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def start_child_job_workflow(child_payload, parent_result)
|
|
50
|
+
if external_operation_payload?(child_payload)
|
|
51
|
+
return start_external_child_workflow(child_payload, parent_result)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
workflow_payload = child_payload_with_parent_result(child_payload, parent_result)
|
|
55
|
+
Temporalio::Workflow.start_child_workflow(
|
|
56
|
+
ActiveJob::Temporal::Workflows::AjWorkflow,
|
|
57
|
+
workflow_payload,
|
|
58
|
+
**child_workflow_options(workflow_payload)
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def start_external_child_workflow(child_payload, parent_result)
|
|
63
|
+
Temporalio::Workflow.start_child_workflow(
|
|
64
|
+
payload_value(child_payload, :temporal_type),
|
|
65
|
+
parent_result,
|
|
66
|
+
**external_child_workflow_options(child_payload)
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def child_workflow_options(child_payload)
|
|
71
|
+
task_queue = payload_value(child_payload, :workflow_task_queue) ||
|
|
72
|
+
payload_value(child_payload, :activity_task_queue)
|
|
73
|
+
options = {
|
|
74
|
+
id: payload_value(child_payload, :workflow_id),
|
|
75
|
+
task_queue: task_queue,
|
|
76
|
+
parent_close_policy: Temporalio::Workflow::ParentClosePolicy::REQUEST_CANCEL,
|
|
77
|
+
cancellation_type: Temporalio::Workflow::ChildWorkflowCancellationType::WAIT_CANCELLATION_COMPLETED
|
|
78
|
+
}.compact
|
|
79
|
+
search_attributes = child_search_attributes(payload_value(child_payload, :search_attributes))
|
|
80
|
+
options[:search_attributes] = search_attributes if search_attributes
|
|
81
|
+
options
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def external_child_workflow_options(child_payload)
|
|
85
|
+
external_operation_options(child_payload).merge(
|
|
86
|
+
parent_close_policy: Temporalio::Workflow::ParentClosePolicy::REQUEST_CANCEL,
|
|
87
|
+
cancellation_type: Temporalio::Workflow::ChildWorkflowCancellationType::WAIT_CANCELLATION_COMPLETED
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def child_payload_with_parent_result(child_payload, parent_result)
|
|
92
|
+
key = child_payload.key?(:arguments) ? :arguments : "arguments"
|
|
93
|
+
child_payload.merge(key => [parent_result])
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def child_workflow_result(child_payload, result)
|
|
97
|
+
return external_child_workflow_result(child_payload, result) if external_operation_payload?(child_payload)
|
|
98
|
+
|
|
99
|
+
{
|
|
100
|
+
"job_class" => payload_value(child_payload, :job_class),
|
|
101
|
+
"job_id" => payload_value(child_payload, :job_id),
|
|
102
|
+
"workflow_id" => payload_value(child_payload, :workflow_id),
|
|
103
|
+
"result" => result
|
|
104
|
+
}
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def external_child_workflow_result(child_payload, result)
|
|
108
|
+
options = payload_value(child_payload, :options) || {}
|
|
109
|
+
{
|
|
110
|
+
"temporal_operation" => "workflow",
|
|
111
|
+
"temporal_type" => payload_value(child_payload, :temporal_type),
|
|
112
|
+
"workflow_id" => payload_value(options, :id),
|
|
113
|
+
"task_queue" => payload_value(options, :task_queue),
|
|
114
|
+
"result" => result
|
|
115
|
+
}.compact
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def child_search_attributes(metadata)
|
|
119
|
+
return unless metadata
|
|
120
|
+
|
|
121
|
+
attributes = Temporalio::SearchAttributes.new
|
|
122
|
+
attributes[search_attribute_key(:aj_class)] = payload_value(metadata, :job_class)
|
|
123
|
+
attributes[search_attribute_key(:aj_queue)] = payload_value(metadata, :queue_name) || "default"
|
|
124
|
+
attributes[search_attribute_key(:aj_job_id)] = payload_value(metadata, :job_id)
|
|
125
|
+
attributes[search_attribute_key(:aj_enqueued_at)] = Time.iso8601(payload_value(metadata, :enqueued_at))
|
|
126
|
+
|
|
127
|
+
tags = Array(payload_value(metadata, :tags))
|
|
128
|
+
attributes[search_attribute_key(:aj_tags)] = tags if tags.any?
|
|
129
|
+
attributes
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def search_attribute_key(key)
|
|
133
|
+
name, type = ActiveJob::Temporal::SearchAttributes::SEARCH_ATTRIBUTE_KEY_DEFINITIONS.fetch(key)
|
|
134
|
+
type_constant = Temporalio::SearchAttributes::IndexedValueType.const_get(type)
|
|
135
|
+
Temporalio::SearchAttributes::Key.new(name, type_constant)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveJob
|
|
4
|
+
module Temporal
|
|
5
|
+
module Workflows
|
|
6
|
+
module WorkflowContinueAsNew
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def continue_as_new_if_needed(payload)
|
|
10
|
+
return unless workflow_patch_enabled?(:continue_as_new)
|
|
11
|
+
|
|
12
|
+
continue_as_new_options = payload[:continue_as_new] || payload["continue_as_new"]
|
|
13
|
+
return unless continue_as_new_options
|
|
14
|
+
return unless continue_as_new_threshold_reached?(continue_as_new_options)
|
|
15
|
+
|
|
16
|
+
wait_for_continue_as_new_handlers
|
|
17
|
+
workflow_state["phase"] = "continuing_as_new"
|
|
18
|
+
raise Temporalio::Workflow::ContinueAsNewError.new(
|
|
19
|
+
continue_as_new_payload(payload),
|
|
20
|
+
search_attributes: Temporalio::Workflow.search_attributes
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def continue_as_new_threshold_reached?(continue_as_new_options)
|
|
25
|
+
threshold = continue_as_new_options[:history_event_threshold] ||
|
|
26
|
+
continue_as_new_options["history_event_threshold"]
|
|
27
|
+
return false unless threshold.to_i.positive?
|
|
28
|
+
|
|
29
|
+
Temporalio::Workflow.current_history_length >= threshold.to_i
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def continue_as_new_payload(payload)
|
|
33
|
+
deep_copy(payload).merge("workflow_state" => deep_copy(workflow_state))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def wait_for_continue_as_new_handlers
|
|
37
|
+
return if Temporalio::Workflow.all_handlers_finished?
|
|
38
|
+
|
|
39
|
+
Temporalio::Workflow.wait_condition { Temporalio::Workflow.all_handlers_finished? }
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "temporalio/error"
|
|
4
|
+
|
|
5
|
+
require_relative "../activities/dependency_status_activity"
|
|
6
|
+
|
|
7
|
+
module ActiveJob
|
|
8
|
+
module Temporal
|
|
9
|
+
module Workflows
|
|
10
|
+
module WorkflowDependencies
|
|
11
|
+
DEPENDENCY_CHECK_ACTIVITY_TIMEOUT = 30.0
|
|
12
|
+
DEPENDENCY_CHECK_RETRY_POLICY = Temporalio::RetryPolicy.new(max_attempts: 1)
|
|
13
|
+
DEPENDENCY_WAIT_INTERVAL = 10.0
|
|
14
|
+
DEPENDENCY_NOT_FOUND_MAX_CHECKS = 6
|
|
15
|
+
COMPLETED_DEPENDENCY_STATES = %w[completed].freeze
|
|
16
|
+
FAILED_DEPENDENCY_STATES = %w[failed canceled terminated timed_out unknown].freeze
|
|
17
|
+
NOT_FOUND_DEPENDENCY_STATES = %w[not_found].freeze
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def wait_for_dependencies(payload)
|
|
22
|
+
dependencies = dependency_metadata(payload)
|
|
23
|
+
return if dependencies.empty?
|
|
24
|
+
|
|
25
|
+
workflow_state["phase"] = "waiting_dependencies"
|
|
26
|
+
not_found_counts = Hash.new(0)
|
|
27
|
+
loop do
|
|
28
|
+
wait_while_paused
|
|
29
|
+
statuses = check_dependency_statuses(dependencies)
|
|
30
|
+
expired_not_found_statuses = expired_not_found_statuses(statuses, not_found_counts)
|
|
31
|
+
failed_statuses = failed_dependency_statuses(statuses) + expired_not_found_statuses
|
|
32
|
+
fail_for_dependencies!(failed_statuses) if fail_on_dependency_failure?(payload) && failed_statuses.any?
|
|
33
|
+
break if dependencies_satisfied?(statuses, expired_not_found_statuses)
|
|
34
|
+
|
|
35
|
+
Temporalio::Workflow.sleep(DEPENDENCY_WAIT_INTERVAL)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def dependency_metadata(payload)
|
|
40
|
+
Array(payload[:dependencies] || payload["dependencies"])
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def check_dependency_statuses(dependencies)
|
|
44
|
+
Temporalio::Workflow.execute_activity(
|
|
45
|
+
ActiveJob::Temporal::Activities::DependencyStatusActivity,
|
|
46
|
+
dependencies,
|
|
47
|
+
schedule_to_close_timeout: DEPENDENCY_CHECK_ACTIVITY_TIMEOUT,
|
|
48
|
+
start_to_close_timeout: DEPENDENCY_CHECK_ACTIVITY_TIMEOUT,
|
|
49
|
+
retry_policy: DEPENDENCY_CHECK_RETRY_POLICY
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def dependencies_satisfied?(statuses, expired_not_found_statuses)
|
|
54
|
+
expired_not_found_keys = expired_not_found_statuses.map { |status| dependency_status_key(status) }
|
|
55
|
+
statuses.all? do |status|
|
|
56
|
+
state = dependency_state(status)
|
|
57
|
+
COMPLETED_DEPENDENCY_STATES.include?(state) ||
|
|
58
|
+
FAILED_DEPENDENCY_STATES.include?(state) ||
|
|
59
|
+
expired_not_found_keys.include?(dependency_status_key(status))
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def expired_not_found_statuses(statuses, not_found_counts)
|
|
64
|
+
statuses.each_with_object([]) do |status, expired_statuses|
|
|
65
|
+
key = dependency_status_key(status)
|
|
66
|
+
if NOT_FOUND_DEPENDENCY_STATES.include?(dependency_state(status))
|
|
67
|
+
not_found_counts[key] += 1
|
|
68
|
+
expired_statuses << status if not_found_counts[key] >= DEPENDENCY_NOT_FOUND_MAX_CHECKS
|
|
69
|
+
else
|
|
70
|
+
not_found_counts.delete(key)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def failed_dependency_statuses(statuses)
|
|
76
|
+
statuses.select do |status|
|
|
77
|
+
FAILED_DEPENDENCY_STATES.include?(dependency_state(status))
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def dependency_state(status)
|
|
82
|
+
status["state"] || status[:state]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def dependency_status_key(status)
|
|
86
|
+
job_class = status["job_class"] || status[:job_class]
|
|
87
|
+
job_id = status["job_id"] || status[:job_id]
|
|
88
|
+
return "#{job_class}:#{job_id}" if job_class && job_id
|
|
89
|
+
return job_id if job_id
|
|
90
|
+
|
|
91
|
+
status["workflow_id"] || status[:workflow_id] || status.hash
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def fail_on_dependency_failure?(payload)
|
|
95
|
+
policy = payload[:dependency_failure_policy] || payload["dependency_failure_policy"] || "fail"
|
|
96
|
+
policy.to_s == "fail"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def fail_for_dependencies!(statuses)
|
|
100
|
+
descriptions = statuses.map do |status|
|
|
101
|
+
identifier = status["workflow_id"] || status[:workflow_id] || status["job_id"] || status[:job_id]
|
|
102
|
+
state = status["state"] || status[:state]
|
|
103
|
+
"#{identifier}: #{state}"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
raise Temporalio::Error::ApplicationError.new(
|
|
107
|
+
"Job dependency failed: #{descriptions.join(', ')}",
|
|
108
|
+
type: "ActiveJob::Temporal::DependencyFailed",
|
|
109
|
+
non_retryable: true
|
|
110
|
+
)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveJob
|
|
4
|
+
module Temporal
|
|
5
|
+
module Workflows
|
|
6
|
+
module WorkflowExecutionSteps
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def execute_workflow_steps(payload, &)
|
|
10
|
+
continue_as_new_if_needed(payload)
|
|
11
|
+
wait_until_scheduled(payload)
|
|
12
|
+
continue_as_new_if_needed(payload)
|
|
13
|
+
wait_for_dependencies(payload)
|
|
14
|
+
continue_as_new_if_needed(payload)
|
|
15
|
+
result = execute_activity_payload(payload)
|
|
16
|
+
result = execute_child_workflow_sequence(payload, result)
|
|
17
|
+
execute_chain_sequence(payload, result, &)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|