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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/.agents/instructions/coding-style.md +38 -0
  3. data/.agents/instructions/domain.md +37 -0
  4. data/.agents/instructions/environment.md +44 -0
  5. data/.agents/instructions/general.md +29 -0
  6. data/.agents/instructions/security.md +20 -0
  7. data/.agents/instructions/structure.md +43 -0
  8. data/.agents/instructions/tech-stack.md +40 -0
  9. data/.agents/instructions/testing.md +46 -0
  10. data/.agents/instructions/workflow.md +39 -0
  11. data/.rubocop.yml +1 -2
  12. data/AGENTS.md +23 -0
  13. data/CHANGELOG.md +26 -0
  14. data/README.md +1 -1
  15. data/app/controllers/job_workflow/monitoring/application_controller.rb +11 -0
  16. data/app/controllers/job_workflow/monitoring/executions_controller.rb +28 -0
  17. data/app/controllers/job_workflow/monitoring/workflows_controller.rb +11 -0
  18. data/app/views/job_workflow/monitoring/executions/index.html.erb +57 -0
  19. data/app/views/job_workflow/monitoring/executions/show.html.erb +200 -0
  20. data/app/views/job_workflow/monitoring/workflows/index.html.erb +39 -0
  21. data/app/views/layouts/job_workflow/monitoring/application.html.erb +117 -0
  22. data/config/routes.rb +8 -0
  23. data/guides/API_REFERENCE.md +9 -6
  24. data/guides/DEPENDENCY_WAIT.md +9 -5
  25. data/guides/MONITORING_UI.md +74 -0
  26. data/guides/PARALLEL_PROCESSING.md +33 -21
  27. data/guides/PRODUCTION_DEPLOYMENT.md +1 -1
  28. data/guides/README.md +6 -1
  29. data/guides/THROTTLING.md +24 -0
  30. data/guides/WORKFLOW_STATUS_QUERY.md +7 -1
  31. data/lib/job_workflow/context.rb +7 -5
  32. data/lib/job_workflow/dsl.rb +0 -4
  33. data/lib/job_workflow/instrumentation/opentelemetry_subscriber.rb +1 -1
  34. data/lib/job_workflow/instrumentation.rb +14 -14
  35. data/lib/job_workflow/job_status.rb +16 -1
  36. data/lib/job_workflow/monitoring/dag_layout.rb +186 -0
  37. data/lib/job_workflow/monitoring/engine.rb +15 -0
  38. data/lib/job_workflow/monitoring/execution_page.rb +16 -0
  39. data/lib/job_workflow/monitoring/execution_registry.rb +50 -0
  40. data/lib/job_workflow/monitoring/execution_view_model.rb +258 -0
  41. data/lib/job_workflow/monitoring/parameter_filter.rb +37 -0
  42. data/lib/job_workflow/monitoring/workflow_definition.rb +24 -0
  43. data/lib/job_workflow/monitoring/workflow_registry.rb +24 -0
  44. data/lib/job_workflow/monitoring.rb +120 -0
  45. data/lib/job_workflow/queue_adapters/abstract.rb +7 -2
  46. data/lib/job_workflow/queue_adapters/null_adapter.rb +12 -1
  47. data/lib/job_workflow/queue_adapters/solid_queue_adapter.rb +42 -12
  48. data/lib/job_workflow/railtie.rb +2 -0
  49. data/lib/job_workflow/runner.rb +5 -3
  50. data/lib/job_workflow/sub_task_job.rb +93 -0
  51. data/lib/job_workflow/task_enqueue.rb +19 -12
  52. data/lib/job_workflow/version.rb +1 -1
  53. data/lib/job_workflow/workflow_status.rb +39 -3
  54. data/lib/job_workflow.rb +2 -0
  55. data/rbs_collection.lock.yaml +11 -11
  56. data/sig/generated/job_workflow/context.rbs +7 -7
  57. data/sig/generated/job_workflow/instrumentation/opentelemetry_subscriber.rbs +0 -1
  58. data/sig/generated/job_workflow/instrumentation.rbs +28 -28
  59. data/sig/generated/job_workflow/job_status.rbs +5 -2
  60. data/sig/generated/job_workflow/monitoring/dag_layout.rbs +80 -0
  61. data/sig/generated/job_workflow/monitoring/engine.rbs +8 -0
  62. data/sig/generated/job_workflow/monitoring/execution_page.rbs +14 -0
  63. data/sig/generated/job_workflow/monitoring/execution_registry.rbs +21 -0
  64. data/sig/generated/job_workflow/monitoring/execution_view_model.rbs +111 -0
  65. data/sig/generated/job_workflow/monitoring/parameter_filter.rbs +16 -0
  66. data/sig/generated/job_workflow/monitoring/workflow_definition.rbs +18 -0
  67. data/sig/generated/job_workflow/monitoring/workflow_registry.rbs +13 -0
  68. data/sig/generated/job_workflow/monitoring.rbs +38 -0
  69. data/sig/generated/job_workflow/queue_adapters/abstract.rbs +7 -4
  70. data/sig/generated/job_workflow/queue_adapters/null_adapter.rbs +5 -2
  71. data/sig/generated/job_workflow/queue_adapters/solid_queue_adapter.rbs +18 -6
  72. data/sig/generated/job_workflow/runner.rbs +1 -1
  73. data/sig/generated/job_workflow/sub_task_job.rbs +40 -0
  74. data/sig/generated/job_workflow/task_enqueue.rbs +5 -8
  75. data/sig/generated/job_workflow/workflow_status.rbs +18 -2
  76. data/sig-private/job-workflow.rbs +11 -0
  77. data/sig-private/rails.rbs +5 -0
  78. 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
- #: (DSL, Numeric) -> bool
90
+ #: (_JobInterface, Numeric) -> bool
86
91
  def reschedule_job(_job, _wait)
87
92
  false
88
93
  end
89
94
 
90
- #: (DSL) -> void
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
- #: (DSL, Numeric) -> bool
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
- args = job.arguments
164
- {
165
- "job_id" => job.active_job_id,
166
- "class_name" => job.class_name,
167
- "queue_name" => job.queue_name,
168
- "arguments" => args.is_a?(Hash) ? args["arguments"] : args,
169
- "job_workflow_context" => args.is_a?(Hash) ? args["job_workflow_context"] : nil,
170
- "status" => job_status(job)
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
- #: (DSL, Numeric) -> bool
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
- #: (DSL) -> void
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, DSL, Numeric) -> bool
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!
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "monitoring/engine"
4
+
3
5
  module JobWorkflow
4
6
  class Railtie < Rails::Railtie
5
7
  config.after_initialize do
@@ -25,11 +25,11 @@ module JobWorkflow
25
25
 
26
26
  private
27
27
 
28
- attr_reader :job #: DSL
28
+ attr_reader :job #: _JobInterface
29
29
 
30
30
  #: () -> Workflow
31
31
  def workflow
32
- job.class._workflow
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 { |ctx| job.class.from_context(ctx) }
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)