job-workflow 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +91 -0
- data/CHANGELOG.md +23 -0
- data/LICENSE.txt +21 -0
- data/README.md +47 -0
- data/Rakefile +55 -0
- data/Steepfile +10 -0
- data/guides/API_REFERENCE.md +112 -0
- data/guides/BEST_PRACTICES.md +113 -0
- data/guides/CACHE_STORE_INTEGRATION.md +145 -0
- data/guides/CONDITIONAL_EXECUTION.md +66 -0
- data/guides/DEPENDENCY_WAIT.md +386 -0
- data/guides/DRY_RUN.md +390 -0
- data/guides/DSL_BASICS.md +216 -0
- data/guides/ERROR_HANDLING.md +187 -0
- data/guides/GETTING_STARTED.md +524 -0
- data/guides/INSTRUMENTATION.md +131 -0
- data/guides/LIFECYCLE_HOOKS.md +415 -0
- data/guides/NAMESPACES.md +75 -0
- data/guides/OPENTELEMETRY_INTEGRATION.md +86 -0
- data/guides/PARALLEL_PROCESSING.md +302 -0
- data/guides/PRODUCTION_DEPLOYMENT.md +110 -0
- data/guides/QUEUE_MANAGEMENT.md +141 -0
- data/guides/README.md +174 -0
- data/guides/SCHEDULED_JOBS.md +165 -0
- data/guides/STRUCTURED_LOGGING.md +268 -0
- data/guides/TASK_OUTPUTS.md +240 -0
- data/guides/TESTING_STRATEGY.md +56 -0
- data/guides/THROTTLING.md +198 -0
- data/guides/TROUBLESHOOTING.md +53 -0
- data/guides/WORKFLOW_COMPOSITION.md +675 -0
- data/guides/WORKFLOW_STATUS_QUERY.md +288 -0
- data/lib/job-workflow.rb +3 -0
- data/lib/job_workflow/argument_def.rb +16 -0
- data/lib/job_workflow/arguments.rb +40 -0
- data/lib/job_workflow/auto_scaling/adapter/aws_adapter.rb +66 -0
- data/lib/job_workflow/auto_scaling/adapter.rb +31 -0
- data/lib/job_workflow/auto_scaling/configuration.rb +85 -0
- data/lib/job_workflow/auto_scaling/executor.rb +43 -0
- data/lib/job_workflow/auto_scaling.rb +69 -0
- data/lib/job_workflow/cache_store_adapters.rb +46 -0
- data/lib/job_workflow/context.rb +352 -0
- data/lib/job_workflow/dry_run_config.rb +31 -0
- data/lib/job_workflow/dsl.rb +236 -0
- data/lib/job_workflow/error_hook.rb +24 -0
- data/lib/job_workflow/hook.rb +24 -0
- data/lib/job_workflow/hook_registry.rb +66 -0
- data/lib/job_workflow/instrumentation/log_subscriber.rb +194 -0
- data/lib/job_workflow/instrumentation/opentelemetry_subscriber.rb +221 -0
- data/lib/job_workflow/instrumentation.rb +257 -0
- data/lib/job_workflow/job_status.rb +92 -0
- data/lib/job_workflow/logger.rb +86 -0
- data/lib/job_workflow/namespace.rb +36 -0
- data/lib/job_workflow/output.rb +81 -0
- data/lib/job_workflow/output_def.rb +14 -0
- data/lib/job_workflow/queue.rb +74 -0
- data/lib/job_workflow/queue_adapter.rb +38 -0
- data/lib/job_workflow/queue_adapters/abstract.rb +87 -0
- data/lib/job_workflow/queue_adapters/null_adapter.rb +127 -0
- data/lib/job_workflow/queue_adapters/solid_queue_adapter.rb +224 -0
- data/lib/job_workflow/runner.rb +173 -0
- data/lib/job_workflow/schedule.rb +46 -0
- data/lib/job_workflow/semaphore.rb +71 -0
- data/lib/job_workflow/task.rb +83 -0
- data/lib/job_workflow/task_callable.rb +43 -0
- data/lib/job_workflow/task_context.rb +70 -0
- data/lib/job_workflow/task_dependency_wait.rb +66 -0
- data/lib/job_workflow/task_enqueue.rb +50 -0
- data/lib/job_workflow/task_graph.rb +43 -0
- data/lib/job_workflow/task_job_status.rb +70 -0
- data/lib/job_workflow/task_output.rb +51 -0
- data/lib/job_workflow/task_retry.rb +64 -0
- data/lib/job_workflow/task_throttle.rb +46 -0
- data/lib/job_workflow/version.rb +5 -0
- data/lib/job_workflow/workflow.rb +87 -0
- data/lib/job_workflow/workflow_status.rb +112 -0
- data/lib/job_workflow.rb +59 -0
- data/rbs_collection.lock.yaml +172 -0
- data/rbs_collection.yaml +14 -0
- data/sig/generated/job-workflow.rbs +2 -0
- data/sig/generated/job_workflow/argument_def.rbs +14 -0
- data/sig/generated/job_workflow/arguments.rbs +26 -0
- data/sig/generated/job_workflow/auto_scaling/adapter/aws_adapter.rbs +32 -0
- data/sig/generated/job_workflow/auto_scaling/adapter.rbs +22 -0
- data/sig/generated/job_workflow/auto_scaling/configuration.rbs +50 -0
- data/sig/generated/job_workflow/auto_scaling/executor.rbs +29 -0
- data/sig/generated/job_workflow/auto_scaling.rbs +47 -0
- data/sig/generated/job_workflow/cache_store_adapters.rbs +28 -0
- data/sig/generated/job_workflow/context.rbs +155 -0
- data/sig/generated/job_workflow/dry_run_config.rbs +16 -0
- data/sig/generated/job_workflow/dsl.rbs +117 -0
- data/sig/generated/job_workflow/error_hook.rbs +18 -0
- data/sig/generated/job_workflow/hook.rbs +18 -0
- data/sig/generated/job_workflow/hook_registry.rbs +47 -0
- data/sig/generated/job_workflow/instrumentation/log_subscriber.rbs +102 -0
- data/sig/generated/job_workflow/instrumentation/opentelemetry_subscriber.rbs +113 -0
- data/sig/generated/job_workflow/instrumentation.rbs +138 -0
- data/sig/generated/job_workflow/job_status.rbs +46 -0
- data/sig/generated/job_workflow/logger.rbs +56 -0
- data/sig/generated/job_workflow/namespace.rbs +24 -0
- data/sig/generated/job_workflow/output.rbs +39 -0
- data/sig/generated/job_workflow/output_def.rbs +12 -0
- data/sig/generated/job_workflow/queue.rbs +49 -0
- data/sig/generated/job_workflow/queue_adapter.rbs +18 -0
- data/sig/generated/job_workflow/queue_adapters/abstract.rbs +56 -0
- data/sig/generated/job_workflow/queue_adapters/null_adapter.rbs +73 -0
- data/sig/generated/job_workflow/queue_adapters/solid_queue_adapter.rbs +111 -0
- data/sig/generated/job_workflow/runner.rbs +66 -0
- data/sig/generated/job_workflow/schedule.rbs +34 -0
- data/sig/generated/job_workflow/semaphore.rbs +37 -0
- data/sig/generated/job_workflow/task.rbs +60 -0
- data/sig/generated/job_workflow/task_callable.rbs +30 -0
- data/sig/generated/job_workflow/task_context.rbs +52 -0
- data/sig/generated/job_workflow/task_dependency_wait.rbs +42 -0
- data/sig/generated/job_workflow/task_enqueue.rbs +27 -0
- data/sig/generated/job_workflow/task_graph.rbs +27 -0
- data/sig/generated/job_workflow/task_job_status.rbs +42 -0
- data/sig/generated/job_workflow/task_output.rbs +29 -0
- data/sig/generated/job_workflow/task_retry.rbs +30 -0
- data/sig/generated/job_workflow/task_throttle.rbs +20 -0
- data/sig/generated/job_workflow/version.rbs +5 -0
- data/sig/generated/job_workflow/workflow.rbs +48 -0
- data/sig/generated/job_workflow/workflow_status.rbs +55 -0
- data/sig/generated/job_workflow.rbs +8 -0
- data/sig-private/activejob.rbs +35 -0
- data/sig-private/activesupport.rbs +23 -0
- data/sig-private/aws.rbs +32 -0
- data/sig-private/opentelemetry.rbs +40 -0
- data/sig-private/solid_queue.rbs +108 -0
- data/tmp/.keep +0 -0
- metadata +190 -0
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
# Workflow Composition
|
|
2
|
+
|
|
3
|
+
JobWorkflow allows you to invoke existing workflow jobs from other workflows, enabling you to modularize large workflows and reuse common processing. This guide explains workflow composition patterns, their benefits, and important considerations.
|
|
4
|
+
|
|
5
|
+
## Core Concepts
|
|
6
|
+
|
|
7
|
+
Workflow composition is effective in the following scenarios:
|
|
8
|
+
|
|
9
|
+
- **Modularizing large workflows**: Breaking down complex workflows into smaller, maintainable units
|
|
10
|
+
- **Reusing common processes**: Defining shared processing logic as independent workflows
|
|
11
|
+
- **Separation of concerns**: Ensuring each workflow has a well-defined responsibility
|
|
12
|
+
|
|
13
|
+
## Basic Workflow Invocation
|
|
14
|
+
|
|
15
|
+
You can invoke other workflow jobs from within a regular task.
|
|
16
|
+
|
|
17
|
+
### Synchronous Execution Pattern
|
|
18
|
+
|
|
19
|
+
Use `perform_now` to execute a child workflow synchronously and obtain its results:
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
class UserRegistrationJob < ApplicationJob
|
|
23
|
+
include JobWorkflow::DSL
|
|
24
|
+
|
|
25
|
+
argument :user_id, "Integer"
|
|
26
|
+
argument :email, "String"
|
|
27
|
+
|
|
28
|
+
task :register_user do |ctx|
|
|
29
|
+
# User registration logic
|
|
30
|
+
User.create!(id: ctx.arguments.user_id, email: ctx.arguments.email)
|
|
31
|
+
puts "User registered: #{ctx.arguments.email}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
task :send_welcome_email, depends_on: [:register_user] do |ctx|
|
|
35
|
+
# Email sending logic
|
|
36
|
+
UserMailer.welcome_email(ctx.arguments.email).deliver_now
|
|
37
|
+
puts "Welcome email sent"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
class OnboardingWorkflowJob < ApplicationJob
|
|
42
|
+
include JobWorkflow::DSL
|
|
43
|
+
|
|
44
|
+
argument :user_id, "Integer"
|
|
45
|
+
argument :email, "String"
|
|
46
|
+
|
|
47
|
+
# Invoke child workflow
|
|
48
|
+
task :run_registration do |ctx|
|
|
49
|
+
UserRegistrationJob.perform_now(
|
|
50
|
+
user_id: ctx.arguments.user_id,
|
|
51
|
+
email: ctx.arguments.email
|
|
52
|
+
)
|
|
53
|
+
puts "Registration workflow completed"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
task :setup_preferences, depends_on: [:run_registration] do |ctx|
|
|
57
|
+
# Post-registration setup
|
|
58
|
+
UserPreference.create!(user_id: ctx.arguments.user_id)
|
|
59
|
+
puts "User preferences initialized"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Asynchronous Execution Pattern
|
|
65
|
+
|
|
66
|
+
Use `perform_later` to execute a child workflow asynchronously. The parent workflow continues without waiting for the child to complete:
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
class NotificationWorkflowJob < ApplicationJob
|
|
70
|
+
include JobWorkflow::DSL
|
|
71
|
+
|
|
72
|
+
argument :user_id, "Integer"
|
|
73
|
+
|
|
74
|
+
task :send_notifications do |ctx|
|
|
75
|
+
# Fire-and-forget: execute notification workflows asynchronously
|
|
76
|
+
EmailNotificationJob.perform_later(user_id: ctx.arguments.user_id)
|
|
77
|
+
PushNotificationJob.perform_later(user_id: ctx.arguments.user_id)
|
|
78
|
+
puts "Notifications scheduled"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
task :update_status, depends_on: [:send_notifications] do |ctx|
|
|
82
|
+
# Proceed without waiting for notifications to complete
|
|
83
|
+
User.find(ctx.arguments.user_id).update!(status: "notified")
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Note**: With asynchronous execution, the parent workflow does not wait for the child workflow's results or completion. If you need to wait for completion, use synchronous execution (`perform_now`).
|
|
89
|
+
|
|
90
|
+
## Utilizing Child Workflow Outputs
|
|
91
|
+
|
|
92
|
+
You can retrieve and use the execution results from child workflows in the parent workflow.
|
|
93
|
+
|
|
94
|
+
### Accessing Task Outputs
|
|
95
|
+
|
|
96
|
+
Retrieve outputs defined in the child workflow:
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
class DataFetchJob < ApplicationJob
|
|
100
|
+
include JobWorkflow::DSL
|
|
101
|
+
|
|
102
|
+
argument :source, "String"
|
|
103
|
+
|
|
104
|
+
task :fetch_data, output: { records: "Array", count: "Integer" } do |ctx|
|
|
105
|
+
data = ExternalAPI.fetch(ctx.arguments.source)
|
|
106
|
+
{
|
|
107
|
+
records: data,
|
|
108
|
+
count: data.size
|
|
109
|
+
}
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
class DataProcessingJob < ApplicationJob
|
|
114
|
+
include JobWorkflow::DSL
|
|
115
|
+
|
|
116
|
+
argument :source, "String"
|
|
117
|
+
|
|
118
|
+
task :invoke_fetch, output: { fetched_count: "Integer" } do |ctx|
|
|
119
|
+
# Execute child workflow and retrieve its output
|
|
120
|
+
result = DataFetchJob.perform_now(source: ctx.arguments.source)
|
|
121
|
+
|
|
122
|
+
# Access child workflow output
|
|
123
|
+
fetch_output = result.output[:fetch_data].first
|
|
124
|
+
|
|
125
|
+
puts "Fetched #{fetch_output.count} records"
|
|
126
|
+
|
|
127
|
+
# Return as parent workflow output
|
|
128
|
+
{
|
|
129
|
+
fetched_count: fetch_output.count
|
|
130
|
+
}
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
task :process_data, depends_on: [:invoke_fetch] do |ctx|
|
|
134
|
+
count = ctx.output[:invoke_fetch].first.fetched_count
|
|
135
|
+
puts "Processing #{count} records..."
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Handling Complex Outputs
|
|
141
|
+
|
|
142
|
+
Retrieve multiple task outputs from a child workflow:
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
class ReportGenerationJob < ApplicationJob
|
|
146
|
+
include JobWorkflow::DSL
|
|
147
|
+
|
|
148
|
+
argument :user_id, "Integer"
|
|
149
|
+
|
|
150
|
+
task :fetch_user_data, output: { name: "String", email: "String" } do |ctx|
|
|
151
|
+
user = User.find(ctx.arguments.user_id)
|
|
152
|
+
{ name: user.name, email: user.email }
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
task :fetch_activity, depends_on: [:fetch_user_data], output: { activity_count: "Integer" } do |ctx|
|
|
156
|
+
count = Activity.where(user_id: ctx.arguments.user_id).count
|
|
157
|
+
{ activity_count: count }
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
class DashboardJob < ApplicationJob
|
|
162
|
+
include JobWorkflow::DSL
|
|
163
|
+
|
|
164
|
+
argument :user_id, "Integer"
|
|
165
|
+
|
|
166
|
+
task :generate_report, output: { report: "Hash" } do |ctx|
|
|
167
|
+
# Execute child workflow
|
|
168
|
+
result = ReportGenerationJob.perform_now(user_id: ctx.arguments.user_id)
|
|
169
|
+
|
|
170
|
+
# Retrieve multiple task outputs
|
|
171
|
+
user_data = result.output[:fetch_user_data].first
|
|
172
|
+
activity_data = result.output[:fetch_activity].first
|
|
173
|
+
|
|
174
|
+
report = {
|
|
175
|
+
user: {
|
|
176
|
+
name: user_data.name,
|
|
177
|
+
email: user_data.email
|
|
178
|
+
},
|
|
179
|
+
stats: {
|
|
180
|
+
activities: activity_data.activity_count
|
|
181
|
+
},
|
|
182
|
+
generated_at: Time.current
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
{ report: report }
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
task :display, depends_on: [:generate_report] do |ctx|
|
|
189
|
+
report = ctx.output[:generate_report].first.report
|
|
190
|
+
puts "Report for #{report[:user][:name]}: #{report[:stats][:activities]} activities"
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Executing Child Workflows with Map Tasks
|
|
196
|
+
|
|
197
|
+
You can parallelize child workflow execution across multiple items and collect results:
|
|
198
|
+
|
|
199
|
+
```ruby
|
|
200
|
+
class SingleItemProcessingJob < ApplicationJob
|
|
201
|
+
include JobWorkflow::DSL
|
|
202
|
+
|
|
203
|
+
argument :item_id, "Integer"
|
|
204
|
+
|
|
205
|
+
task :process, output: { status: "String", result: "String" } do |ctx|
|
|
206
|
+
item = Item.find(ctx.arguments.item_id)
|
|
207
|
+
result = process_item(item)
|
|
208
|
+
|
|
209
|
+
{
|
|
210
|
+
status: "success",
|
|
211
|
+
result: result
|
|
212
|
+
}
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
class BatchProcessingJob < ApplicationJob
|
|
217
|
+
include JobWorkflow::DSL
|
|
218
|
+
|
|
219
|
+
argument :item_ids, "Array[Integer]"
|
|
220
|
+
|
|
221
|
+
# Execute child workflow for each item
|
|
222
|
+
task :process_items,
|
|
223
|
+
each: ->(ctx) { ctx.arguments.item_ids },
|
|
224
|
+
output: { item_id: "Integer", status: "String" } do |ctx|
|
|
225
|
+
item_id = ctx.each_value
|
|
226
|
+
|
|
227
|
+
# Execute child workflow
|
|
228
|
+
result = SingleItemProcessingJob.perform_now(item_id: item_id)
|
|
229
|
+
|
|
230
|
+
# Collect outputs
|
|
231
|
+
process_output = result.output[:process].first
|
|
232
|
+
|
|
233
|
+
{
|
|
234
|
+
item_id: item_id,
|
|
235
|
+
status: process_output.status
|
|
236
|
+
}
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
task :summarize,
|
|
240
|
+
depends_on: [:process_items] do |ctx|
|
|
241
|
+
outputs = ctx.output[:process_items]
|
|
242
|
+
successful = outputs.count { |o| o.status == "success" }
|
|
243
|
+
|
|
244
|
+
puts "Processed #{outputs.size} items, #{successful} successful"
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Execution example
|
|
249
|
+
BatchProcessingJob.perform_now(item_ids: [1, 2, 3, 4, 5])
|
|
250
|
+
# Output:
|
|
251
|
+
# Processed 5 items, 5 successful
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## Building Arguments Dynamically
|
|
255
|
+
|
|
256
|
+
Construct arguments for child workflows based on the parent workflow's state:
|
|
257
|
+
|
|
258
|
+
```ruby
|
|
259
|
+
class UserDataExportJob < ApplicationJob
|
|
260
|
+
include JobWorkflow::DSL
|
|
261
|
+
|
|
262
|
+
argument :user_id, "Integer"
|
|
263
|
+
argument :format, "String"
|
|
264
|
+
|
|
265
|
+
task :export, output: { file_path: "String", size: "Integer" } do |ctx|
|
|
266
|
+
# Data export logic
|
|
267
|
+
file = export_user_data(ctx.arguments.user_id, ctx.arguments.format)
|
|
268
|
+
{
|
|
269
|
+
file_path: file.path,
|
|
270
|
+
size: file.size
|
|
271
|
+
}
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
class MonthlyReportJob < ApplicationJob
|
|
276
|
+
include JobWorkflow::DSL
|
|
277
|
+
|
|
278
|
+
argument :month, "String"
|
|
279
|
+
|
|
280
|
+
task :fetch_users, output: { user_ids: "Array[Integer]" } do |ctx|
|
|
281
|
+
users = User.where("created_at >= ?", Date.parse(ctx.arguments.month).beginning_of_month)
|
|
282
|
+
{ user_ids: users.pluck(:id) }
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
task :export_user_reports,
|
|
286
|
+
depends_on: [:fetch_users],
|
|
287
|
+
each: ->(ctx) { ctx.output[:fetch_users].first.user_ids },
|
|
288
|
+
output: { exported_file: "String" } do |ctx|
|
|
289
|
+
user_id = ctx.each_value
|
|
290
|
+
|
|
291
|
+
# Execute child workflow for each user
|
|
292
|
+
result = UserDataExportJob.perform_now(
|
|
293
|
+
user_id: user_id,
|
|
294
|
+
format: "csv" # Format determined by parent workflow
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
export_output = result.output[:export].first
|
|
298
|
+
|
|
299
|
+
{
|
|
300
|
+
exported_file: export_output.file_path
|
|
301
|
+
}
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
task :archive_reports, depends_on: [:export_user_reports] do |ctx|
|
|
305
|
+
files = ctx.output[:export_user_reports].map(&:exported_file)
|
|
306
|
+
puts "Archiving #{files.size} report files..."
|
|
307
|
+
# Archive logic
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
## Error Handling
|
|
313
|
+
|
|
314
|
+
How to handle errors that occur in child workflows:
|
|
315
|
+
|
|
316
|
+
### Basic Error Handling
|
|
317
|
+
|
|
318
|
+
```ruby
|
|
319
|
+
class RiskySubWorkflowJob < ApplicationJob
|
|
320
|
+
include JobWorkflow::DSL
|
|
321
|
+
|
|
322
|
+
argument :data, "String"
|
|
323
|
+
|
|
324
|
+
task :risky_operation do |ctx|
|
|
325
|
+
# Operation that may fail
|
|
326
|
+
raise "Processing failed" if ctx.arguments.data == "bad"
|
|
327
|
+
puts "Processing succeeded"
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
class ParentWorkflowJob < ApplicationJob
|
|
332
|
+
include JobWorkflow::DSL
|
|
333
|
+
|
|
334
|
+
argument :data, "String"
|
|
335
|
+
|
|
336
|
+
task :invoke_child do |ctx|
|
|
337
|
+
begin
|
|
338
|
+
RiskySubWorkflowJob.perform_now(data: ctx.arguments.data)
|
|
339
|
+
puts "Child workflow succeeded"
|
|
340
|
+
rescue StandardError => e
|
|
341
|
+
puts "Child workflow failed: #{e.message}"
|
|
342
|
+
# Fallback logic
|
|
343
|
+
puts "Executing fallback logic"
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
task :continue, depends_on: [:invoke_child] do |ctx|
|
|
348
|
+
puts "Parent workflow continues"
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### Error Handling with Retries
|
|
354
|
+
|
|
355
|
+
When a child workflow has retry configuration, the parent workflow waits for retry completion:
|
|
356
|
+
|
|
357
|
+
```ruby
|
|
358
|
+
class RetryableSubWorkflowJob < ApplicationJob
|
|
359
|
+
include JobWorkflow::DSL
|
|
360
|
+
|
|
361
|
+
argument :attempt_id, "Integer"
|
|
362
|
+
|
|
363
|
+
task :operation, retry: { max_retries: 3, wait: 5 } do |ctx|
|
|
364
|
+
# Operation that can be retried
|
|
365
|
+
success = perform_operation(ctx.arguments.attempt_id)
|
|
366
|
+
raise "Operation failed" unless success
|
|
367
|
+
puts "Operation succeeded"
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
class CoordinatorJob < ApplicationJob
|
|
372
|
+
include JobWorkflow::DSL
|
|
373
|
+
|
|
374
|
+
argument :attempt_id, "Integer"
|
|
375
|
+
|
|
376
|
+
task :coordinate do |ctx|
|
|
377
|
+
# Wait for full execution including retries
|
|
378
|
+
RetryableSubWorkflowJob.perform_now(attempt_id: ctx.arguments.attempt_id)
|
|
379
|
+
puts "Retryable sub-workflow completed (with retries if needed)"
|
|
380
|
+
end
|
|
381
|
+
end
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
## Best Practices
|
|
385
|
+
|
|
386
|
+
### 1. Divide Workflows at Appropriate Granularity
|
|
387
|
+
|
|
388
|
+
Follow the single responsibility principle when dividing workflows:
|
|
389
|
+
|
|
390
|
+
```ruby
|
|
391
|
+
# ✅ Good example: Clear responsibilities
|
|
392
|
+
class UserCreationJob < ApplicationJob
|
|
393
|
+
include JobWorkflow::DSL
|
|
394
|
+
# Focus on user creation only
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
class NotificationJob < ApplicationJob
|
|
398
|
+
include JobWorkflow::DSL
|
|
399
|
+
# Focus on sending notifications only
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
class OnboardingJob < ApplicationJob
|
|
403
|
+
include JobWorkflow::DSL
|
|
404
|
+
# Combine them
|
|
405
|
+
task :create_user do |ctx|
|
|
406
|
+
UserCreationJob.perform_now(...)
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
task :notify, depends_on: [:create_user] do |ctx|
|
|
410
|
+
NotificationJob.perform_now(...)
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
### 2. Define Output Interfaces Clearly
|
|
416
|
+
|
|
417
|
+
Explicitly define and document child workflow outputs:
|
|
418
|
+
|
|
419
|
+
```ruby
|
|
420
|
+
class DataFetchJob < ApplicationJob
|
|
421
|
+
include JobWorkflow::DSL
|
|
422
|
+
|
|
423
|
+
# Define outputs clearly
|
|
424
|
+
task :fetch,
|
|
425
|
+
output: {
|
|
426
|
+
records: "Array", # Retrieved records
|
|
427
|
+
count: "Integer", # Record count
|
|
428
|
+
timestamp: "Time" # Fetch timestamp
|
|
429
|
+
} do |ctx|
|
|
430
|
+
# ...
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
### 3. Avoid Deep Nesting
|
|
436
|
+
|
|
437
|
+
Deep nesting of workflow invocations makes debugging difficult:
|
|
438
|
+
|
|
439
|
+
```ruby
|
|
440
|
+
# ❌ Bad example: Deep nesting
|
|
441
|
+
class LevelThreeJob < ApplicationJob
|
|
442
|
+
include JobWorkflow::DSL
|
|
443
|
+
task :do_something do; end
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
class LevelTwoJob < ApplicationJob
|
|
447
|
+
include JobWorkflow::DSL
|
|
448
|
+
task :call_three do
|
|
449
|
+
LevelThreeJob.perform_now
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
class LevelOneJob < ApplicationJob
|
|
454
|
+
include JobWorkflow::DSL
|
|
455
|
+
task :call_two do
|
|
456
|
+
LevelTwoJob.perform_now # Three levels is too complex
|
|
457
|
+
end
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
# ✅ Good example: Flat structure
|
|
461
|
+
class CoordinatorJob < ApplicationJob
|
|
462
|
+
include JobWorkflow::DSL
|
|
463
|
+
|
|
464
|
+
task :step_one do
|
|
465
|
+
StepOneJob.perform_now
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
task :step_two, depends_on: [:step_one] do
|
|
469
|
+
StepTwoJob.perform_now
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
task :step_three, depends_on: [:step_two] do
|
|
473
|
+
StepThreeJob.perform_now
|
|
474
|
+
end
|
|
475
|
+
end
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
### 4. Maintain Idempotency
|
|
479
|
+
|
|
480
|
+
Design child workflows to be idempotent, supporting retries and re-execution:
|
|
481
|
+
|
|
482
|
+
```ruby
|
|
483
|
+
class IdempotentSubWorkflowJob < ApplicationJob
|
|
484
|
+
include JobWorkflow::DSL
|
|
485
|
+
|
|
486
|
+
argument :order_id, "Integer"
|
|
487
|
+
|
|
488
|
+
task :process_order do |ctx|
|
|
489
|
+
order = Order.find(ctx.arguments.order_id)
|
|
490
|
+
|
|
491
|
+
# Skip if already processed
|
|
492
|
+
return if order.processed?
|
|
493
|
+
|
|
494
|
+
# Execute processing
|
|
495
|
+
order.process!
|
|
496
|
+
puts "Order #{order.id} processed"
|
|
497
|
+
end
|
|
498
|
+
end
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
### 5. Use Appropriate Queues
|
|
502
|
+
|
|
503
|
+
Use different queues for parent and child workflows with different priority or resource requirements:
|
|
504
|
+
|
|
505
|
+
```ruby
|
|
506
|
+
class HighPriorityParentJob < ApplicationJob
|
|
507
|
+
include JobWorkflow::DSL
|
|
508
|
+
|
|
509
|
+
queue "urgent"
|
|
510
|
+
|
|
511
|
+
task :urgent_task do |ctx|
|
|
512
|
+
# High priority task
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
task :delegate_to_background do |ctx|
|
|
516
|
+
# Child workflow uses a different queue
|
|
517
|
+
BackgroundProcessingJob.set(queue: "background").perform_now(...)
|
|
518
|
+
end
|
|
519
|
+
end
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
## Limitations and Important Considerations
|
|
523
|
+
|
|
524
|
+
### 1. Serialization Limits on Outputs
|
|
525
|
+
|
|
526
|
+
Child workflow outputs are serialized and stored in the parent workflow's Context. Be cautious when passing large data:
|
|
527
|
+
|
|
528
|
+
```ruby
|
|
529
|
+
# ❌ Bad example: Passing large data directly
|
|
530
|
+
task :fetch_large_data, output: { data: "Array" } do |ctx|
|
|
531
|
+
{
|
|
532
|
+
data: LargeDataSet.all.to_a # Serializing thousands of records
|
|
533
|
+
}
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
# ✅ Good example: Return only essential information or use external storage
|
|
537
|
+
task :fetch_large_data, output: { file_path: "String", count: "Integer" } do |ctx|
|
|
538
|
+
records = LargeDataSet.all
|
|
539
|
+
file_path = write_to_temp_file(records)
|
|
540
|
+
|
|
541
|
+
{
|
|
542
|
+
file_path: file_path,
|
|
543
|
+
count: records.size
|
|
544
|
+
}
|
|
545
|
+
end
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
### 2. Timeouts in Synchronous Execution
|
|
549
|
+
|
|
550
|
+
With `perform_now`, the parent workflow is blocked until the child workflow completes. Consider timeout configuration for long-running child workflows:
|
|
551
|
+
|
|
552
|
+
```ruby
|
|
553
|
+
task :invoke_long_running do |ctx|
|
|
554
|
+
# Set timeout for child workflow
|
|
555
|
+
Timeout.timeout(300) do # 5 minutes timeout
|
|
556
|
+
LongRunningJob.perform_now(...)
|
|
557
|
+
end
|
|
558
|
+
rescue Timeout::Error
|
|
559
|
+
puts "Child workflow timed out"
|
|
560
|
+
# Timeout handling
|
|
561
|
+
end
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
### 3. Invoking Workflows with Dependency Wait (Critical Limitation)
|
|
565
|
+
|
|
566
|
+
**⚠️ Critical Current Limitation**: If a child workflow uses Dependency Wait (automatic rescheduling when waiting for dependent tasks), calling it with `perform_now` does **not guarantee** the parent workflow waits for the child's **complete** completion.
|
|
567
|
+
|
|
568
|
+
This is a current implementation limitation. When the child workflow is rescheduled, the `perform_now` call returns, and the parent workflow may proceed before the child finishes.
|
|
569
|
+
|
|
570
|
+
```ruby
|
|
571
|
+
class ChildWithDependencyWaitJob < ApplicationJob
|
|
572
|
+
include JobWorkflow::DSL
|
|
573
|
+
|
|
574
|
+
# Dependency Wait is enabled (default: enable_dependency_wait: true)
|
|
575
|
+
argument :data, "String"
|
|
576
|
+
|
|
577
|
+
task :slow_task do |ctx|
|
|
578
|
+
sleep 10
|
|
579
|
+
puts "Slow task completed"
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
task :dependent_task, depends_on: [:slow_task] do |ctx|
|
|
583
|
+
# May be rescheduled while waiting for slow_task
|
|
584
|
+
puts "Dependent task executed"
|
|
585
|
+
end
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
class ParentWorkflowJob < ApplicationJob
|
|
589
|
+
include JobWorkflow::DSL
|
|
590
|
+
|
|
591
|
+
argument :data, "String"
|
|
592
|
+
|
|
593
|
+
task :invoke_child do |ctx|
|
|
594
|
+
# ⚠️ Warning: If child workflow is rescheduled,
|
|
595
|
+
# control returns here (does not wait for full completion)
|
|
596
|
+
ChildWithDependencyWaitJob.perform_now(data: ctx.arguments.data)
|
|
597
|
+
puts "Child workflow invocation returned"
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
task :next_task, depends_on: [:invoke_child] do |ctx|
|
|
601
|
+
# ⚠️ Child workflow may not be fully completed at this point
|
|
602
|
+
puts "Next task in parent"
|
|
603
|
+
end
|
|
604
|
+
end
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
**Workarounds**:
|
|
608
|
+
|
|
609
|
+
If you need to guarantee complete child workflow completion, consider these approaches:
|
|
610
|
+
|
|
611
|
+
1. **Disable Dependency Wait in the child workflow** (if child completes quickly):
|
|
612
|
+
```ruby
|
|
613
|
+
class ChildWorkflowJob < ApplicationJob
|
|
614
|
+
include JobWorkflow::DSL
|
|
615
|
+
|
|
616
|
+
# Explicitly disable Dependency Wait
|
|
617
|
+
enable_dependency_wait false
|
|
618
|
+
|
|
619
|
+
# Task definitions...
|
|
620
|
+
end
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
2. **Poll for completion** (for long-running child workflows):
|
|
624
|
+
```ruby
|
|
625
|
+
task :invoke_and_wait do |ctx|
|
|
626
|
+
job = ChildWithDependencyWaitJob.perform_now(data: ctx.arguments.data)
|
|
627
|
+
|
|
628
|
+
# Check completion using job ID
|
|
629
|
+
loop do
|
|
630
|
+
status = JobWorkflow::WorkflowStatus.find_by_job_id(job.job_id)
|
|
631
|
+
break if status.all_completed?
|
|
632
|
+
|
|
633
|
+
sleep 5 # Polling interval
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
puts "Child workflow fully completed"
|
|
637
|
+
end
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
3. **Redesign the child workflow** to be smaller and not require Dependency Wait
|
|
641
|
+
|
|
642
|
+
**Note**: This limitation will be improved in future versions (see below).
|
|
643
|
+
|
|
644
|
+
### 4. Dependency Handling with Asynchronous Execution
|
|
645
|
+
|
|
646
|
+
When using `perform_later` for asynchronous child workflow execution, the parent workflow does not wait for the child to complete. If the next task depends on the child workflow's results, always use `perform_now`.
|
|
647
|
+
|
|
648
|
+
## Summary
|
|
649
|
+
|
|
650
|
+
Workflow composition enables you to modularize complex business logic and manage reusable components effectively.
|
|
651
|
+
|
|
652
|
+
**Key Points**:
|
|
653
|
+
|
|
654
|
+
- Synchronous execution (`perform_now`) allows retrieval of child workflow outputs
|
|
655
|
+
- Asynchronous execution (`perform_later`) is fire-and-forget without waiting for results
|
|
656
|
+
- Map Tasks enable parallel execution of multiple child workflows with result collection
|
|
657
|
+
- Define output interfaces clearly and divide workflows at appropriate granularity
|
|
658
|
+
- Consider idempotency and error handling in your design
|
|
659
|
+
- **Important**: With current implementation, `perform_now` does not guarantee waiting for child workflow completion if Dependency Wait causes rescheduling
|
|
660
|
+
|
|
661
|
+
By leveraging these patterns, you can build maintainable and highly reusable workflow systems.
|
|
662
|
+
|
|
663
|
+
## Future Improvements
|
|
664
|
+
|
|
665
|
+
### Full Support for Synchronous Execution with Dependency Wait
|
|
666
|
+
|
|
667
|
+
Currently, when invoking a child workflow with `perform_now`, if the child workflow reschedules due to Dependency Wait, the parent workflow does not wait for **complete** completion. This is a current implementation limitation.
|
|
668
|
+
|
|
669
|
+
This issue will be addressed in future versions. Planned improvements include:
|
|
670
|
+
|
|
671
|
+
- **Child workflow completion tracking**: Parent workflow tracks child job IDs and waits for complete completion
|
|
672
|
+
- **Continuable workflow invocation**: Parent workflow properly continues after child rescheduling
|
|
673
|
+
- **Explicit wait option**: Options like `wait_for_completion: true` to guarantee complete completion
|
|
674
|
+
|
|
675
|
+
Check the GitHub repository issues for progress updates.
|