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,240 @@
1
+ # Task Outputs
2
+
3
+ JobWorkflow allows tasks to define and collect outputs, making it easy to access task execution results. This is particularly useful when you need to use results from previous tasks in subsequent tasks or when collecting results from parallel map tasks.
4
+
5
+ ## Defining Task Outputs
6
+
7
+ Use the `output:` option to define the structure of task outputs. Specify output field names and their types as a hash.
8
+
9
+ ### Basic Output Definition
10
+
11
+ ```ruby
12
+ class DataProcessingJob < ApplicationJob
13
+ include JobWorkflow::DSL
14
+
15
+ argument :input_value, "Integer", default: 0
16
+
17
+ # Define task with outputs
18
+ task :calculate, output: { result: "Integer", message: "String" } do |ctx|
19
+ input_value = ctx.arguments.input_value
20
+ # Return a hash with the defined keys
21
+ {
22
+ result: input_value * 2,
23
+ message: "Calculation complete"
24
+ }
25
+ end
26
+
27
+ # Access the output from another task
28
+ task :report, depends_on: [:calculate] do |ctx|
29
+ puts "Result: #{ctx.output[:calculate].first.result}"
30
+ puts "Message: #{ctx.output[:calculate].first.message}"
31
+ end
32
+ end
33
+ ```
34
+
35
+ ### Output with Map Tasks
36
+
37
+ Outputs from map tasks are collected as an array, with one output per iteration.
38
+
39
+ ```ruby
40
+ class BatchCalculationJob < ApplicationJob
41
+ include JobWorkflow::DSL
42
+
43
+ argument :numbers, "Array[Integer]", default: []
44
+
45
+ # Map task with output definition
46
+ task :double_numbers,
47
+ each: ->(ctx) { ctx.arguments.numbers },
48
+ output: { doubled: "Integer", original: "Integer" } do |ctx|
49
+ value = ctx.each_value
50
+ {
51
+ doubled: value * 2,
52
+ original: value
53
+ }
54
+ end
55
+
56
+ # Access all outputs from the map task
57
+ task :summarize, depends_on: [:double_numbers] do |ctx|
58
+ ctx.output[:double_numbers].each do |output|
59
+ puts "Original: #{output.original}, Doubled: #{output.doubled}"
60
+ end
61
+
62
+ # Calculate total
63
+ total = ctx.output[:double_numbers].sum(&:doubled)
64
+ puts "Total: #{total}"
65
+ end
66
+ end
67
+
68
+ # Execution
69
+ BatchCalculationJob.perform_now(numbers: [1, 2, 3, 4, 5])
70
+ # Output:
71
+ # Original: 1, Doubled: 2
72
+ # Original: 2, Doubled: 4
73
+ # Original: 3, Doubled: 6
74
+ # Original: 4, Doubled: 8
75
+ # Original: 5, Doubled: 10
76
+ # Total: 30
77
+ ```
78
+
79
+ ## Accessing Task Outputs
80
+
81
+ Task outputs are accessible through `ctx.output` using `[]` with the task name. It always returns an Array of TaskOutput-like objects.
82
+
83
+ ### Regular Task Output
84
+
85
+ ```ruby
86
+ task :fetch_data, output: { count: "Integer", items: "Array" } do |ctx|
87
+ data = ExternalAPI.fetch
88
+ {
89
+ count: data.size,
90
+ items: data
91
+ }
92
+ end
93
+
94
+ task :process, depends_on: [:fetch_data] do |ctx|
95
+ # Access output fields directly
96
+ puts "Received #{ctx.output[:fetch_data].first.count} items"
97
+ ctx.output[:fetch_data].first.items.each do |item|
98
+ process_item(item)
99
+ end
100
+ end
101
+ ```
102
+
103
+ ### Map Task Output Array
104
+
105
+ ```ruby
106
+ task :process_items,
107
+ each: ->(ctx) { ctx.arguments.items },
108
+ output: { result: "String", status: "String" } do |ctx|
109
+ item = ctx.each_value
110
+ {
111
+ result: transform(item),
112
+ status: "success"
113
+ }
114
+ end
115
+
116
+ task :verify, depends_on: [:process_items] do |ctx|
117
+ # outputs is an array of TaskOutput objects
118
+ outputs = ctx.output[:process_items]
119
+
120
+ successful = outputs.count { |o| o.status == "success" }
121
+ puts "Processed #{outputs.size} items, #{successful} successful"
122
+
123
+ # Access individual outputs by index
124
+ first_result = outputs[0].result
125
+ last_result = outputs[-1].result
126
+ end
127
+ ```
128
+
129
+ ## Output Field Normalization
130
+
131
+ Task outputs are automatically normalized based on the output definition:
132
+
133
+ 1. **Only defined fields are collected**: Fields not in the output definition are ignored
134
+ 2. **Missing fields default to nil**: If a defined field is not returned, it defaults to `nil`
135
+ 3. **Type safety**: Output definitions document expected types for better code clarity
136
+
137
+ ```ruby
138
+ task :example, output: { required: "String", optional: "Integer" } do |ctx|
139
+ # Only return one field
140
+ { required: "value" }
141
+ # optional will be nil
142
+ end
143
+
144
+ task :check_output, depends_on: [:example] do |ctx|
145
+ puts ctx.output[:example].first.required # => "value"
146
+ puts ctx.output[:example].first.optional # => nil
147
+ end
148
+ ```
149
+
150
+ ## Output Persistence
151
+
152
+ Task outputs are automatically serialized and persisted with the Context, allowing them to:
153
+
154
+ - **Survive job restarts**: Outputs are preserved across job retries
155
+ - **Resume correctly**: When using continuations, outputs from completed tasks are available
156
+ - **Pass between jobs**: In map tasks with concurrency, outputs from subjobs are collected
157
+
158
+ ## Output Design Guidelines
159
+
160
+ ### When to Use Outputs
161
+
162
+ Use task outputs when you need to:
163
+
164
+ - **Extract structured data** from a task for use in later tasks
165
+ - **Collect results** from parallel map task executions
166
+ - **Document return values** with types for better code clarity
167
+ - **Separate concerns** between task execution and result usage
168
+
169
+ ### When to Use Context Instead
170
+
171
+ Use Context fields when you need to:
172
+
173
+ - **Share mutable state** that tasks modify incrementally
174
+ - **Pass configuration** or settings through the workflow
175
+ - **Store final results** that are the primary goal of the workflow
176
+
177
+ ### Best Practices
178
+
179
+ ```ruby
180
+ class WellDesignedJob < ApplicationJob
181
+ include JobWorkflow::DSL
182
+
183
+ # Arguments for configuration
184
+ argument :user_id, "Integer"
185
+
186
+ # Use outputs for intermediate structured data
187
+ task :fetch_user,
188
+ output: { name: "String", email: "String", role: "String" } do |ctx|
189
+ user = User.find(ctx.arguments.user_id)
190
+ {
191
+ name: user.name,
192
+ email: user.email,
193
+ role: user.role
194
+ }
195
+ end
196
+
197
+ task :fetch_permissions,
198
+ depends_on: [:fetch_user],
199
+ output: { permissions: "Array[String]" } do |ctx|
200
+ role = ctx.output[:fetch_user].first.role
201
+ {
202
+ permissions: PermissionService.get_permissions(role)
203
+ }
204
+ end
205
+
206
+ # Build final report as output
207
+ task :generate_report,
208
+ depends_on: [:fetch_user, :fetch_permissions],
209
+ output: { final_report: "Hash" } do |ctx|
210
+ user = ctx.output[:fetch_user].first
211
+ perms = ctx.output[:fetch_permissions].first
212
+
213
+ {
214
+ final_report: {
215
+ user: { name: user.name, email: user.email },
216
+ permissions: perms.permissions,
217
+ generated_at: Time.current
218
+ }
219
+ }
220
+ end
221
+ end
222
+ ```
223
+
224
+ ## Limitations
225
+
226
+ ### Arguments are Immutable
227
+
228
+ Arguments cannot be modified during workflow execution. To pass data between tasks, use task outputs:
229
+
230
+ ```ruby
231
+ # ✅ Correct: Use outputs
232
+ task :process, output: { result: "String" } do |ctx|
233
+ { result: "processed" }
234
+ end
235
+
236
+ # ❌ Wrong: Cannot modify arguments
237
+ task :wrong do |ctx|
238
+ ctx.arguments.result = "value" # Error!
239
+ end
240
+ ```
@@ -0,0 +1,56 @@
1
+ # Testing Strategy
2
+
3
+ This section covers effective testing methods for workflows built with JobWorkflow.
4
+
5
+ ## Unit Testing
6
+
7
+ ### Testing Individual Tasks
8
+
9
+ Test each task as a unit.
10
+
11
+ ```ruby
12
+ # spec/jobs/user_registration_job_spec.rb
13
+ RSpec.describe UserRegistrationJob do
14
+ describe 'task: validate_email' do
15
+ it 'validates correct email format' do
16
+ job = described_class.new
17
+ arguments = JobWorkflow::Arguments.new(email: 'user@example.com')
18
+ ctx = JobWorkflow::Context.new(arguments: arguments)
19
+
20
+ task = described_class._workflow_tasks[:validate_email]
21
+ expect { job.instance_exec(ctx, &task[:block]) }.not_to raise_error
22
+ end
23
+
24
+ it 'raises error for invalid email' do
25
+ job = described_class.new
26
+ arguments = JobWorkflow::Arguments.new(email: 'invalid')
27
+ ctx = JobWorkflow::Context.new(arguments: arguments)
28
+
29
+ task = described_class._workflow_tasks[:validate_email]
30
+ expect { job.instance_exec(ctx, &task[:block]) }.to raise_error(/Invalid email/)
31
+ end
32
+ end
33
+
34
+ describe 'task: create_user' do
35
+ it 'creates a new user' do
36
+ job = described_class.new
37
+ arguments = JobWorkflow::Arguments.new(
38
+ email: 'user@example.com',
39
+ password: 'password123'
40
+ )
41
+ ctx = JobWorkflow::Context.new(arguments: arguments)
42
+
43
+ task = described_class._workflow_tasks[:create_user]
44
+
45
+ expect {
46
+ job.instance_exec(ctx, &task[:block])
47
+ }.to change(User, :count).by(1)
48
+
49
+ # Verify output
50
+ output = ctx.output[:create_user].first
51
+ expect(output.user).to be_a(User)
52
+ expect(output.user.email).to eq('user@example.com')
53
+ end
54
+ end
55
+ end
56
+ ```
@@ -0,0 +1,198 @@
1
+ # Throttling
2
+
3
+ JobWorkflow provides semaphore-based throttling to handle external API rate limits and protect shared resources. Throttling works across multiple jobs and workers, ensuring system-wide rate limiting.
4
+
5
+ ## Task-Level Throttling
6
+
7
+ ### Simple Integer Syntax (Recommended)
8
+
9
+ For most use cases, specify the concurrency limit as an integer:
10
+
11
+ ```ruby
12
+ class ExternalAPIJob < ApplicationJob
13
+ include JobWorkflow::DSL
14
+
15
+ argument :user_ids, "Array[Integer]"
16
+
17
+ # Allow up to 10 concurrent executions of this task
18
+ # Default key: "ExternalAPIJob:fetch_user_data"
19
+ # Default TTL: 180 seconds
20
+ task :fetch_user_data, throttle: 10, each: ->(ctx) { ctx.arguments.user_ids }, output: { user_data: "Hash" } do |ctx|
21
+ { user_data: ExternalAPI.fetch_user(ctx.each_value) }
22
+ end
23
+ end
24
+ ```
25
+
26
+ ### Hash Syntax (Advanced Configuration)
27
+
28
+ For detailed control, use the hash syntax:
29
+
30
+ ```ruby
31
+ task :fetch_user_data,
32
+ throttle: {
33
+ key: "external_user_api", # Custom semaphore key
34
+ limit: 10, # Concurrency limit
35
+ ttl: 120 # Lease TTL in seconds (default: 180)
36
+ },
37
+ output: { api_results: "Hash" } do |ctx|
38
+ { api_results: ExternalAPI.fetch_user(ctx.arguments.user_id) }
39
+ end
40
+ ```
41
+
42
+ ## Sharing Throttle Keys Across Jobs
43
+
44
+ Use the same `key` to share rate limits across different jobs and tasks:
45
+
46
+ ```ruby
47
+ # Both jobs share the same "payment_api" throttle limit
48
+ class CreateUserJob < ApplicationJob
49
+ include JobWorkflow::DSL
50
+
51
+ argument :user_data, "Hash"
52
+
53
+ task :create_customer, throttle: { key: "payment_api", limit: 5 } do |ctx|
54
+ PaymentService.create_customer(ctx.arguments.user_data)
55
+ end
56
+ end
57
+
58
+ class UpdateBillingJob < ApplicationJob
59
+ include JobWorkflow::DSL
60
+
61
+ argument :billing_id, "String"
62
+
63
+ task :update_billing, throttle: { key: "payment_api", limit: 5 } do |ctx|
64
+ PaymentService.update_billing(ctx.arguments.billing_id)
65
+ end
66
+ end
67
+
68
+ # Total concurrent calls to payment API: max 5 across both jobs
69
+ ```
70
+
71
+ ## Throttling Behavior
72
+
73
+ 1. Acquire semaphore lease before task execution
74
+ 2. If lease cannot be acquired, wait (automatic polling with 3-second intervals)
75
+ 3. Execute task
76
+ 4. Release lease after completion (guaranteed by ensure block)
77
+ 5. If a worker crashes before releasing, the lease is recovered after `ttl` expires and the SolidQueue dispatcher concurrency maintenance runs (worst case: `ttl + concurrency_maintenance_interval`)
78
+
79
+ ```ruby
80
+ argument :data, "Hash"
81
+
82
+ # Example: Task with max 3 concurrent executions
83
+ task :limited_task, throttle: 3, output: { result: "String" } do |ctx|
84
+ data = ctx.arguments.data
85
+ { result: SharedResource.use(data) }
86
+ end
87
+
88
+ # Execution state:
89
+ # Job 1: Acquire lease → Executing
90
+ # Job 2: Acquire lease → Executing
91
+ # Job 3: Acquire lease → Executing
92
+ # Job 4: Waiting (no lease available)
93
+ # Job 1: Complete → Release lease
94
+ # Job 4: Acquire lease → Executing
95
+ ```
96
+
97
+ ## Throttling with Map Tasks
98
+
99
+ Throttling is especially useful with map tasks to limit API calls:
100
+
101
+ ```ruby
102
+ class BatchFetchJob < ApplicationJob
103
+ include JobWorkflow::DSL
104
+
105
+ argument :ids, "Array[Integer]"
106
+
107
+ # Each iteration waits for a throttle slot
108
+ task :fetch_all, throttle: 5, each: ->(ctx) { ctx.arguments.ids }, output: { data: "Hash" } do |ctx|
109
+ { data: RateLimitedAPI.fetch(ctx.each_value) }
110
+ end
111
+ end
112
+
113
+ # With 100 IDs and throttle: 5
114
+ # → Max 5 concurrent API calls at any time
115
+ ```
116
+
117
+ ## Runtime Throttling
118
+
119
+ For fine-grained control within a task, use the `ctx.throttle` method to wrap specific code blocks. This method can only be called inside a task block; calling it outside will raise an error.
120
+
121
+ ```ruby
122
+ class ComplexProcessingJob < ApplicationJob
123
+ include JobWorkflow::DSL
124
+
125
+ argument :data, "Hash"
126
+
127
+ task :process_and_save do |ctx|
128
+ # Read operations - no throttle needed
129
+ data = ExternalAPI.fetch(ctx.arguments.data[:id])
130
+
131
+ # Write operations - throttled
132
+ ctx.throttle(limit: 3, key: "db_write") do
133
+ Model.create!(data)
134
+ end
135
+ end
136
+ end
137
+ ```
138
+
139
+ ### Multiple Throttle Blocks
140
+
141
+ Apply different rate limits to different operations within the same task:
142
+
143
+ ```ruby
144
+ task :multi_api_task do |ctx|
145
+ # Payment API: max 5 concurrent
146
+ ctx.throttle(limit: 5, key: "payment_api") do
147
+ PaymentService.process(ctx.arguments.payment_data)
148
+ end
149
+
150
+ # Notification API: max 10 concurrent
151
+ ctx.throttle(limit: 10, key: "notification_api") do
152
+ NotificationService.send(ctx.arguments.message_params)
153
+ end
154
+ end
155
+ ```
156
+
157
+ ### Auto-Generated Keys
158
+
159
+ When `key` is omitted, a unique key is generated automatically based on the job name, task name, and call index. The index resets to 0 for each task execution:
160
+
161
+ ```ruby
162
+ task :sequential_operations do |ctx|
163
+ # Key: "MyJob:sequential_operations:0"
164
+ ctx.throttle(limit: 5) do
165
+ first_operation
166
+ end
167
+
168
+ # Key: "MyJob:sequential_operations:1"
169
+ ctx.throttle(limit: 5) do
170
+ second_operation
171
+ end
172
+ end
173
+ ```
174
+
175
+ ## Combining Task-Level and Runtime Throttling
176
+
177
+ Use both approaches for comprehensive rate limiting:
178
+
179
+ ```ruby
180
+ class APIIntegrationJob < ApplicationJob
181
+ include JobWorkflow::DSL
182
+
183
+ argument :ids, "Array[Integer]"
184
+
185
+ # Task-level throttle: limits overall task concurrency
186
+ task :process_items, throttle: 10, each: ->(ctx) { ctx.arguments.ids } do |ctx|
187
+
188
+ data = ExternalAPI.fetch(ctx.each_value)
189
+
190
+ # Runtime throttle: limits specific write operations
191
+ ctx.throttle(limit: 3, key: "cache_write") do
192
+ CacheStorage.update(ctx.each_value, data)
193
+ end
194
+
195
+ data
196
+ end
197
+ end
198
+ ```
@@ -0,0 +1,53 @@
1
+ # Troubleshooting
2
+
3
+ This section covers common issues encountered during JobWorkflow operation and their solutions.
4
+
5
+ ## Common Issues
6
+
7
+ ### CircularDependencyError
8
+
9
+ **Symptom**: Workflow crashes with `JobWorkflow::CircularDependencyError`
10
+
11
+ ```ruby
12
+ # ❌ Circular dependency
13
+ task :a, depends_on: [:b] do |ctx|
14
+ # ...
15
+ end
16
+
17
+ task :b, depends_on: [:a] do |ctx|
18
+ # ...
19
+ end
20
+ ```
21
+
22
+ **Solution**: Review and remove circular dependency
23
+
24
+ ```ruby
25
+ # ✅ Correct dependency
26
+ task :a do |ctx|
27
+ # ...
28
+ end
29
+
30
+ task :b, depends_on: [:a] do |ctx|
31
+ # ...
32
+ end
33
+ ```
34
+
35
+ ### UnknownTaskError
36
+
37
+ **Symptom**: `JobWorkflow::UnknownTaskError: Unknown task: :typo_task`
38
+
39
+ ```ruby
40
+ # ❌ Depending on non-existent task
41
+ task :process, depends_on: [:typo_task] do |ctx|
42
+ # ...
43
+ end
44
+ ```
45
+
46
+ **Solution**: Fix task name typo
47
+
48
+ ```ruby
49
+ # ✅ Correct task name
50
+ task :process, depends_on: [:correct_task] do |ctx|
51
+ # ...
52
+ end
53
+ ```