job-workflow 0.5.0 → 0.6.1
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 +4 -4
- data/.agents/instructions/coding-style.md +38 -0
- data/.agents/instructions/domain.md +37 -0
- data/.agents/instructions/environment.md +44 -0
- data/.agents/instructions/general.md +29 -0
- data/.agents/instructions/security.md +20 -0
- data/.agents/instructions/structure.md +43 -0
- data/.agents/instructions/tech-stack.md +40 -0
- data/.agents/instructions/testing.md +46 -0
- data/.agents/instructions/workflow.md +39 -0
- data/.rubocop.yml +1 -2
- data/AGENTS.md +23 -0
- data/CHANGELOG.md +26 -0
- data/README.md +1 -1
- data/app/controllers/job_workflow/monitoring/application_controller.rb +11 -0
- data/app/controllers/job_workflow/monitoring/executions_controller.rb +28 -0
- data/app/controllers/job_workflow/monitoring/workflows_controller.rb +11 -0
- data/app/views/job_workflow/monitoring/executions/index.html.erb +57 -0
- data/app/views/job_workflow/monitoring/executions/show.html.erb +200 -0
- data/app/views/job_workflow/monitoring/workflows/index.html.erb +39 -0
- data/app/views/layouts/job_workflow/monitoring/application.html.erb +117 -0
- data/config/routes.rb +8 -0
- data/guides/API_REFERENCE.md +9 -6
- data/guides/DEPENDENCY_WAIT.md +9 -5
- data/guides/MONITORING_UI.md +74 -0
- data/guides/PARALLEL_PROCESSING.md +33 -21
- data/guides/PRODUCTION_DEPLOYMENT.md +1 -1
- data/guides/README.md +6 -1
- data/guides/THROTTLING.md +24 -0
- data/guides/WORKFLOW_STATUS_QUERY.md +7 -1
- data/lib/job_workflow/context.rb +7 -5
- data/lib/job_workflow/dsl.rb +0 -4
- data/lib/job_workflow/instrumentation/opentelemetry_subscriber.rb +1 -1
- data/lib/job_workflow/instrumentation.rb +14 -14
- data/lib/job_workflow/job_status.rb +16 -1
- data/lib/job_workflow/monitoring/dag_layout.rb +186 -0
- data/lib/job_workflow/monitoring/engine.rb +15 -0
- data/lib/job_workflow/monitoring/execution_page.rb +16 -0
- data/lib/job_workflow/monitoring/execution_registry.rb +50 -0
- data/lib/job_workflow/monitoring/execution_view_model.rb +258 -0
- data/lib/job_workflow/monitoring/parameter_filter.rb +37 -0
- data/lib/job_workflow/monitoring/workflow_definition.rb +24 -0
- data/lib/job_workflow/monitoring/workflow_registry.rb +24 -0
- data/lib/job_workflow/monitoring.rb +120 -0
- data/lib/job_workflow/queue_adapters/abstract.rb +7 -2
- data/lib/job_workflow/queue_adapters/null_adapter.rb +12 -1
- data/lib/job_workflow/queue_adapters/solid_queue_adapter.rb +42 -12
- data/lib/job_workflow/railtie.rb +2 -0
- data/lib/job_workflow/runner.rb +5 -3
- data/lib/job_workflow/sub_task_job.rb +93 -0
- data/lib/job_workflow/task_enqueue.rb +19 -12
- data/lib/job_workflow/version.rb +1 -1
- data/lib/job_workflow/workflow_status.rb +39 -3
- data/lib/job_workflow.rb +2 -0
- data/rbs_collection.lock.yaml +11 -11
- data/sig/generated/job_workflow/context.rbs +7 -7
- data/sig/generated/job_workflow/instrumentation/opentelemetry_subscriber.rbs +0 -1
- data/sig/generated/job_workflow/instrumentation.rbs +28 -28
- data/sig/generated/job_workflow/job_status.rbs +5 -2
- data/sig/generated/job_workflow/monitoring/dag_layout.rbs +80 -0
- data/sig/generated/job_workflow/monitoring/engine.rbs +8 -0
- data/sig/generated/job_workflow/monitoring/execution_page.rbs +14 -0
- data/sig/generated/job_workflow/monitoring/execution_registry.rbs +21 -0
- data/sig/generated/job_workflow/monitoring/execution_view_model.rbs +111 -0
- data/sig/generated/job_workflow/monitoring/parameter_filter.rbs +16 -0
- data/sig/generated/job_workflow/monitoring/workflow_definition.rbs +18 -0
- data/sig/generated/job_workflow/monitoring/workflow_registry.rbs +13 -0
- data/sig/generated/job_workflow/monitoring.rbs +38 -0
- data/sig/generated/job_workflow/queue_adapters/abstract.rbs +7 -4
- data/sig/generated/job_workflow/queue_adapters/null_adapter.rbs +5 -2
- data/sig/generated/job_workflow/queue_adapters/solid_queue_adapter.rbs +18 -6
- data/sig/generated/job_workflow/runner.rbs +1 -1
- data/sig/generated/job_workflow/sub_task_job.rbs +40 -0
- data/sig/generated/job_workflow/task_enqueue.rbs +5 -8
- data/sig/generated/job_workflow/workflow_status.rbs +18 -2
- data/sig-private/job-workflow.rbs +11 -0
- data/sig-private/rails.rbs +5 -0
- metadata +42 -1
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JobWorkflow
|
|
4
|
+
module Monitoring
|
|
5
|
+
class ExecutionViewModel
|
|
6
|
+
attr_reader :job_id #: String
|
|
7
|
+
attr_reader :queue_name #: String?
|
|
8
|
+
attr_reader :status #: WorkflowStatus
|
|
9
|
+
|
|
10
|
+
#: (job_id: String, queue_name: String?, status: WorkflowStatus) -> void
|
|
11
|
+
def initialize(job_id:, queue_name:, status:)
|
|
12
|
+
@job_id = job_id
|
|
13
|
+
@queue_name = queue_name
|
|
14
|
+
@status = status
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
#: () -> String
|
|
18
|
+
def job_class_name
|
|
19
|
+
status.job_class_name
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
#: () -> Symbol
|
|
23
|
+
def workflow_status
|
|
24
|
+
status.status
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
#: () -> Symbol?
|
|
28
|
+
def current_task_name
|
|
29
|
+
status.current_task_name
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
#: () -> Arguments
|
|
33
|
+
def arguments
|
|
34
|
+
status.arguments
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
#: () -> Hash[untyped, untyped]
|
|
38
|
+
def filtered_arguments
|
|
39
|
+
ParameterFilter.filter(arguments.to_h)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
#: () -> Array[Hash[Symbol, untyped]]
|
|
43
|
+
def tasks
|
|
44
|
+
@tasks ||= status.context.workflow.tasks.map { |task| task_view_model(task) }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
#: () -> Symbol?
|
|
48
|
+
def failed_task_name
|
|
49
|
+
@failed_task_name ||= begin
|
|
50
|
+
failed_task = tasks.find { |task| task[:status] == :failed }
|
|
51
|
+
failed_task&.fetch(:name)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
#: () -> String?
|
|
56
|
+
def mission_control_job_path
|
|
57
|
+
JobWorkflow::Monitoring.mission_control_job_path(job_id, status: workflow_status)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
#: () -> Hash[Symbol, untyped]
|
|
61
|
+
def dag_layout
|
|
62
|
+
@dag_layout ||= DagLayout.new(tasks:).to_h
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
#: () -> bool
|
|
66
|
+
def running?
|
|
67
|
+
workflow_status == :running
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
#: () -> Hash[Symbol, untyped]
|
|
71
|
+
def to_h
|
|
72
|
+
{
|
|
73
|
+
job_id:,
|
|
74
|
+
queue_name:,
|
|
75
|
+
job_class_name:,
|
|
76
|
+
status: workflow_status,
|
|
77
|
+
current_task_name:,
|
|
78
|
+
failed_task_name:,
|
|
79
|
+
arguments: filtered_arguments,
|
|
80
|
+
tasks:,
|
|
81
|
+
mission_control_job_path:
|
|
82
|
+
}
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
#: (Symbol, Array[TaskOutput], Array[TaskJobStatus]) -> Symbol
|
|
88
|
+
def task_status(task_name, task_outputs, task_job_statuses)
|
|
89
|
+
return :failed if task_job_statuses.any?(&:failed?)
|
|
90
|
+
return :succeeded if completed_task?(task_name, task_outputs, task_job_statuses)
|
|
91
|
+
return :running if task_running?(task_name, task_job_statuses)
|
|
92
|
+
|
|
93
|
+
:pending
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
#: (Symbol, Array[TaskOutput], Array[TaskJobStatus]) -> bool
|
|
97
|
+
def completed_task?(task_name, task_outputs, task_job_statuses)
|
|
98
|
+
return true if !running? && task_outputs.any?
|
|
99
|
+
return task_job_statuses.all?(&:succeeded?) if task_job_statuses.any?
|
|
100
|
+
return true if status.completed_task_names.include?(task_name)
|
|
101
|
+
|
|
102
|
+
task_outputs.any?
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
#: (Symbol, Array[TaskJobStatus]) -> bool
|
|
106
|
+
def task_running?(task_name, task_job_statuses)
|
|
107
|
+
current_task_running?(task_name) ||
|
|
108
|
+
(!task_job_statuses.empty? && task_job_statuses.any? { |task_status| !task_status.finished? })
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
#: (Symbol) -> bool
|
|
112
|
+
def current_task_running?(task_name) = running? && current_task_name == task_name
|
|
113
|
+
|
|
114
|
+
#: (Task) -> Hash[Symbol, untyped]
|
|
115
|
+
def task_view_model(task)
|
|
116
|
+
task_name = task.task_name
|
|
117
|
+
task_outputs, task_job_statuses = task_state(task_name)
|
|
118
|
+
|
|
119
|
+
task_configuration_view(task).merge(
|
|
120
|
+
task_runtime_view(task_name, task_outputs, task_job_statuses)
|
|
121
|
+
)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
#: (Task) -> Hash[Symbol, untyped]
|
|
125
|
+
def task_configuration_view(task)
|
|
126
|
+
{
|
|
127
|
+
name: task.task_name,
|
|
128
|
+
depends_on: task.depends_on,
|
|
129
|
+
each: task.each?,
|
|
130
|
+
configuration: task_configuration(task)
|
|
131
|
+
}
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
#: (Symbol, Array[TaskOutput], Array[TaskJobStatus]) -> Hash[Symbol, untyped]
|
|
135
|
+
def task_runtime_view(task_name, task_outputs, task_job_statuses)
|
|
136
|
+
{
|
|
137
|
+
status: task_status(task_name, task_outputs, task_job_statuses),
|
|
138
|
+
each_progress: each_progress(task_outputs, task_job_statuses),
|
|
139
|
+
outputs: task_outputs_view(task_outputs),
|
|
140
|
+
sub_task_jobs: sub_task_jobs_view(task_job_statuses)
|
|
141
|
+
}
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
#: (Symbol) -> [Array[TaskOutput], Array[TaskJobStatus]]
|
|
145
|
+
def task_state(task_name) = [status.output.fetch_all(task_name:), status.job_status.fetch_all(task_name:)]
|
|
146
|
+
|
|
147
|
+
#: (Task) -> Hash[Symbol, untyped]
|
|
148
|
+
def task_configuration(task)
|
|
149
|
+
{
|
|
150
|
+
job_name: task.job_name,
|
|
151
|
+
each: callable_summary(task.each),
|
|
152
|
+
condition: callable_summary(task.condition),
|
|
153
|
+
enqueue: enqueue_configuration(task),
|
|
154
|
+
outputs: output_configuration(task),
|
|
155
|
+
retry: retry_configuration(task),
|
|
156
|
+
throttle: throttle_configuration(task),
|
|
157
|
+
timeout: task.timeout,
|
|
158
|
+
dependency_wait: dependency_wait_configuration(task),
|
|
159
|
+
dry_run: callable_summary(task.dry_run_config.evaluator)
|
|
160
|
+
}
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
#: (Task) -> Hash[Symbol, untyped]
|
|
164
|
+
def enqueue_configuration(task)
|
|
165
|
+
{
|
|
166
|
+
enabled: primitive_summary(task.enqueue.condition),
|
|
167
|
+
queue: task.enqueue.queue
|
|
168
|
+
}
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
#: (Task) -> Array[Hash[Symbol, untyped]]
|
|
172
|
+
def output_configuration(task)
|
|
173
|
+
task.output.map { |output| { name: output.name, type: output.type } }
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
#: (Task) -> Hash[Symbol, untyped]
|
|
177
|
+
def retry_configuration(task)
|
|
178
|
+
{
|
|
179
|
+
count: task.task_retry.count,
|
|
180
|
+
strategy: task.task_retry.strategy,
|
|
181
|
+
base_delay: task.task_retry.base_delay,
|
|
182
|
+
jitter: task.task_retry.jitter
|
|
183
|
+
}
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
#: (Task) -> Hash[Symbol, untyped]
|
|
187
|
+
def throttle_configuration(task)
|
|
188
|
+
{
|
|
189
|
+
key: task.throttle.key,
|
|
190
|
+
limit: task.throttle.limit,
|
|
191
|
+
ttl: task.throttle.ttl
|
|
192
|
+
}
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
#: (Task) -> Hash[Symbol, untyped]
|
|
196
|
+
def dependency_wait_configuration(task)
|
|
197
|
+
{
|
|
198
|
+
poll_timeout: task.dependency_wait.poll_timeout,
|
|
199
|
+
poll_interval: task.dependency_wait.poll_interval,
|
|
200
|
+
reschedule_delay: task.dependency_wait.reschedule_delay,
|
|
201
|
+
polling_only: task.dependency_wait.polling_only?
|
|
202
|
+
}
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
#: (untyped) -> untyped
|
|
206
|
+
def callable_summary(value)
|
|
207
|
+
case value
|
|
208
|
+
when nil
|
|
209
|
+
nil
|
|
210
|
+
when Proc
|
|
211
|
+
"proc"
|
|
212
|
+
else
|
|
213
|
+
value
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
#: (untyped) -> untyped
|
|
218
|
+
def primitive_summary(value)
|
|
219
|
+
value.is_a?(Proc) ? "proc" : value
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
#: (Array[TaskOutput], Array[TaskJobStatus]) -> Hash[Symbol, Integer]
|
|
223
|
+
def each_progress(task_outputs, task_job_statuses)
|
|
224
|
+
{
|
|
225
|
+
total: [task_outputs.size, task_job_statuses.size].max,
|
|
226
|
+
succeeded: task_job_statuses.count(&:succeeded?),
|
|
227
|
+
failed: task_job_statuses.count(&:failed?),
|
|
228
|
+
pending: task_job_statuses.count { |task_status| task_status.status == :pending },
|
|
229
|
+
running: task_job_statuses.count { |task_status| task_status.status == :running }
|
|
230
|
+
}
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
#: (Array[TaskOutput]) -> Array[Hash[Symbol, untyped]]
|
|
234
|
+
def task_outputs_view(task_outputs)
|
|
235
|
+
task_outputs.map do |output|
|
|
236
|
+
{ each_index: output.each_index, data: ParameterFilter.filter(output.data) }
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
#: (Array[TaskJobStatus]) -> Array[Hash[Symbol, untyped]]
|
|
241
|
+
def sub_task_jobs_view(task_job_statuses)
|
|
242
|
+
Array(task_job_statuses).map do |task_job_status|
|
|
243
|
+
{
|
|
244
|
+
job_id: task_job_status.job_id,
|
|
245
|
+
each_index: task_job_status.each_index,
|
|
246
|
+
status: task_job_status.status,
|
|
247
|
+
mission_control_job_path: mission_control_job_path_for(task_job_status.job_id, task_job_status.status)
|
|
248
|
+
}
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
#: (String?, Symbol?) -> String?
|
|
253
|
+
def mission_control_job_path_for(job_id, status = nil)
|
|
254
|
+
JobWorkflow::Monitoring.mission_control_job_path(job_id, status:)
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/parameter_filter"
|
|
4
|
+
|
|
5
|
+
module JobWorkflow
|
|
6
|
+
module Monitoring
|
|
7
|
+
class ParameterFilter
|
|
8
|
+
class << self
|
|
9
|
+
#: (untyped) -> untyped
|
|
10
|
+
def filter(value)
|
|
11
|
+
case value
|
|
12
|
+
when Hash
|
|
13
|
+
parameter_filter.filter(value)
|
|
14
|
+
when Array
|
|
15
|
+
value.map { |item| filter(item) }
|
|
16
|
+
else
|
|
17
|
+
value
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
#: () -> ActiveSupport::ParameterFilter
|
|
24
|
+
def parameter_filter
|
|
25
|
+
ActiveSupport::ParameterFilter.new(filters)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
#: () -> Array[untyped]
|
|
29
|
+
def filters
|
|
30
|
+
return [] unless defined?(Rails.application) && Rails.application.config.respond_to?(:filter_parameters)
|
|
31
|
+
|
|
32
|
+
Rails.application.config.filter_parameters
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JobWorkflow
|
|
4
|
+
module Monitoring
|
|
5
|
+
class WorkflowDefinition
|
|
6
|
+
attr_reader :job_class #: singleton(DSL)
|
|
7
|
+
|
|
8
|
+
#: (job_class: singleton(DSL)) -> void
|
|
9
|
+
def initialize(job_class:)
|
|
10
|
+
@job_class = job_class
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
#: () -> String
|
|
14
|
+
def job_class_name
|
|
15
|
+
job_class.name
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
#: () -> Integer
|
|
19
|
+
def task_count
|
|
20
|
+
job_class._workflow.tasks.size
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JobWorkflow
|
|
4
|
+
module Monitoring
|
|
5
|
+
class WorkflowRegistry
|
|
6
|
+
class << self
|
|
7
|
+
#: () -> Array[singleton(DSL)]
|
|
8
|
+
def all
|
|
9
|
+
DSL._included_classes.to_a
|
|
10
|
+
.reverse
|
|
11
|
+
.select(&:name)
|
|
12
|
+
.uniq(&:name)
|
|
13
|
+
.reverse
|
|
14
|
+
.sort_by(&:name)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
#: (String) -> singleton(DSL)?
|
|
18
|
+
def find(job_class_name)
|
|
19
|
+
all.find { |job_class| job_class.name == job_class_name }
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "monitoring/workflow_registry"
|
|
4
|
+
require_relative "monitoring/workflow_definition"
|
|
5
|
+
require_relative "monitoring/execution_page"
|
|
6
|
+
require_relative "monitoring/dag_layout"
|
|
7
|
+
require_relative "monitoring/parameter_filter"
|
|
8
|
+
require_relative "monitoring/execution_view_model"
|
|
9
|
+
require_relative "monitoring/execution_registry"
|
|
10
|
+
|
|
11
|
+
module JobWorkflow
|
|
12
|
+
module Monitoring
|
|
13
|
+
mattr_accessor :base_controller_class
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
#: () -> Array[WorkflowDefinition]
|
|
17
|
+
def workflows
|
|
18
|
+
WorkflowRegistry.all.map { |job_class| WorkflowDefinition.new(job_class:) }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
#: (String?, Symbol?) -> String?
|
|
22
|
+
def mission_control_job_path(job_id, status: nil)
|
|
23
|
+
return if job_id.nil?
|
|
24
|
+
|
|
25
|
+
path_template = mission_control_job_route_path
|
|
26
|
+
return if path_template.nil?
|
|
27
|
+
|
|
28
|
+
path = path_template.sub(":id", job_id.to_s)
|
|
29
|
+
path += "#error" if status == :failed
|
|
30
|
+
path
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
#: () -> String
|
|
34
|
+
def resolved_base_controller_class
|
|
35
|
+
return base_controller_class if base_controller_class.present?
|
|
36
|
+
|
|
37
|
+
mission_control_base_controller_class = defined?(MissionControl::Jobs) &&
|
|
38
|
+
MissionControl::Jobs.base_controller_class
|
|
39
|
+
return mission_control_base_controller_class if mission_control_base_controller_class.present?
|
|
40
|
+
|
|
41
|
+
"::ApplicationController"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
#: (untyped config) -> void
|
|
45
|
+
def configure_engine_config(config)
|
|
46
|
+
config.job_workflow = ActiveSupport::OrderedOptions.new unless config.try(:job_workflow)
|
|
47
|
+
config.job_workflow.monitoring ||= ActiveSupport::OrderedOptions.new
|
|
48
|
+
|
|
49
|
+
config.before_initialize do
|
|
50
|
+
config.job_workflow.monitoring.each do |key, value|
|
|
51
|
+
JobWorkflow::Monitoring.public_send("#{key}=", value)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
#: () -> String?
|
|
59
|
+
def mission_control_job_route_path
|
|
60
|
+
application_id = mission_control_application_id
|
|
61
|
+
route_name = application_id ? "application_job" : "job"
|
|
62
|
+
mount_path = mission_control_mount_path
|
|
63
|
+
return if mount_path.nil?
|
|
64
|
+
|
|
65
|
+
path = engine_route_path(route_name)
|
|
66
|
+
return if path.nil?
|
|
67
|
+
|
|
68
|
+
combined_path = "#{mount_path.chomp("/")}#{path}"
|
|
69
|
+
application_id ? combined_path.sub(":application_id", application_id.to_s) : combined_path
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
#: () -> String?
|
|
73
|
+
def mission_control_application_id
|
|
74
|
+
return unless defined?(MissionControl::Jobs)
|
|
75
|
+
|
|
76
|
+
MissionControl::Jobs.applications.first&.id
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
#: () -> String?
|
|
80
|
+
def mission_control_mount_path
|
|
81
|
+
path = rails_routes&.find { |candidate| candidate.name == "mission_control_jobs" }&.path
|
|
82
|
+
normalized_route_path(path)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
#: (String) -> String?
|
|
86
|
+
def engine_route_path(route_name)
|
|
87
|
+
path = mission_control_routes&.find { |candidate| candidate.name == route_name }&.path
|
|
88
|
+
normalized_route_path(path)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
#: (untyped) -> String?
|
|
92
|
+
def normalized_route_path(path)
|
|
93
|
+
return unless path.respond_to?(:spec)
|
|
94
|
+
|
|
95
|
+
path_spec = path.spec.to_s
|
|
96
|
+
return unless path_spec.is_a?(String) && !path_spec.empty?
|
|
97
|
+
|
|
98
|
+
path_spec.delete_suffix("(.:format)")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
#: () -> untyped
|
|
102
|
+
def mission_control_routes
|
|
103
|
+
return unless defined?(MissionControl::Jobs::Engine)
|
|
104
|
+
|
|
105
|
+
MissionControl::Jobs::Engine.routes.routes
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
#: () -> untyped
|
|
109
|
+
def rails_routes
|
|
110
|
+
return unless defined?(Rails.application)
|
|
111
|
+
return unless Rails.application.respond_to?(:routes)
|
|
112
|
+
|
|
113
|
+
routes = Rails.application.routes
|
|
114
|
+
return unless routes.respond_to?(:routes)
|
|
115
|
+
|
|
116
|
+
routes.routes
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -77,17 +77,22 @@ module JobWorkflow
|
|
|
77
77
|
raise NotImplementedError, "#{self.class}#find_job must be implemented"
|
|
78
78
|
end
|
|
79
79
|
|
|
80
|
+
#: (job_class_name: String, limit: Integer, cursor: String?) -> Hash[Symbol, untyped]
|
|
81
|
+
def fetch_root_workflow_job_page(job_class_name:, limit:, cursor:)
|
|
82
|
+
raise NotImplementedError, "#{self.class}#fetch_root_workflow_job_page must be implemented"
|
|
83
|
+
end
|
|
84
|
+
|
|
80
85
|
#: (Array[String]) -> Array[Hash[String, untyped]]
|
|
81
86
|
def fetch_job_contexts(_job_ids)
|
|
82
87
|
raise NotImplementedError, "#{self.class}#fetch_job_contexts must be implemented"
|
|
83
88
|
end
|
|
84
89
|
|
|
85
|
-
#: (
|
|
90
|
+
#: (_JobInterface, Numeric) -> bool
|
|
86
91
|
def reschedule_job(_job, _wait)
|
|
87
92
|
false
|
|
88
93
|
end
|
|
89
94
|
|
|
90
|
-
#: (
|
|
95
|
+
#: (_JobInterface) -> void
|
|
91
96
|
def persist_job_context(_job); end
|
|
92
97
|
end
|
|
93
98
|
# rubocop:enable Naming/PredicateMethod
|
|
@@ -90,12 +90,23 @@ module JobWorkflow
|
|
|
90
90
|
@stored_jobs[job_id]
|
|
91
91
|
end
|
|
92
92
|
|
|
93
|
+
#: (job_class_name: String, limit: Integer, cursor: String?) -> Hash[Symbol, untyped]
|
|
94
|
+
def fetch_root_workflow_job_page(job_class_name:, limit:, cursor:)
|
|
95
|
+
jobs = @stored_jobs.values.select { |job| job["class_name"] == job_class_name }.reverse
|
|
96
|
+
offset = cursor.nil? ? 0 : cursor.to_i
|
|
97
|
+
page_jobs = jobs.slice(offset, limit + 1) || []
|
|
98
|
+
{
|
|
99
|
+
jobs: page_jobs.first(limit),
|
|
100
|
+
next_cursor: page_jobs.size > limit ? (offset + limit).to_s : nil
|
|
101
|
+
}
|
|
102
|
+
end
|
|
103
|
+
|
|
93
104
|
#: (Array[String]) -> Array[Hash[String, untyped]]
|
|
94
105
|
def fetch_job_contexts(_job_ids)
|
|
95
106
|
[]
|
|
96
107
|
end
|
|
97
108
|
|
|
98
|
-
#: (
|
|
109
|
+
#: (_JobInterface, Numeric) -> bool
|
|
99
110
|
def reschedule_job(_job, _wait)
|
|
100
111
|
false
|
|
101
112
|
end
|
|
@@ -160,15 +160,17 @@ module JobWorkflow
|
|
|
160
160
|
job = without_query_cache { SolidQueue::Job.find_by(active_job_id: job_id) }
|
|
161
161
|
return if job.nil?
|
|
162
162
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
163
|
+
normalized_job_data(job)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
#: (job_class_name: String, limit: Integer, cursor: String?) -> Hash[Symbol, untyped]
|
|
167
|
+
def fetch_root_workflow_job_page(job_class_name:, limit:, cursor:)
|
|
168
|
+
return { jobs: [], next_cursor: nil } unless defined?(SolidQueue::Job)
|
|
169
|
+
|
|
170
|
+
without_query_cache do
|
|
171
|
+
jobs = root_jobs_relation(job_class_name:, cursor:).limit(limit + 1).to_a
|
|
172
|
+
build_page(jobs, limit:)
|
|
173
|
+
end
|
|
172
174
|
end
|
|
173
175
|
|
|
174
176
|
# @note
|
|
@@ -186,7 +188,7 @@ module JobWorkflow
|
|
|
186
188
|
end
|
|
187
189
|
end
|
|
188
190
|
|
|
189
|
-
#: (
|
|
191
|
+
#: (_JobInterface, Numeric) -> bool
|
|
190
192
|
def reschedule_job(job, wait)
|
|
191
193
|
return false unless defined?(SolidQueue::Job)
|
|
192
194
|
|
|
@@ -204,7 +206,7 @@ module JobWorkflow
|
|
|
204
206
|
# outputs computed during job execution would be lost because
|
|
205
207
|
# SolidQueue does not re-serialize job arguments after perform.
|
|
206
208
|
#
|
|
207
|
-
#: (
|
|
209
|
+
#: (_JobInterface) -> void
|
|
208
210
|
def persist_job_context(job)
|
|
209
211
|
return unless defined?(SolidQueue::Job)
|
|
210
212
|
|
|
@@ -229,7 +231,35 @@ module JobWorkflow
|
|
|
229
231
|
defined?(SolidQueue::Job) ? SolidQueue::Job.uncached(&) : yield
|
|
230
232
|
end
|
|
231
233
|
|
|
232
|
-
#: (SolidQueue::Job
|
|
234
|
+
#: (SolidQueue::Job) -> Hash[String, untyped]
|
|
235
|
+
def normalized_job_data(job)
|
|
236
|
+
args = job.arguments
|
|
237
|
+
{
|
|
238
|
+
"job_id" => job.active_job_id,
|
|
239
|
+
"class_name" => job.class_name,
|
|
240
|
+
"queue_name" => job.queue_name,
|
|
241
|
+
"arguments" => args.is_a?(Hash) ? args["arguments"] : args,
|
|
242
|
+
"job_workflow_context" => args.is_a?(Hash) ? args["job_workflow_context"] : nil,
|
|
243
|
+
"status" => job_status(job)
|
|
244
|
+
}
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
#: (job_class_name: String, cursor: String?) -> untyped
|
|
248
|
+
def root_jobs_relation(job_class_name:, cursor:)
|
|
249
|
+
relation = SolidQueue::Job.where(class_name: job_class_name)
|
|
250
|
+
relation = relation.where("id < ?", cursor.to_i) unless cursor.nil?
|
|
251
|
+
relation.order(id: :desc)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
#: (Array[SolidQueue::Job], limit: Integer) -> Hash[Symbol, untyped]
|
|
255
|
+
def build_page(jobs, limit:)
|
|
256
|
+
{
|
|
257
|
+
jobs: jobs.first(limit).map { |job| normalized_job_data(job) },
|
|
258
|
+
next_cursor: jobs.size > limit ? jobs[limit - 1].id.to_s : nil
|
|
259
|
+
}
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
#: (SolidQueue::Job, _JobInterface, Numeric) -> bool
|
|
233
263
|
def reschedule_solid_queue_job(solid_queue_job, active_job, wait)
|
|
234
264
|
solid_queue_job.with_lock do
|
|
235
265
|
solid_queue_job.claimed_execution&.destroy!
|
data/lib/job_workflow/railtie.rb
CHANGED
data/lib/job_workflow/runner.rb
CHANGED
|
@@ -25,11 +25,11 @@ module JobWorkflow
|
|
|
25
25
|
|
|
26
26
|
private
|
|
27
27
|
|
|
28
|
-
attr_reader :job #:
|
|
28
|
+
attr_reader :job #: _JobInterface
|
|
29
29
|
|
|
30
30
|
#: () -> Workflow
|
|
31
31
|
def workflow
|
|
32
|
-
|
|
32
|
+
context.workflow
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
#: () -> Array[Task]
|
|
@@ -133,7 +133,9 @@ module JobWorkflow
|
|
|
133
133
|
|
|
134
134
|
#: (Task) -> void
|
|
135
135
|
def enqueue_task(task)
|
|
136
|
-
sub_jobs = context._with_each_value(task).map
|
|
136
|
+
sub_jobs = context._with_each_value(task).map do |ctx|
|
|
137
|
+
SubTaskJob.from_parent_context(context: ctx)
|
|
138
|
+
end
|
|
137
139
|
persist_current_job_context
|
|
138
140
|
ActiveJob.perform_all_later(sub_jobs)
|
|
139
141
|
context.job_status.update_task_job_statuses_from_jobs(task_name: task.task_name, jobs: sub_jobs)
|