job-workflow 0.2.0 → 0.4.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 +4 -4
- data/CHANGELOG.md +31 -0
- data/README.md +1 -1
- data/guides/PRODUCTION_DEPLOYMENT.md +1 -1
- data/guides/README.md +1 -6
- data/lib/job_workflow/context.rb +11 -9
- data/lib/job_workflow/dsl.rb +0 -20
- data/lib/job_workflow/output.rb +4 -12
- data/lib/job_workflow/queue_adapters/abstract.rb +8 -0
- data/lib/job_workflow/queue_adapters/null_adapter.rb +5 -0
- data/lib/job_workflow/queue_adapters/solid_queue_adapter.rb +59 -11
- data/lib/job_workflow/runner.rb +19 -6
- data/lib/job_workflow/task.rb +3 -7
- data/lib/job_workflow/version.rb +1 -1
- data/lib/job_workflow/workflow.rb +0 -11
- data/lib/job_workflow/workflow_status.rb +1 -1
- data/lib/job_workflow.rb +0 -1
- data/rbs_collection.lock.yaml +11 -11
- data/sig/generated/job_workflow/context.rbs +6 -6
- data/sig/generated/job_workflow/dsl.rbs +0 -8
- data/sig/generated/job_workflow/output.rbs +2 -5
- data/sig/generated/job_workflow/queue_adapters/abstract.rbs +6 -0
- data/sig/generated/job_workflow/queue_adapters/null_adapter.rbs +3 -0
- data/sig/generated/job_workflow/queue_adapters/solid_queue_adapter.rbs +25 -1
- data/sig/generated/job_workflow/runner.rbs +5 -2
- data/sig/generated/job_workflow/task.rbs +2 -5
- data/sig/generated/job_workflow/workflow.rbs +0 -5
- metadata +1 -4
- data/guides/NAMESPACES.md +0 -75
- data/lib/job_workflow/namespace.rb +0 -36
- data/sig/generated/job_workflow/namespace.rbs +0 -24
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 213a0654cb0482f1a2574f4e55d26c72f5274d24b61b933abd04e9e21f4c19d1
|
|
4
|
+
data.tar.gz: 0f19fce390ff18dcb13d80c8a4c6841dc2e096d4ea7586f0a2d6e2c6a467a45b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 89e0d194c4f14817bd2866b4b07f2efe3eea0ffdbc74ff4b7c1f5474d59fb0a18749d68fc2c724ea690f370d1b335d1e69bf0f006ab9bb1964f74fcfd689232b
|
|
7
|
+
data.tar.gz: 49150a706fb10a4ba79a87397b4cbbfcb4554eba5e974d22d6176799d377f5d9814f20862be996bca253328fc1efa6a10429947cc308f4aa3dd884eaadd378d0
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.4.0] - 2026-05-12
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Fix `enqueue: true` consumer tasks that read `depends_on` outputs by persisting the parent context before `perform_all_later`, so sub-jobs load the latest dependency outputs instead of stale persisted state
|
|
8
|
+
- Fix sequential `each` task resumption to persist continuation cursor progress at the workflow step level, resume from `step.cursor`, and keep the failed iteration retrying from the same `each_index` without skipping previously collected outputs
|
|
9
|
+
|
|
10
|
+
### Removed
|
|
11
|
+
|
|
12
|
+
- Remove the `namespace` DSL and `JobWorkflow::Namespace`, flatten task naming to a single task space, and delete the dedicated namespace guide/example coverage that existed for the removed feature
|
|
13
|
+
|
|
14
|
+
## [0.3.0] - 2026-03-13
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- Add `fetch_job_contexts(job_ids)` to queue adapter interface (`Abstract`, `SolidQueueAdapter`, `NullAdapter`) for fetching sub-job context data without direct `SolidQueue::Job` dependency from domain classes
|
|
19
|
+
- Add `persist_job_context(job)` to queue adapter interface for persisting task outputs back to SolidQueue job records after execution
|
|
20
|
+
- Add `without_query_cache` private helper to `SolidQueueAdapter` to bypass ActiveRecord query cache during polling queries
|
|
21
|
+
- Add `"job_workflow_context"` key to `find_job` return hash for direct access to workflow context data
|
|
22
|
+
- Add `AcceptanceNoDependencyWaitJob` and acceptance tests for `depends_on` without `dependency_wait` (polling-only mode)
|
|
23
|
+
- Add acceptance test for output aggregation verification in async workflows
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- **Breaking (internal):** Replace `Output#update_task_outputs_from_db` and `Output#update_task_outputs_from_jobs` with `Output#update_task_outputs_from_contexts` — callers now pass context data hashes instead of `SolidQueue::Job` objects
|
|
28
|
+
- `Runner#update_task_outputs` now routes through `QueueAdapter.current.fetch_job_contexts` instead of directly querying `SolidQueue::Job`
|
|
29
|
+
- `Runner#run` now calls `QueueAdapter.current.persist_job_context(job)` after both sub-job and workflow execution
|
|
30
|
+
- `WorkflowStatus.from_job_data` now reads `job_workflow_context` from top-level data first, falling back to `arguments.first.dig("job_workflow_context")`
|
|
31
|
+
- `reschedule_solid_queue_job` now saves full serialized job hash (`active_job.serialize.deep_stringify_keys`) instead of only `["arguments"]`
|
|
32
|
+
- Wrap `find_job`, `fetch_job_statuses`, `job_status`, `reschedule_job`, and `fetch_job_contexts` with `without_query_cache` to prevent stale reads under SolidQueue executor
|
|
33
|
+
|
|
3
34
|
## [0.2.0] - 2026-03-12
|
|
4
35
|
|
|
5
36
|
### Added
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# JobWorkflow
|
|
2
2
|
|
|
3
|
-
> ⚠️ **Early Stage (v0.
|
|
3
|
+
> ⚠️ **Early Stage (v0.4.0):** This library is in active development. APIs and features may change in breaking ways without notice. Use in production at your own risk and expect potential breaking changes in future releases.
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Production Deployment
|
|
2
2
|
|
|
3
|
-
> ⚠️ **Early Stage (v0.
|
|
3
|
+
> ⚠️ **Early Stage (v0.4.0):** JobWorkflow is still in early development. While this section outlines potential deployment patterns, please thoroughly test in your specific environment and monitor for any issues before relying on JobWorkflow in critical production systems.
|
|
4
4
|
|
|
5
5
|
This section covers suggested settings and patterns for running JobWorkflow in production-like environments.
|
|
6
6
|
|
data/guides/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# JobWorkflow Guides
|
|
2
2
|
|
|
3
|
-
> ⚠️ **Early Stage (v0.
|
|
3
|
+
> ⚠️ **Early Stage (v0.4.0):** JobWorkflow is in active development. APIs and features may change. The following guides provide patterns and examples for building workflows, but be aware that implementations may need adjustment as the library evolves.
|
|
4
4
|
|
|
5
5
|
Welcome to the JobWorkflow documentation! This directory contains comprehensive guides to help you build robust workflows with JobWorkflow.
|
|
6
6
|
|
|
@@ -67,11 +67,6 @@ Power features for complex workflows:
|
|
|
67
67
|
- Configuration options (poll_timeout, poll_interval, reschedule_delay)
|
|
68
68
|
- SolidQueue integration
|
|
69
69
|
|
|
70
|
-
- **[NAMESPACES.md](NAMESPACES.md)** - Organizing large workflows
|
|
71
|
-
- Basic namespace usage
|
|
72
|
-
- Nested namespaces
|
|
73
|
-
- Cross-namespace dependencies
|
|
74
|
-
|
|
75
70
|
- **[THROTTLING.md](THROTTLING.md)** - Rate limiting and resource control
|
|
76
71
|
- Task-level throttling
|
|
77
72
|
- Runtime throttling
|
data/lib/job_workflow/context.rb
CHANGED
|
@@ -108,13 +108,13 @@ module JobWorkflow
|
|
|
108
108
|
[task_context.parent_job_id, task.task_name].compact.join("/")
|
|
109
109
|
end
|
|
110
110
|
|
|
111
|
-
#: (Task) -> Enumerator[Context]
|
|
112
|
-
def _with_each_value(task)
|
|
111
|
+
#: (Task, ?start_index: Integer?) -> Enumerator[Context]
|
|
112
|
+
def _with_each_value(task, start_index: nil)
|
|
113
113
|
raise "Nested _with_each_value calls are not allowed" if enabled_with_each_value
|
|
114
114
|
|
|
115
115
|
self.enabled_with_each_value = true
|
|
116
116
|
Enumerator.new do |y|
|
|
117
|
-
with_task_context(task, y)
|
|
117
|
+
with_task_context(task, y, start_index:)
|
|
118
118
|
ensure
|
|
119
119
|
self.enabled_with_each_value = false
|
|
120
120
|
end
|
|
@@ -270,11 +270,11 @@ module JobWorkflow
|
|
|
270
270
|
}
|
|
271
271
|
end
|
|
272
272
|
|
|
273
|
-
#: (Task, Enumerator::Yielder) -> void
|
|
274
|
-
def with_task_context(task, yielder) # rubocop:disable Metrics/MethodLength
|
|
273
|
+
#: (Task, Enumerator::Yielder, ?start_index: Integer?) -> void
|
|
274
|
+
def with_task_context(task, yielder, start_index: nil) # rubocop:disable Metrics/MethodLength
|
|
275
275
|
reset_task_context_if_task_changed(task)
|
|
276
276
|
|
|
277
|
-
with_each_index_and_value(task) do |value, index|
|
|
277
|
+
with_each_index_and_value(task, start_index:) do |value, index|
|
|
278
278
|
dry_run = calculate_dry_run(task)
|
|
279
279
|
with_retry(task) do |retry_count|
|
|
280
280
|
self.task_context = TaskContext.new(task:, parent_job_id:, index:, value:, retry_count:, dry_run:)
|
|
@@ -294,10 +294,12 @@ module JobWorkflow
|
|
|
294
294
|
self.task_context = TaskContext.new if task_context.task&.task_name != task.task_name
|
|
295
295
|
end
|
|
296
296
|
|
|
297
|
-
#: (Task) { (untyped, Integer) -> void } -> void
|
|
298
|
-
def with_each_index_and_value(task)
|
|
297
|
+
#: (Task, ?start_index: Integer?) { (untyped, Integer) -> void } -> void
|
|
298
|
+
def with_each_index_and_value(task, start_index: nil)
|
|
299
|
+
resume_index = start_index || task_context.index
|
|
300
|
+
|
|
299
301
|
task.each.call(self).each.with_index do |value, index|
|
|
300
|
-
next if index <
|
|
302
|
+
next if index < resume_index
|
|
301
303
|
|
|
302
304
|
yield value, index
|
|
303
305
|
|
data/lib/job_workflow/dsl.rb
CHANGED
|
@@ -108,15 +108,9 @@ module JobWorkflow
|
|
|
108
108
|
|
|
109
109
|
#: (Symbol argument_name, String type, ?default: untyped) -> void
|
|
110
110
|
def argument(argument_name, type, default: nil)
|
|
111
|
-
validate_namespace!
|
|
112
111
|
_workflow.add_argument(ArgumentDef.new(name: argument_name, type:, default:))
|
|
113
112
|
end
|
|
114
113
|
|
|
115
|
-
#: (Symbol) { () -> void } -> void
|
|
116
|
-
def namespace(namespace_name, &)
|
|
117
|
-
_workflow.add_namespace(Namespace.new(name: namespace_name), &)
|
|
118
|
-
end
|
|
119
|
-
|
|
120
114
|
# rubocop:disable Metrics/ParameterLists
|
|
121
115
|
#
|
|
122
116
|
#: (
|
|
@@ -149,7 +143,6 @@ module JobWorkflow
|
|
|
149
143
|
new_task = Task.new(
|
|
150
144
|
job_name: name,
|
|
151
145
|
name: task_name,
|
|
152
|
-
namespace: _workflow.namespace,
|
|
153
146
|
block: block,
|
|
154
147
|
enqueue:,
|
|
155
148
|
each:,
|
|
@@ -172,25 +165,21 @@ module JobWorkflow
|
|
|
172
165
|
|
|
173
166
|
#: (*Symbol) { (Context) -> void } -> void
|
|
174
167
|
def before(*task_names, &block)
|
|
175
|
-
validate_namespace!
|
|
176
168
|
_workflow.add_hook(:before, task_names:, block:)
|
|
177
169
|
end
|
|
178
170
|
|
|
179
171
|
#: (*Symbol) { (Context) -> void } -> void
|
|
180
172
|
def after(*task_names, &block)
|
|
181
|
-
validate_namespace!
|
|
182
173
|
_workflow.add_hook(:after, task_names:, block:)
|
|
183
174
|
end
|
|
184
175
|
|
|
185
176
|
#: (*Symbol) { (Context, TaskCallable) -> void } -> void
|
|
186
177
|
def around(*task_names, &block)
|
|
187
|
-
validate_namespace!
|
|
188
178
|
_workflow.add_hook(:around, task_names:, block:)
|
|
189
179
|
end
|
|
190
180
|
|
|
191
181
|
#: (*Symbol) { (Context, StandardError, Task) -> void } -> void
|
|
192
182
|
def on_error(*task_names, &block)
|
|
193
|
-
validate_namespace!
|
|
194
183
|
_workflow.add_hook(:error, task_names:, block:)
|
|
195
184
|
end
|
|
196
185
|
|
|
@@ -240,7 +229,6 @@ module JobWorkflow
|
|
|
240
229
|
|
|
241
230
|
#: (?bool) ?{ (Context) -> bool } -> void
|
|
242
231
|
def dry_run(value = nil, &block)
|
|
243
|
-
validate_namespace!
|
|
244
232
|
_workflow.dry_run_config = block || value
|
|
245
233
|
end
|
|
246
234
|
|
|
@@ -254,7 +242,6 @@ module JobWorkflow
|
|
|
254
242
|
# ?description: String?
|
|
255
243
|
# ) -> void
|
|
256
244
|
def schedule(expression, key: nil, queue: nil, priority: nil, args: {}, description: nil)
|
|
257
|
-
validate_namespace!
|
|
258
245
|
_workflow.add_schedule(
|
|
259
246
|
Schedule.new(
|
|
260
247
|
expression:,
|
|
@@ -268,13 +255,6 @@ module JobWorkflow
|
|
|
268
255
|
)
|
|
269
256
|
end
|
|
270
257
|
# rubocop:enable Metrics/ParameterLists
|
|
271
|
-
|
|
272
|
-
private
|
|
273
|
-
|
|
274
|
-
#: () -> void
|
|
275
|
-
def validate_namespace!
|
|
276
|
-
raise "cannot be defined within a namespace." unless _workflow.namespace.default?
|
|
277
|
-
end
|
|
278
258
|
end
|
|
279
259
|
end
|
|
280
260
|
end
|
data/lib/job_workflow/output.rb
CHANGED
|
@@ -50,18 +50,10 @@ module JobWorkflow
|
|
|
50
50
|
task_outputs[task_output.task_name][task_output.each_index] = task_output
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
-
#: (Array[String], Workflow) -> void
|
|
54
|
-
def
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
update_task_outputs_from_jobs(jobs.to_a, workflow)
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
#: (Array[SolidQueue::Job], Workflow) -> void
|
|
62
|
-
def update_task_outputs_from_jobs(jobs, workflow)
|
|
63
|
-
jobs.each do |job|
|
|
64
|
-
context = Context.deserialize(job.arguments["job_workflow_context"].merge("workflow" => workflow))
|
|
53
|
+
#: (Array[Hash[String, untyped]], Workflow) -> void
|
|
54
|
+
def update_task_outputs_from_contexts(context_data_list, workflow)
|
|
55
|
+
context_data_list.each do |context_data|
|
|
56
|
+
context = Context.deserialize(context_data.merge("workflow" => workflow))
|
|
65
57
|
task_output = context.each_task_output
|
|
66
58
|
next if task_output.nil?
|
|
67
59
|
|
|
@@ -77,10 +77,18 @@ module JobWorkflow
|
|
|
77
77
|
raise NotImplementedError, "#{self.class}#find_job must be implemented"
|
|
78
78
|
end
|
|
79
79
|
|
|
80
|
+
#: (Array[String]) -> Array[Hash[String, untyped]]
|
|
81
|
+
def fetch_job_contexts(_job_ids)
|
|
82
|
+
raise NotImplementedError, "#{self.class}#fetch_job_contexts must be implemented"
|
|
83
|
+
end
|
|
84
|
+
|
|
80
85
|
#: (DSL, Numeric) -> bool
|
|
81
86
|
def reschedule_job(_job, _wait)
|
|
82
87
|
false
|
|
83
88
|
end
|
|
89
|
+
|
|
90
|
+
#: (DSL) -> void
|
|
91
|
+
def persist_job_context(_job); end
|
|
84
92
|
end
|
|
85
93
|
# rubocop:enable Naming/PredicateMethod
|
|
86
94
|
end
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module JobWorkflow
|
|
4
4
|
module QueueAdapters
|
|
5
|
-
# rubocop:disable Naming/PredicateMethod
|
|
5
|
+
# rubocop:disable Naming/PredicateMethod, Metrics/ClassLength
|
|
6
6
|
class SolidQueueAdapter < Abstract
|
|
7
7
|
# @note
|
|
8
8
|
# - Registry scope: @semaphore_registry is process-scoped (shared across fibers/threads
|
|
@@ -74,16 +74,20 @@ module JobWorkflow
|
|
|
74
74
|
def fetch_job_statuses(job_ids)
|
|
75
75
|
return {} unless defined?(SolidQueue::Job)
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
without_query_cache do
|
|
78
|
+
SolidQueue::Job.where(active_job_id: job_ids).index_by(&:active_job_id)
|
|
79
|
+
end
|
|
78
80
|
end
|
|
79
81
|
|
|
80
82
|
#: (untyped) -> Symbol
|
|
81
83
|
def job_status(job)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
without_query_cache do
|
|
85
|
+
return :failed if job.failed?
|
|
86
|
+
return :succeeded if job.finished?
|
|
87
|
+
return :running if job.claimed?
|
|
85
88
|
|
|
86
|
-
|
|
89
|
+
:pending
|
|
90
|
+
end
|
|
87
91
|
end
|
|
88
92
|
|
|
89
93
|
#: () -> bool
|
|
@@ -153,23 +157,40 @@ module JobWorkflow
|
|
|
153
157
|
def find_job(job_id)
|
|
154
158
|
return unless defined?(SolidQueue::Job)
|
|
155
159
|
|
|
156
|
-
job = SolidQueue::Job.find_by(active_job_id: job_id)
|
|
160
|
+
job = without_query_cache { SolidQueue::Job.find_by(active_job_id: job_id) }
|
|
157
161
|
return if job.nil?
|
|
158
162
|
|
|
163
|
+
args = job.arguments
|
|
159
164
|
{
|
|
160
165
|
"job_id" => job.active_job_id,
|
|
161
166
|
"class_name" => job.class_name,
|
|
162
167
|
"queue_name" => job.queue_name,
|
|
163
|
-
"arguments" =>
|
|
168
|
+
"arguments" => args.is_a?(Hash) ? args["arguments"] : args,
|
|
169
|
+
"job_workflow_context" => args.is_a?(Hash) ? args["job_workflow_context"] : nil,
|
|
164
170
|
"status" => job_status(job)
|
|
165
171
|
}
|
|
166
172
|
end
|
|
167
173
|
|
|
174
|
+
# @note
|
|
175
|
+
# - Fetches job_workflow_context hashes for the given job IDs.
|
|
176
|
+
#
|
|
177
|
+
#: (Array[String]) -> Array[Hash[String, untyped]]
|
|
178
|
+
def fetch_job_contexts(job_ids)
|
|
179
|
+
return [] unless defined?(SolidQueue::Job)
|
|
180
|
+
return [] if job_ids.empty?
|
|
181
|
+
|
|
182
|
+
jobs = without_query_cache { SolidQueue::Job.where(active_job_id: job_ids).to_a }
|
|
183
|
+
jobs.filter_map do |job|
|
|
184
|
+
args = job.arguments
|
|
185
|
+
args.is_a?(Hash) ? args["job_workflow_context"] : nil
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
168
189
|
#: (DSL, Numeric) -> bool
|
|
169
190
|
def reschedule_job(job, wait)
|
|
170
191
|
return false unless defined?(SolidQueue::Job)
|
|
171
192
|
|
|
172
|
-
solid_queue_job = SolidQueue::Job.find_by(active_job_id: job.job_id)
|
|
193
|
+
solid_queue_job = without_query_cache { SolidQueue::Job.find_by(active_job_id: job.job_id) }
|
|
173
194
|
return false unless solid_queue_job&.claimed?
|
|
174
195
|
|
|
175
196
|
reschedule_solid_queue_job(solid_queue_job, job, wait)
|
|
@@ -177,17 +198,44 @@ module JobWorkflow
|
|
|
177
198
|
false
|
|
178
199
|
end
|
|
179
200
|
|
|
201
|
+
# @note
|
|
202
|
+
# - Persists the job's updated context (including task outputs) back
|
|
203
|
+
# to the SolidQueue job record after execution completes. Without this,
|
|
204
|
+
# outputs computed during job execution would be lost because
|
|
205
|
+
# SolidQueue does not re-serialize job arguments after perform.
|
|
206
|
+
#
|
|
207
|
+
#: (DSL) -> void
|
|
208
|
+
def persist_job_context(job)
|
|
209
|
+
return unless defined?(SolidQueue::Job)
|
|
210
|
+
|
|
211
|
+
solid_queue_job = SolidQueue::Job.find_by(active_job_id: job.job_id)
|
|
212
|
+
return if solid_queue_job.nil?
|
|
213
|
+
|
|
214
|
+
solid_queue_job.update!(arguments: job.serialize.deep_stringify_keys)
|
|
215
|
+
end
|
|
216
|
+
|
|
180
217
|
private
|
|
181
218
|
|
|
182
219
|
attr_reader :semaphore_registry #: Hash[Object, ^(SolidQueue::Worker) -> void]
|
|
183
220
|
|
|
221
|
+
# @note
|
|
222
|
+
# - Bypasses ActiveRecord query cache for the given block.
|
|
223
|
+
# - When running under SolidQueue's executor, SELECT queries are cached
|
|
224
|
+
# for the entire job execution. Polling queries must bypass this cache
|
|
225
|
+
# to observe status changes made by other threads/processes.
|
|
226
|
+
#
|
|
227
|
+
#: [T] () { () -> T } -> T
|
|
228
|
+
def without_query_cache(&)
|
|
229
|
+
defined?(SolidQueue::Job) ? SolidQueue::Job.uncached(&) : yield
|
|
230
|
+
end
|
|
231
|
+
|
|
184
232
|
#: (SolidQueue::Job, DSL, Numeric) -> bool
|
|
185
233
|
def reschedule_solid_queue_job(solid_queue_job, active_job, wait)
|
|
186
234
|
solid_queue_job.with_lock do
|
|
187
235
|
solid_queue_job.claimed_execution&.destroy!
|
|
188
236
|
solid_queue_job.update!(
|
|
189
237
|
scheduled_at: wait.seconds.from_now,
|
|
190
|
-
arguments: active_job.serialize.deep_stringify_keys
|
|
238
|
+
arguments: active_job.serialize.deep_stringify_keys
|
|
191
239
|
)
|
|
192
240
|
solid_queue_job.prepare_for_execution
|
|
193
241
|
end
|
|
@@ -219,6 +267,6 @@ module JobWorkflow
|
|
|
219
267
|
end
|
|
220
268
|
end
|
|
221
269
|
end
|
|
222
|
-
# rubocop:enable Naming/PredicateMethod
|
|
270
|
+
# rubocop:enable Naming/PredicateMethod, Metrics/ClassLength
|
|
223
271
|
end
|
|
224
272
|
end
|
data/lib/job_workflow/runner.rb
CHANGED
|
@@ -13,9 +13,14 @@ module JobWorkflow
|
|
|
13
13
|
#: () -> void
|
|
14
14
|
def run
|
|
15
15
|
task = context._task_context.task
|
|
16
|
-
|
|
16
|
+
if !task.nil? && context.sub_job?
|
|
17
|
+
run_task(task)
|
|
18
|
+
persist_current_job_context
|
|
19
|
+
return
|
|
20
|
+
end
|
|
17
21
|
|
|
18
22
|
catch(:rescheduled) { run_workflow }
|
|
23
|
+
persist_current_job_context
|
|
19
24
|
end
|
|
20
25
|
|
|
21
26
|
private
|
|
@@ -45,7 +50,7 @@ module JobWorkflow
|
|
|
45
50
|
|
|
46
51
|
job.step(task.task_name) do |step|
|
|
47
52
|
wait_for_dependent_tasks(task, step)
|
|
48
|
-
task.enqueue.should_enqueue?(context) ? enqueue_task(task) : run_task(task)
|
|
53
|
+
task.enqueue.should_enqueue?(context) ? enqueue_task(task) : run_task(task, step:)
|
|
49
54
|
end
|
|
50
55
|
end
|
|
51
56
|
end
|
|
@@ -58,11 +63,12 @@ module JobWorkflow
|
|
|
58
63
|
result
|
|
59
64
|
end
|
|
60
65
|
|
|
61
|
-
#: (Task) -> void
|
|
62
|
-
def run_task(task)
|
|
66
|
+
#: (Task, ?step: ActiveJob::Continuation::Step?) -> void
|
|
67
|
+
def run_task(task, step: nil)
|
|
63
68
|
context._load_parent_task_output
|
|
64
|
-
context._with_each_value(task).each do |ctx|
|
|
69
|
+
context._with_each_value(task, start_index: step&.cursor).each do |ctx|
|
|
65
70
|
run_each_task(task, ctx)
|
|
71
|
+
step&.set!(ctx._task_context.index + 1)
|
|
66
72
|
rescue StandardError => e
|
|
67
73
|
run_error_hooks(task, ctx, e)
|
|
68
74
|
raise
|
|
@@ -107,6 +113,7 @@ module JobWorkflow
|
|
|
107
113
|
#: (Task) -> void
|
|
108
114
|
def enqueue_task(task)
|
|
109
115
|
sub_jobs = context._with_each_value(task).map { |ctx| job.class.from_context(ctx) }
|
|
116
|
+
persist_current_job_context
|
|
110
117
|
ActiveJob.perform_all_later(sub_jobs)
|
|
111
118
|
context.job_status.update_task_job_statuses_from_jobs(task_name: task.task_name, jobs: sub_jobs)
|
|
112
119
|
Instrumentation.notify_task_enqueue(job, task, sub_jobs.size)
|
|
@@ -167,7 +174,13 @@ module JobWorkflow
|
|
|
167
174
|
#: (Task) -> void
|
|
168
175
|
def update_task_outputs(task)
|
|
169
176
|
finished_job_ids = context.job_status.finished_job_ids(task_name: task.task_name)
|
|
170
|
-
|
|
177
|
+
context_data_list = QueueAdapter.current.fetch_job_contexts(finished_job_ids)
|
|
178
|
+
context.output.update_task_outputs_from_contexts(context_data_list, context.workflow)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
#: () -> void
|
|
182
|
+
def persist_current_job_context
|
|
183
|
+
QueueAdapter.current.persist_job_context(job)
|
|
171
184
|
end
|
|
172
185
|
end
|
|
173
186
|
end
|
data/lib/job_workflow/task.rb
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
module JobWorkflow
|
|
4
4
|
class Task
|
|
5
5
|
attr_reader :job_name #: String
|
|
6
|
-
attr_reader :namespace #: Namespace
|
|
7
6
|
attr_reader :block #: ^(untyped) -> void
|
|
8
7
|
attr_reader :each #: ^(Context) -> untyped
|
|
9
8
|
attr_reader :enqueue #: TaskEnqueue
|
|
@@ -16,11 +15,10 @@ module JobWorkflow
|
|
|
16
15
|
attr_reader :dependency_wait #: TaskDependencyWait
|
|
17
16
|
attr_reader :dry_run_config #: DryRunConfig
|
|
18
17
|
|
|
19
|
-
# rubocop:disable Metrics/ParameterLists, Metrics/MethodLength
|
|
18
|
+
# rubocop:disable Metrics/ParameterLists, Metrics/MethodLength
|
|
20
19
|
#: (
|
|
21
20
|
# job_name: String,
|
|
22
21
|
# name: Symbol,
|
|
23
|
-
# namespace: Namespace,
|
|
24
22
|
# block: ^(untyped) -> void,
|
|
25
23
|
# ?each: ^(Context) -> untyped,
|
|
26
24
|
# ?enqueue: true | false | ^(Context) -> bool | Hash[Symbol, untyped],
|
|
@@ -36,7 +34,6 @@ module JobWorkflow
|
|
|
36
34
|
def initialize(
|
|
37
35
|
job_name:,
|
|
38
36
|
name:,
|
|
39
|
-
namespace:,
|
|
40
37
|
block:,
|
|
41
38
|
each: nil,
|
|
42
39
|
enqueue: nil,
|
|
@@ -51,7 +48,6 @@ module JobWorkflow
|
|
|
51
48
|
)
|
|
52
49
|
@job_name = job_name
|
|
53
50
|
@name = name
|
|
54
|
-
@namespace = namespace #: Namespace
|
|
55
51
|
@block = block
|
|
56
52
|
@each = each
|
|
57
53
|
@enqueue = TaskEnqueue.from_primitive_value(enqueue)
|
|
@@ -64,11 +60,11 @@ module JobWorkflow
|
|
|
64
60
|
@dependency_wait = TaskDependencyWait.from_primitive_value(dependency_wait)
|
|
65
61
|
@dry_run_config = DryRunConfig.from_primitive_value(dry_run)
|
|
66
62
|
end
|
|
67
|
-
# rubocop:enable Metrics/ParameterLists, Metrics/MethodLength
|
|
63
|
+
# rubocop:enable Metrics/ParameterLists, Metrics/MethodLength
|
|
68
64
|
|
|
69
65
|
#: () -> Symbol
|
|
70
66
|
def task_name
|
|
71
|
-
|
|
67
|
+
name
|
|
72
68
|
end
|
|
73
69
|
|
|
74
70
|
#: () -> String
|
data/lib/job_workflow/version.rb
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
module JobWorkflow
|
|
4
4
|
class Workflow
|
|
5
|
-
attr_reader :namespace #: Namespace
|
|
6
5
|
attr_reader :dry_run_config #: DryRunConfig
|
|
7
6
|
|
|
8
7
|
#: () -> void
|
|
@@ -10,7 +9,6 @@ module JobWorkflow
|
|
|
10
9
|
@task_graph = TaskGraph.new
|
|
11
10
|
@argument_defs = {} #: Hash[Symbol, ArgumentDef]
|
|
12
11
|
@hook_registry = HookRegistry.new
|
|
13
|
-
@namespace = Namespace.default #: Namespace
|
|
14
12
|
@schedules = {} #: Hash[Symbol, Schedule]
|
|
15
13
|
@dry_run_config = DryRunConfig.new
|
|
16
14
|
end
|
|
@@ -20,15 +18,6 @@ module JobWorkflow
|
|
|
20
18
|
@dry_run_config = DryRunConfig.from_primitive_value(value)
|
|
21
19
|
end
|
|
22
20
|
|
|
23
|
-
#: (Namespace) { () -> void } -> void
|
|
24
|
-
def add_namespace(namespace)
|
|
25
|
-
original_namespace = @namespace
|
|
26
|
-
@namespace = namespace.update_parent(original_namespace)
|
|
27
|
-
yield
|
|
28
|
-
ensure
|
|
29
|
-
@namespace = original_namespace
|
|
30
|
-
end
|
|
31
|
-
|
|
32
21
|
#: (Task) -> void
|
|
33
22
|
def add_task(task)
|
|
34
23
|
@task_graph.add(task)
|
|
@@ -34,7 +34,7 @@ module JobWorkflow
|
|
|
34
34
|
job_class = job_class_name.constantize
|
|
35
35
|
workflow = job_class._workflow
|
|
36
36
|
|
|
37
|
-
context_data = data["arguments"]
|
|
37
|
+
context_data = data["job_workflow_context"] || data["arguments"]&.first&.dig("job_workflow_context")
|
|
38
38
|
context = if context_data
|
|
39
39
|
Context.deserialize(context_data.merge("workflow" => workflow))
|
|
40
40
|
else
|
data/lib/job_workflow.rb
CHANGED
|
@@ -23,7 +23,6 @@ require_relative "job_workflow/task_throttle"
|
|
|
23
23
|
require_relative "job_workflow/task_enqueue"
|
|
24
24
|
require_relative "job_workflow/task_dependency_wait"
|
|
25
25
|
require_relative "job_workflow/semaphore"
|
|
26
|
-
require_relative "job_workflow/namespace"
|
|
27
26
|
require_relative "job_workflow/hook"
|
|
28
27
|
require_relative "job_workflow/error_hook"
|
|
29
28
|
require_relative "job_workflow/hook_registry"
|
data/rbs_collection.lock.yaml
CHANGED
|
@@ -6,7 +6,7 @@ gems:
|
|
|
6
6
|
source:
|
|
7
7
|
type: git
|
|
8
8
|
name: ruby/gem_rbs_collection
|
|
9
|
-
revision:
|
|
9
|
+
revision: 81fa8bd0617286078617a62b6a3229cebfd4af23
|
|
10
10
|
remote: https://github.com/ruby/gem_rbs_collection.git
|
|
11
11
|
repo_dir: gems
|
|
12
12
|
- name: activerecord
|
|
@@ -14,7 +14,7 @@ gems:
|
|
|
14
14
|
source:
|
|
15
15
|
type: git
|
|
16
16
|
name: ruby/gem_rbs_collection
|
|
17
|
-
revision:
|
|
17
|
+
revision: 81fa8bd0617286078617a62b6a3229cebfd4af23
|
|
18
18
|
remote: https://github.com/ruby/gem_rbs_collection.git
|
|
19
19
|
repo_dir: gems
|
|
20
20
|
- name: activesupport
|
|
@@ -22,7 +22,7 @@ gems:
|
|
|
22
22
|
source:
|
|
23
23
|
type: git
|
|
24
24
|
name: ruby/gem_rbs_collection
|
|
25
|
-
revision:
|
|
25
|
+
revision: 81fa8bd0617286078617a62b6a3229cebfd4af23
|
|
26
26
|
remote: https://github.com/ruby/gem_rbs_collection.git
|
|
27
27
|
repo_dir: gems
|
|
28
28
|
- name: base64
|
|
@@ -34,7 +34,7 @@ gems:
|
|
|
34
34
|
source:
|
|
35
35
|
type: git
|
|
36
36
|
name: ruby/gem_rbs_collection
|
|
37
|
-
revision:
|
|
37
|
+
revision: 81fa8bd0617286078617a62b6a3229cebfd4af23
|
|
38
38
|
remote: https://github.com/ruby/gem_rbs_collection.git
|
|
39
39
|
repo_dir: gems
|
|
40
40
|
- name: concurrent-ruby
|
|
@@ -42,7 +42,7 @@ gems:
|
|
|
42
42
|
source:
|
|
43
43
|
type: git
|
|
44
44
|
name: ruby/gem_rbs_collection
|
|
45
|
-
revision:
|
|
45
|
+
revision: 81fa8bd0617286078617a62b6a3229cebfd4af23
|
|
46
46
|
remote: https://github.com/ruby/gem_rbs_collection.git
|
|
47
47
|
repo_dir: gems
|
|
48
48
|
- name: connection_pool
|
|
@@ -50,7 +50,7 @@ gems:
|
|
|
50
50
|
source:
|
|
51
51
|
type: git
|
|
52
52
|
name: ruby/gem_rbs_collection
|
|
53
|
-
revision:
|
|
53
|
+
revision: 81fa8bd0617286078617a62b6a3229cebfd4af23
|
|
54
54
|
remote: https://github.com/ruby/gem_rbs_collection.git
|
|
55
55
|
repo_dir: gems
|
|
56
56
|
- name: date
|
|
@@ -74,7 +74,7 @@ gems:
|
|
|
74
74
|
source:
|
|
75
75
|
type: git
|
|
76
76
|
name: ruby/gem_rbs_collection
|
|
77
|
-
revision:
|
|
77
|
+
revision: 81fa8bd0617286078617a62b6a3229cebfd4af23
|
|
78
78
|
remote: https://github.com/ruby/gem_rbs_collection.git
|
|
79
79
|
repo_dir: gems
|
|
80
80
|
- name: i18n
|
|
@@ -82,7 +82,7 @@ gems:
|
|
|
82
82
|
source:
|
|
83
83
|
type: git
|
|
84
84
|
name: ruby/gem_rbs_collection
|
|
85
|
-
revision:
|
|
85
|
+
revision: 81fa8bd0617286078617a62b6a3229cebfd4af23
|
|
86
86
|
remote: https://github.com/ruby/gem_rbs_collection.git
|
|
87
87
|
repo_dir: gems
|
|
88
88
|
- name: json
|
|
@@ -98,7 +98,7 @@ gems:
|
|
|
98
98
|
source:
|
|
99
99
|
type: git
|
|
100
100
|
name: ruby/gem_rbs_collection
|
|
101
|
-
revision:
|
|
101
|
+
revision: 81fa8bd0617286078617a62b6a3229cebfd4af23
|
|
102
102
|
remote: https://github.com/ruby/gem_rbs_collection.git
|
|
103
103
|
repo_dir: gems
|
|
104
104
|
- name: monitor
|
|
@@ -138,7 +138,7 @@ gems:
|
|
|
138
138
|
source:
|
|
139
139
|
type: git
|
|
140
140
|
name: ruby/gem_rbs_collection
|
|
141
|
-
revision:
|
|
141
|
+
revision: 81fa8bd0617286078617a62b6a3229cebfd4af23
|
|
142
142
|
remote: https://github.com/ruby/gem_rbs_collection.git
|
|
143
143
|
repo_dir: gems
|
|
144
144
|
- name: stringio
|
|
@@ -162,7 +162,7 @@ gems:
|
|
|
162
162
|
source:
|
|
163
163
|
type: git
|
|
164
164
|
name: ruby/gem_rbs_collection
|
|
165
|
-
revision:
|
|
165
|
+
revision: 81fa8bd0617286078617a62b6a3229cebfd4af23
|
|
166
166
|
remote: https://github.com/ruby/gem_rbs_collection.git
|
|
167
167
|
repo_dir: gems
|
|
168
168
|
- name: uri
|
|
@@ -48,8 +48,8 @@ module JobWorkflow
|
|
|
48
48
|
# : () -> String?
|
|
49
49
|
def concurrency_key: () -> String?
|
|
50
50
|
|
|
51
|
-
# : (Task) -> Enumerator[Context]
|
|
52
|
-
def _with_each_value: (Task) -> Enumerator[Context]
|
|
51
|
+
# : (Task, ?start_index: Integer?) -> Enumerator[Context]
|
|
52
|
+
def _with_each_value: (Task, ?start_index: Integer?) -> Enumerator[Context]
|
|
53
53
|
|
|
54
54
|
# : () { () -> void } -> void
|
|
55
55
|
def _with_task_throttle: () { () -> void } -> void
|
|
@@ -128,14 +128,14 @@ module JobWorkflow
|
|
|
128
128
|
# : () -> Hash[String, untyped]
|
|
129
129
|
def serialize_for_sub_job: () -> Hash[String, untyped]
|
|
130
130
|
|
|
131
|
-
# : (Task, Enumerator::Yielder) -> void
|
|
132
|
-
def with_task_context: (Task, Enumerator::Yielder) -> void
|
|
131
|
+
# : (Task, Enumerator::Yielder, ?start_index: Integer?) -> void
|
|
132
|
+
def with_task_context: (Task, Enumerator::Yielder, ?start_index: Integer?) -> void
|
|
133
133
|
|
|
134
134
|
# : (Task) -> void
|
|
135
135
|
def reset_task_context_if_task_changed: (Task) -> void
|
|
136
136
|
|
|
137
|
-
# : (Task) { (untyped, Integer) -> void } -> void
|
|
138
|
-
def with_each_index_and_value: (Task) { (untyped, Integer) -> void } -> void
|
|
137
|
+
# : (Task, ?start_index: Integer?) { (untyped, Integer) -> void } -> void
|
|
138
|
+
def with_each_index_and_value: (Task, ?start_index: Integer?) { (untyped, Integer) -> void } -> void
|
|
139
139
|
|
|
140
140
|
# : () -> void
|
|
141
141
|
def clear_after_each_index_and_value: () -> void
|
|
@@ -62,9 +62,6 @@ module JobWorkflow
|
|
|
62
62
|
# : (Symbol argument_name, String type, ?default: untyped) -> void
|
|
63
63
|
def argument: (Symbol argument_name, String type, ?default: untyped) -> void
|
|
64
64
|
|
|
65
|
-
# : (Symbol) { () -> void } -> void
|
|
66
|
-
def namespace: (Symbol) { () -> void } -> void
|
|
67
|
-
|
|
68
65
|
# rubocop:disable Metrics/ParameterLists
|
|
69
66
|
#
|
|
70
67
|
# : (
|
|
@@ -139,11 +136,6 @@ module JobWorkflow
|
|
|
139
136
|
# ?description: String?
|
|
140
137
|
# ) -> void
|
|
141
138
|
def schedule: (String expression, ?key: (String | Symbol)?, ?queue: String?, ?priority: Integer?, ?args: Hash[Symbol, untyped], ?description: String?) -> void
|
|
142
|
-
|
|
143
|
-
private
|
|
144
|
-
|
|
145
|
-
# : () -> void
|
|
146
|
-
def validate_namespace!: () -> void
|
|
147
139
|
end
|
|
148
140
|
end
|
|
149
141
|
end
|
|
@@ -23,11 +23,8 @@ module JobWorkflow
|
|
|
23
23
|
# : (TaskOutput) -> void
|
|
24
24
|
def add_task_output: (TaskOutput) -> void
|
|
25
25
|
|
|
26
|
-
# : (Array[String], Workflow) -> void
|
|
27
|
-
def
|
|
28
|
-
|
|
29
|
-
# : (Array[SolidQueue::Job], Workflow) -> void
|
|
30
|
-
def update_task_outputs_from_jobs: (Array[SolidQueue::Job], Workflow) -> void
|
|
26
|
+
# : (Array[Hash[String, untyped]], Workflow) -> void
|
|
27
|
+
def update_task_outputs_from_contexts: (Array[Hash[String, untyped]], Workflow) -> void
|
|
31
28
|
|
|
32
29
|
# : () -> Array[TaskOutput]
|
|
33
30
|
def flat_task_outputs: () -> Array[TaskOutput]
|
|
@@ -49,8 +49,14 @@ module JobWorkflow
|
|
|
49
49
|
# : (String) -> Hash[String, untyped]?
|
|
50
50
|
def find_job: (String) -> Hash[String, untyped]?
|
|
51
51
|
|
|
52
|
+
# : (Array[String]) -> Array[Hash[String, untyped]]
|
|
53
|
+
def fetch_job_contexts: (Array[String]) -> Array[Hash[String, untyped]]
|
|
54
|
+
|
|
52
55
|
# : (DSL, Numeric) -> bool
|
|
53
56
|
def reschedule_job: (DSL, Numeric) -> bool
|
|
57
|
+
|
|
58
|
+
# : (DSL) -> void
|
|
59
|
+
def persist_job_context: (DSL) -> void
|
|
54
60
|
end
|
|
55
61
|
end
|
|
56
62
|
end
|
|
@@ -51,6 +51,9 @@ module JobWorkflow
|
|
|
51
51
|
# : (String) -> Hash[String, untyped]?
|
|
52
52
|
def find_job: (String) -> Hash[String, untyped]?
|
|
53
53
|
|
|
54
|
+
# : (Array[String]) -> Array[Hash[String, untyped]]
|
|
55
|
+
def fetch_job_contexts: (Array[String]) -> Array[Hash[String, untyped]]
|
|
56
|
+
|
|
54
57
|
# : (DSL, Numeric) -> bool
|
|
55
58
|
def reschedule_job: (DSL, Numeric) -> bool
|
|
56
59
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module JobWorkflow
|
|
4
4
|
module QueueAdapters
|
|
5
|
-
# rubocop:disable Naming/PredicateMethod
|
|
5
|
+
# rubocop:disable Naming/PredicateMethod, Metrics/ClassLength
|
|
6
6
|
class SolidQueueAdapter < Abstract
|
|
7
7
|
# @note
|
|
8
8
|
# - Registry scope: @semaphore_registry is process-scoped (shared across fibers/threads
|
|
@@ -82,13 +82,37 @@ module JobWorkflow
|
|
|
82
82
|
# : (String) -> Hash[String, untyped]?
|
|
83
83
|
def find_job: (String) -> Hash[String, untyped]?
|
|
84
84
|
|
|
85
|
+
# @note
|
|
86
|
+
# - Fetches job_workflow_context hashes for the given job IDs.
|
|
87
|
+
#
|
|
88
|
+
# : (Array[String]) -> Array[Hash[String, untyped]]
|
|
89
|
+
def fetch_job_contexts: (Array[String]) -> Array[Hash[String, untyped]]
|
|
90
|
+
|
|
85
91
|
# : (DSL, Numeric) -> bool
|
|
86
92
|
def reschedule_job: (DSL, Numeric) -> bool
|
|
87
93
|
|
|
94
|
+
# @note
|
|
95
|
+
# - Persists the job's updated context (including task outputs) back
|
|
96
|
+
# to the SolidQueue job record after execution completes. Without this,
|
|
97
|
+
# outputs computed during job execution would be lost because
|
|
98
|
+
# SolidQueue does not re-serialize job arguments after perform.
|
|
99
|
+
#
|
|
100
|
+
# : (DSL) -> void
|
|
101
|
+
def persist_job_context: (DSL) -> void
|
|
102
|
+
|
|
88
103
|
private
|
|
89
104
|
|
|
90
105
|
attr_reader semaphore_registry: Hash[Object, ^(SolidQueue::Worker) -> void]
|
|
91
106
|
|
|
107
|
+
# @note
|
|
108
|
+
# - Bypasses ActiveRecord query cache for the given block.
|
|
109
|
+
# - When running under SolidQueue's executor, SELECT queries are cached
|
|
110
|
+
# for the entire job execution. Polling queries must bypass this cache
|
|
111
|
+
# to observe status changes made by other threads/processes.
|
|
112
|
+
#
|
|
113
|
+
# : [T] () { () -> T } -> T
|
|
114
|
+
def without_query_cache: [T] () { () -> T } -> T
|
|
115
|
+
|
|
92
116
|
# : (SolidQueue::Job, DSL, Numeric) -> bool
|
|
93
117
|
def reschedule_solid_queue_job: (SolidQueue::Job, DSL, Numeric) -> bool
|
|
94
118
|
|
|
@@ -30,8 +30,8 @@ module JobWorkflow
|
|
|
30
30
|
# : (Task) -> bool
|
|
31
31
|
def skip_task?: (Task) -> bool
|
|
32
32
|
|
|
33
|
-
# : (Task) -> void
|
|
34
|
-
def run_task: (Task) -> void
|
|
33
|
+
# : (Task, ?step: ActiveJob::Continuation::Step?) -> void
|
|
34
|
+
def run_task: (Task, ?step: ActiveJob::Continuation::Step?) -> void
|
|
35
35
|
|
|
36
36
|
# : (Task, Context) -> void
|
|
37
37
|
def run_each_task: (Task, Context) -> void
|
|
@@ -62,5 +62,8 @@ module JobWorkflow
|
|
|
62
62
|
|
|
63
63
|
# : (Task) -> void
|
|
64
64
|
def update_task_outputs: (Task) -> void
|
|
65
|
+
|
|
66
|
+
# : () -> void
|
|
67
|
+
def persist_current_job_context: () -> void
|
|
65
68
|
end
|
|
66
69
|
end
|
|
@@ -4,8 +4,6 @@ module JobWorkflow
|
|
|
4
4
|
class Task
|
|
5
5
|
attr_reader job_name: String
|
|
6
6
|
|
|
7
|
-
attr_reader namespace: Namespace
|
|
8
|
-
|
|
9
7
|
attr_reader block: ^(untyped) -> void
|
|
10
8
|
|
|
11
9
|
attr_reader each: ^(Context) -> untyped
|
|
@@ -28,11 +26,10 @@ module JobWorkflow
|
|
|
28
26
|
|
|
29
27
|
attr_reader dry_run_config: DryRunConfig
|
|
30
28
|
|
|
31
|
-
# rubocop:disable Metrics/ParameterLists, Metrics/MethodLength
|
|
29
|
+
# rubocop:disable Metrics/ParameterLists, Metrics/MethodLength
|
|
32
30
|
# : (
|
|
33
31
|
# job_name: String,
|
|
34
32
|
# name: Symbol,
|
|
35
|
-
# namespace: Namespace,
|
|
36
33
|
# block: ^(untyped) -> void,
|
|
37
34
|
# ?each: ^(Context) -> untyped,
|
|
38
35
|
# ?enqueue: true | false | ^(Context) -> bool | Hash[Symbol, untyped],
|
|
@@ -45,7 +42,7 @@ module JobWorkflow
|
|
|
45
42
|
# ?dependency_wait: Hash[Symbol, untyped],
|
|
46
43
|
# ?dry_run: bool | ^(Context) -> bool
|
|
47
44
|
# ) -> void
|
|
48
|
-
def initialize: (job_name: String, name: Symbol,
|
|
45
|
+
def initialize: (job_name: String, name: Symbol, block: ^(untyped) -> void, condition: ^(Context) -> bool, ?each: ^(Context) -> untyped, ?enqueue: true | false | ^(Context) -> bool | Hash[Symbol, untyped], ?output: Hash[Symbol, String], ?depends_on: Array[Symbol], ?task_retry: Integer | Hash[Symbol, untyped], ?throttle: Integer | Hash[Symbol, untyped], ?timeout: Numeric?, ?dependency_wait: Hash[Symbol, untyped], ?dry_run: bool | ^(Context) -> bool) -> void
|
|
49
46
|
|
|
50
47
|
# : () -> Symbol
|
|
51
48
|
def task_name: () -> Symbol
|
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
module JobWorkflow
|
|
4
4
|
class Workflow
|
|
5
|
-
attr_reader namespace: Namespace
|
|
6
|
-
|
|
7
5
|
attr_reader dry_run_config: DryRunConfig
|
|
8
6
|
|
|
9
7
|
# : () -> void
|
|
@@ -12,9 +10,6 @@ module JobWorkflow
|
|
|
12
10
|
# : (bool | ^(Context) -> bool) -> void
|
|
13
11
|
def dry_run_config=: (bool | ^(Context) -> bool) -> void
|
|
14
12
|
|
|
15
|
-
# : (Namespace) { () -> void } -> void
|
|
16
|
-
def add_namespace: (Namespace) { () -> void } -> void
|
|
17
|
-
|
|
18
13
|
# : (Task) -> void
|
|
19
14
|
def add_task: (Task) -> void
|
|
20
15
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: job-workflow
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- shoma07
|
|
@@ -49,7 +49,6 @@ files:
|
|
|
49
49
|
- guides/GETTING_STARTED.md
|
|
50
50
|
- guides/INSTRUMENTATION.md
|
|
51
51
|
- guides/LIFECYCLE_HOOKS.md
|
|
52
|
-
- guides/NAMESPACES.md
|
|
53
52
|
- guides/OPENTELEMETRY_INTEGRATION.md
|
|
54
53
|
- guides/PARALLEL_PROCESSING.md
|
|
55
54
|
- guides/PRODUCTION_DEPLOYMENT.md
|
|
@@ -84,7 +83,6 @@ files:
|
|
|
84
83
|
- lib/job_workflow/instrumentation/opentelemetry_subscriber.rb
|
|
85
84
|
- lib/job_workflow/job_status.rb
|
|
86
85
|
- lib/job_workflow/logger.rb
|
|
87
|
-
- lib/job_workflow/namespace.rb
|
|
88
86
|
- lib/job_workflow/output.rb
|
|
89
87
|
- lib/job_workflow/output_def.rb
|
|
90
88
|
- lib/job_workflow/queue.rb
|
|
@@ -136,7 +134,6 @@ files:
|
|
|
136
134
|
- sig/generated/job_workflow/instrumentation/opentelemetry_subscriber.rbs
|
|
137
135
|
- sig/generated/job_workflow/job_status.rbs
|
|
138
136
|
- sig/generated/job_workflow/logger.rbs
|
|
139
|
-
- sig/generated/job_workflow/namespace.rbs
|
|
140
137
|
- sig/generated/job_workflow/output.rbs
|
|
141
138
|
- sig/generated/job_workflow/output_def.rbs
|
|
142
139
|
- sig/generated/job_workflow/queue.rbs
|
data/guides/NAMESPACES.md
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
# Namespaces
|
|
2
|
-
|
|
3
|
-
Logically grouping tasks improves readability and maintainability of complex workflows. JobWorkflow provides namespace functionality.
|
|
4
|
-
|
|
5
|
-
## Basic Namespaces
|
|
6
|
-
|
|
7
|
-
### namespace DSL
|
|
8
|
-
|
|
9
|
-
Group related tasks.
|
|
10
|
-
|
|
11
|
-
```ruby
|
|
12
|
-
class ECommerceOrderJob < ApplicationJob
|
|
13
|
-
include JobWorkflow::DSL
|
|
14
|
-
|
|
15
|
-
argument :order, "Order"
|
|
16
|
-
|
|
17
|
-
# Payment-related tasks
|
|
18
|
-
namespace :payment do
|
|
19
|
-
task :validate do |ctx|
|
|
20
|
-
order = ctx.arguments.order
|
|
21
|
-
PaymentValidator.validate(order)
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
task :charge, depends_on: [:"payment:validate"], output: { payment_result: "Hash" } do |ctx|
|
|
25
|
-
order = ctx.arguments.order
|
|
26
|
-
{ payment_result: PaymentProcessor.charge(order) }
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
task :send_receipt, depends_on: [:"payment:charge"] do |ctx|
|
|
30
|
-
order = ctx.arguments.order
|
|
31
|
-
payment_result = ctx.output[:"payment:charge"].first.payment_result
|
|
32
|
-
ReceiptMailer.send(order, payment_result)
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
# Inventory-related tasks
|
|
37
|
-
namespace :inventory do
|
|
38
|
-
task :check_availability do |ctx|
|
|
39
|
-
order = ctx.arguments.order
|
|
40
|
-
InventoryService.check(order.items)
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
task :reserve, depends_on: [:"inventory:check_availability"], output: { reserved: "bool" } do |ctx|
|
|
44
|
-
order = ctx.arguments.order
|
|
45
|
-
{ reserved: InventoryService.reserve(order.items) }
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
# Shipping-related tasks
|
|
50
|
-
namespace :shipping do
|
|
51
|
-
task :calculate_cost, output: { shipping_cost: "Float" } do |ctx|
|
|
52
|
-
order = ctx.arguments.order
|
|
53
|
-
{ shipping_cost: ShippingCalculator.calculate(order) }
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
task :create_label, depends_on: [:"shipping:calculate_cost"], output: { shipping_label: "String" } do |ctx|
|
|
57
|
-
order = ctx.arguments.order
|
|
58
|
-
{ shipping_label: ShippingService.create_label(order) }
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
Tasks in namespaces are identified as `:namespace:task_name` at runtime:
|
|
65
|
-
|
|
66
|
-
```ruby
|
|
67
|
-
# Executed tasks:
|
|
68
|
-
# - :payment:validate
|
|
69
|
-
# - :payment:charge
|
|
70
|
-
# - :payment:send_receipt
|
|
71
|
-
# - :inventory:check_availability
|
|
72
|
-
# - :inventory:reserve
|
|
73
|
-
# - :shipping:calculate_cost
|
|
74
|
-
# - :shipping:create_label
|
|
75
|
-
```
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module JobWorkflow
|
|
4
|
-
class Namespace
|
|
5
|
-
attr_reader :name #: Symbol
|
|
6
|
-
attr_reader :parent #: Namespace?
|
|
7
|
-
|
|
8
|
-
class << self
|
|
9
|
-
#: () -> Namespace
|
|
10
|
-
def default
|
|
11
|
-
new(name: :"")
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
#: (name: Symbol, ?parent: Namespace?) -> void
|
|
16
|
-
def initialize(name:, parent: nil)
|
|
17
|
-
@name = name #: Symbol
|
|
18
|
-
@parent = parent #: Namespace?
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
#: () -> bool
|
|
22
|
-
def default?
|
|
23
|
-
name.empty?
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
#: (Namespace) -> Namespace
|
|
27
|
-
def update_parent(parent)
|
|
28
|
-
self.class.new(name:, parent:)
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
#: () -> Symbol
|
|
32
|
-
def full_name
|
|
33
|
-
[parent&.full_name, name.to_s].compact.reject(&:empty?).join(":").to_sym
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
end
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
# Generated from lib/job_workflow/namespace.rb with RBS::Inline
|
|
2
|
-
|
|
3
|
-
module JobWorkflow
|
|
4
|
-
class Namespace
|
|
5
|
-
attr_reader name: Symbol
|
|
6
|
-
|
|
7
|
-
attr_reader parent: Namespace?
|
|
8
|
-
|
|
9
|
-
# : () -> Namespace
|
|
10
|
-
def self.default: () -> Namespace
|
|
11
|
-
|
|
12
|
-
# : (name: Symbol, ?parent: Namespace?) -> void
|
|
13
|
-
def initialize: (name: Symbol, ?parent: Namespace?) -> void
|
|
14
|
-
|
|
15
|
-
# : () -> bool
|
|
16
|
-
def default?: () -> bool
|
|
17
|
-
|
|
18
|
-
# : (Namespace) -> Namespace
|
|
19
|
-
def update_parent: (Namespace) -> Namespace
|
|
20
|
-
|
|
21
|
-
# : () -> Symbol
|
|
22
|
-
def full_name: () -> Symbol
|
|
23
|
-
end
|
|
24
|
-
end
|