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.
Files changed (132) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +91 -0
  4. data/CHANGELOG.md +23 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +47 -0
  7. data/Rakefile +55 -0
  8. data/Steepfile +10 -0
  9. data/guides/API_REFERENCE.md +112 -0
  10. data/guides/BEST_PRACTICES.md +113 -0
  11. data/guides/CACHE_STORE_INTEGRATION.md +145 -0
  12. data/guides/CONDITIONAL_EXECUTION.md +66 -0
  13. data/guides/DEPENDENCY_WAIT.md +386 -0
  14. data/guides/DRY_RUN.md +390 -0
  15. data/guides/DSL_BASICS.md +216 -0
  16. data/guides/ERROR_HANDLING.md +187 -0
  17. data/guides/GETTING_STARTED.md +524 -0
  18. data/guides/INSTRUMENTATION.md +131 -0
  19. data/guides/LIFECYCLE_HOOKS.md +415 -0
  20. data/guides/NAMESPACES.md +75 -0
  21. data/guides/OPENTELEMETRY_INTEGRATION.md +86 -0
  22. data/guides/PARALLEL_PROCESSING.md +302 -0
  23. data/guides/PRODUCTION_DEPLOYMENT.md +110 -0
  24. data/guides/QUEUE_MANAGEMENT.md +141 -0
  25. data/guides/README.md +174 -0
  26. data/guides/SCHEDULED_JOBS.md +165 -0
  27. data/guides/STRUCTURED_LOGGING.md +268 -0
  28. data/guides/TASK_OUTPUTS.md +240 -0
  29. data/guides/TESTING_STRATEGY.md +56 -0
  30. data/guides/THROTTLING.md +198 -0
  31. data/guides/TROUBLESHOOTING.md +53 -0
  32. data/guides/WORKFLOW_COMPOSITION.md +675 -0
  33. data/guides/WORKFLOW_STATUS_QUERY.md +288 -0
  34. data/lib/job-workflow.rb +3 -0
  35. data/lib/job_workflow/argument_def.rb +16 -0
  36. data/lib/job_workflow/arguments.rb +40 -0
  37. data/lib/job_workflow/auto_scaling/adapter/aws_adapter.rb +66 -0
  38. data/lib/job_workflow/auto_scaling/adapter.rb +31 -0
  39. data/lib/job_workflow/auto_scaling/configuration.rb +85 -0
  40. data/lib/job_workflow/auto_scaling/executor.rb +43 -0
  41. data/lib/job_workflow/auto_scaling.rb +69 -0
  42. data/lib/job_workflow/cache_store_adapters.rb +46 -0
  43. data/lib/job_workflow/context.rb +352 -0
  44. data/lib/job_workflow/dry_run_config.rb +31 -0
  45. data/lib/job_workflow/dsl.rb +236 -0
  46. data/lib/job_workflow/error_hook.rb +24 -0
  47. data/lib/job_workflow/hook.rb +24 -0
  48. data/lib/job_workflow/hook_registry.rb +66 -0
  49. data/lib/job_workflow/instrumentation/log_subscriber.rb +194 -0
  50. data/lib/job_workflow/instrumentation/opentelemetry_subscriber.rb +221 -0
  51. data/lib/job_workflow/instrumentation.rb +257 -0
  52. data/lib/job_workflow/job_status.rb +92 -0
  53. data/lib/job_workflow/logger.rb +86 -0
  54. data/lib/job_workflow/namespace.rb +36 -0
  55. data/lib/job_workflow/output.rb +81 -0
  56. data/lib/job_workflow/output_def.rb +14 -0
  57. data/lib/job_workflow/queue.rb +74 -0
  58. data/lib/job_workflow/queue_adapter.rb +38 -0
  59. data/lib/job_workflow/queue_adapters/abstract.rb +87 -0
  60. data/lib/job_workflow/queue_adapters/null_adapter.rb +127 -0
  61. data/lib/job_workflow/queue_adapters/solid_queue_adapter.rb +224 -0
  62. data/lib/job_workflow/runner.rb +173 -0
  63. data/lib/job_workflow/schedule.rb +46 -0
  64. data/lib/job_workflow/semaphore.rb +71 -0
  65. data/lib/job_workflow/task.rb +83 -0
  66. data/lib/job_workflow/task_callable.rb +43 -0
  67. data/lib/job_workflow/task_context.rb +70 -0
  68. data/lib/job_workflow/task_dependency_wait.rb +66 -0
  69. data/lib/job_workflow/task_enqueue.rb +50 -0
  70. data/lib/job_workflow/task_graph.rb +43 -0
  71. data/lib/job_workflow/task_job_status.rb +70 -0
  72. data/lib/job_workflow/task_output.rb +51 -0
  73. data/lib/job_workflow/task_retry.rb +64 -0
  74. data/lib/job_workflow/task_throttle.rb +46 -0
  75. data/lib/job_workflow/version.rb +5 -0
  76. data/lib/job_workflow/workflow.rb +87 -0
  77. data/lib/job_workflow/workflow_status.rb +112 -0
  78. data/lib/job_workflow.rb +59 -0
  79. data/rbs_collection.lock.yaml +172 -0
  80. data/rbs_collection.yaml +14 -0
  81. data/sig/generated/job-workflow.rbs +2 -0
  82. data/sig/generated/job_workflow/argument_def.rbs +14 -0
  83. data/sig/generated/job_workflow/arguments.rbs +26 -0
  84. data/sig/generated/job_workflow/auto_scaling/adapter/aws_adapter.rbs +32 -0
  85. data/sig/generated/job_workflow/auto_scaling/adapter.rbs +22 -0
  86. data/sig/generated/job_workflow/auto_scaling/configuration.rbs +50 -0
  87. data/sig/generated/job_workflow/auto_scaling/executor.rbs +29 -0
  88. data/sig/generated/job_workflow/auto_scaling.rbs +47 -0
  89. data/sig/generated/job_workflow/cache_store_adapters.rbs +28 -0
  90. data/sig/generated/job_workflow/context.rbs +155 -0
  91. data/sig/generated/job_workflow/dry_run_config.rbs +16 -0
  92. data/sig/generated/job_workflow/dsl.rbs +117 -0
  93. data/sig/generated/job_workflow/error_hook.rbs +18 -0
  94. data/sig/generated/job_workflow/hook.rbs +18 -0
  95. data/sig/generated/job_workflow/hook_registry.rbs +47 -0
  96. data/sig/generated/job_workflow/instrumentation/log_subscriber.rbs +102 -0
  97. data/sig/generated/job_workflow/instrumentation/opentelemetry_subscriber.rbs +113 -0
  98. data/sig/generated/job_workflow/instrumentation.rbs +138 -0
  99. data/sig/generated/job_workflow/job_status.rbs +46 -0
  100. data/sig/generated/job_workflow/logger.rbs +56 -0
  101. data/sig/generated/job_workflow/namespace.rbs +24 -0
  102. data/sig/generated/job_workflow/output.rbs +39 -0
  103. data/sig/generated/job_workflow/output_def.rbs +12 -0
  104. data/sig/generated/job_workflow/queue.rbs +49 -0
  105. data/sig/generated/job_workflow/queue_adapter.rbs +18 -0
  106. data/sig/generated/job_workflow/queue_adapters/abstract.rbs +56 -0
  107. data/sig/generated/job_workflow/queue_adapters/null_adapter.rbs +73 -0
  108. data/sig/generated/job_workflow/queue_adapters/solid_queue_adapter.rbs +111 -0
  109. data/sig/generated/job_workflow/runner.rbs +66 -0
  110. data/sig/generated/job_workflow/schedule.rbs +34 -0
  111. data/sig/generated/job_workflow/semaphore.rbs +37 -0
  112. data/sig/generated/job_workflow/task.rbs +60 -0
  113. data/sig/generated/job_workflow/task_callable.rbs +30 -0
  114. data/sig/generated/job_workflow/task_context.rbs +52 -0
  115. data/sig/generated/job_workflow/task_dependency_wait.rbs +42 -0
  116. data/sig/generated/job_workflow/task_enqueue.rbs +27 -0
  117. data/sig/generated/job_workflow/task_graph.rbs +27 -0
  118. data/sig/generated/job_workflow/task_job_status.rbs +42 -0
  119. data/sig/generated/job_workflow/task_output.rbs +29 -0
  120. data/sig/generated/job_workflow/task_retry.rbs +30 -0
  121. data/sig/generated/job_workflow/task_throttle.rbs +20 -0
  122. data/sig/generated/job_workflow/version.rbs +5 -0
  123. data/sig/generated/job_workflow/workflow.rbs +48 -0
  124. data/sig/generated/job_workflow/workflow_status.rbs +55 -0
  125. data/sig/generated/job_workflow.rbs +8 -0
  126. data/sig-private/activejob.rbs +35 -0
  127. data/sig-private/activesupport.rbs +23 -0
  128. data/sig-private/aws.rbs +32 -0
  129. data/sig-private/opentelemetry.rbs +40 -0
  130. data/sig-private/solid_queue.rbs +108 -0
  131. data/tmp/.keep +0 -0
  132. 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.