fractor 0.1.9 → 0.1.10
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/.rubocop_todo.yml +28 -91
- data/docs/ARCHITECTURE.md +317 -0
- data/docs/PERFORMANCE_TUNING.md +355 -0
- data/docs/TROUBLESHOOTING.md +463 -0
- data/lib/fractor/callback_registry.rb +106 -0
- data/lib/fractor/config_schema.rb +170 -0
- data/lib/fractor/main_loop_handler.rb +4 -8
- data/lib/fractor/main_loop_handler3.rb +10 -12
- data/lib/fractor/main_loop_handler4.rb +48 -20
- data/lib/fractor/result_cache.rb +58 -10
- data/lib/fractor/shutdown_handler.rb +12 -6
- data/lib/fractor/supervisor.rb +100 -13
- data/lib/fractor/version.rb +1 -1
- data/lib/fractor/workflow/execution/dependency_resolver.rb +149 -0
- data/lib/fractor/workflow/execution/fallback_job_handler.rb +68 -0
- data/lib/fractor/workflow/execution/job_executor.rb +242 -0
- data/lib/fractor/workflow/execution/result_builder.rb +76 -0
- data/lib/fractor/workflow/execution/workflow_execution_logger.rb +241 -0
- data/lib/fractor/workflow/workflow_executor.rb +97 -476
- data/lib/fractor/wrapped_ractor.rb +2 -4
- data/lib/fractor.rb +11 -0
- metadata +12 -2
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
# Troubleshooting Guide
|
|
2
|
+
|
|
3
|
+
This guide helps you diagnose and fix common issues with Fractor.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Installation Issues](#installation-issues)
|
|
8
|
+
- [Worker Issues](#worker-issues)
|
|
9
|
+
- [Workflow Issues](#workflow-issues)
|
|
10
|
+
- [Performance Issues](#performance-issues)
|
|
11
|
+
- [Memory Issues](#memory-issues)
|
|
12
|
+
- [Ruby Version Issues](#ruby-version-issues)
|
|
13
|
+
- [Debugging Tips](#debugging-tips)
|
|
14
|
+
|
|
15
|
+
## Installation Issues
|
|
16
|
+
|
|
17
|
+
### Error: `uninitialized constant Fractor`
|
|
18
|
+
|
|
19
|
+
**Symptom**:
|
|
20
|
+
```
|
|
21
|
+
NameError: uninitialized constant Fractor
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Cause**: Fractor is not required or gem is not installed.
|
|
25
|
+
|
|
26
|
+
**Solution**:
|
|
27
|
+
```ruby
|
|
28
|
+
# Add to your Gemfile
|
|
29
|
+
gem "fractor"
|
|
30
|
+
|
|
31
|
+
# Then require in your code
|
|
32
|
+
require "fractor"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Error: `Ractor not available`
|
|
36
|
+
|
|
37
|
+
**Symptom**:
|
|
38
|
+
```
|
|
39
|
+
ArgumentError: Ractor is not available on this Ruby interpreter
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Cause**: Ruby version is too old. Ractor was introduced in Ruby 3.0.
|
|
43
|
+
|
|
44
|
+
**Solution**: Upgrade to Ruby 3.0 or later:
|
|
45
|
+
```bash
|
|
46
|
+
ruby --version # Should be 3.0+
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Worker Issues
|
|
50
|
+
|
|
51
|
+
### Error: `worker_class must be a Class`
|
|
52
|
+
|
|
53
|
+
**Symptom**:
|
|
54
|
+
```
|
|
55
|
+
ArgumentError: worker_class must be a Class (got Symbol), in worker_pools[0]
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Cause**: Passing a symbol or string instead of the class.
|
|
59
|
+
|
|
60
|
+
**Solution**:
|
|
61
|
+
```ruby
|
|
62
|
+
# Wrong
|
|
63
|
+
worker_pools: [{ worker_class: :MyWorker }]
|
|
64
|
+
|
|
65
|
+
# Correct
|
|
66
|
+
worker_pools: [{ worker_class: MyWorker }]
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Error: `must inherit from Fractor::Worker`
|
|
70
|
+
|
|
71
|
+
**Symptom**:
|
|
72
|
+
```
|
|
73
|
+
ArgumentError: MyWorker must inherit from Fractor::Worker
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Cause**: Worker class doesn't inherit from `Fractor::Worker`.
|
|
77
|
+
|
|
78
|
+
**Solution**:
|
|
79
|
+
```ruby
|
|
80
|
+
# Add inheritance
|
|
81
|
+
class MyWorker < Fractor::Worker
|
|
82
|
+
def process(work)
|
|
83
|
+
# ...
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Workers Not Processing Work
|
|
89
|
+
|
|
90
|
+
**Symptom**: Work is added but workers don't process it.
|
|
91
|
+
|
|
92
|
+
**Possible Causes**:
|
|
93
|
+
|
|
94
|
+
1. **Supervisor not started**:
|
|
95
|
+
```ruby
|
|
96
|
+
supervisor.add_work_items(items)
|
|
97
|
+
|
|
98
|
+
# Don't forget to start!
|
|
99
|
+
supervisor.run # For batch mode
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
2. **Work items not valid Fractor::Work**:
|
|
103
|
+
```ruby
|
|
104
|
+
# Ensure work inherits from Fractor::Work
|
|
105
|
+
class MyWork < Fractor::Work
|
|
106
|
+
def initialize(data)
|
|
107
|
+
super({ value: data })
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
3. **Worker's `process` method raises error**:
|
|
113
|
+
```ruby
|
|
114
|
+
# Enable debug to see errors
|
|
115
|
+
supervisor = Fractor::Supervisor.new(
|
|
116
|
+
worker_pools: [{ worker_class: MyWorker }],
|
|
117
|
+
debug: true
|
|
118
|
+
)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Workflow Issues
|
|
122
|
+
|
|
123
|
+
### Error: `Circular dependency detected`
|
|
124
|
+
|
|
125
|
+
**Symptom**:
|
|
126
|
+
```
|
|
127
|
+
Fractor::CircularDependencyError: Circular dependency detected: a -> b -> a
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Cause**: Jobs have circular dependencies.
|
|
131
|
+
|
|
132
|
+
**Solution**: Reorganize jobs to remove circular dependencies:
|
|
133
|
+
```ruby
|
|
134
|
+
# Wrong:
|
|
135
|
+
job "a" do
|
|
136
|
+
runs WorkerA
|
|
137
|
+
needs "b"
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
job "b" do
|
|
141
|
+
runs WorkerB
|
|
142
|
+
needs "a" # Creates circular dependency
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Solution: Refactor to eliminate circular dependency
|
|
146
|
+
# or extract shared logic into a separate job
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Error: `Unknown job in dependency`
|
|
150
|
+
|
|
151
|
+
**Symptom**:
|
|
152
|
+
```
|
|
153
|
+
ArgumentError: Unknown job in dependency: 'nonexistent'
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**Cause**: Job references a non-existent dependency.
|
|
157
|
+
|
|
158
|
+
**Solution**: Ensure all referenced jobs are defined:
|
|
159
|
+
```ruby
|
|
160
|
+
job "process" do
|
|
161
|
+
runs ProcessWorker
|
|
162
|
+
needs "prepare" # Ensure "prepare" job exists
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
job "prepare" do
|
|
166
|
+
runs PrepareWorker
|
|
167
|
+
end
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Workflow Hangs
|
|
171
|
+
|
|
172
|
+
**Symptom**: Workflow execution never completes.
|
|
173
|
+
|
|
174
|
+
**Possible Causes**:
|
|
175
|
+
|
|
176
|
+
1. **Job with no dependencies that's not a start job**:
|
|
177
|
+
```ruby
|
|
178
|
+
# Check for jobs without 'needs' that should have them
|
|
179
|
+
job "orphan" do
|
|
180
|
+
runs OrphanWorker
|
|
181
|
+
# Missing: needs "some_job"
|
|
182
|
+
end
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
2. **Circuit breaker open preventing execution**:
|
|
186
|
+
```ruby
|
|
187
|
+
# Check circuit breaker status
|
|
188
|
+
# Disable temporarily to debug
|
|
189
|
+
job "external_api" do
|
|
190
|
+
runs ExternalAPIWorker
|
|
191
|
+
# Comment out circuit breaker to test
|
|
192
|
+
# circuit_breaker threshold: 5, timeout: 60
|
|
193
|
+
end
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Performance Issues
|
|
197
|
+
|
|
198
|
+
### Slow Execution
|
|
199
|
+
|
|
200
|
+
**Symptom**: Jobs take longer than expected.
|
|
201
|
+
|
|
202
|
+
**Diagnosis**:
|
|
203
|
+
```ruby
|
|
204
|
+
# Enable performance monitoring
|
|
205
|
+
supervisor = Fractor::Supervisor.new(
|
|
206
|
+
worker_pools: [{ worker_class: MyWorker }],
|
|
207
|
+
enable_performance_monitoring: true
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
supervisor.run
|
|
211
|
+
|
|
212
|
+
# Check metrics
|
|
213
|
+
metrics = supervisor.performance_metrics
|
|
214
|
+
puts "Average latency: #{metrics.avg_latency}ms"
|
|
215
|
+
puts "Throughput: #{metrics.throughput} items/sec"
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
**Solutions**:
|
|
219
|
+
1. Increase worker count for CPU-bound work
|
|
220
|
+
2. Use batch processing for many small items
|
|
221
|
+
3. Enable workflow execution caching
|
|
222
|
+
|
|
223
|
+
### Uneven Worker Utilization
|
|
224
|
+
|
|
225
|
+
**Symptom**: Some workers busy, others idle.
|
|
226
|
+
|
|
227
|
+
**Diagnosis**:
|
|
228
|
+
```ruby
|
|
229
|
+
# Check worker status
|
|
230
|
+
status = supervisor.workers_status
|
|
231
|
+
puts "Total: #{status[:total]}"
|
|
232
|
+
puts "Idle: #{status[:idle]}"
|
|
233
|
+
puts "Busy: #{status[:busy]}"
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
**Solution**: Use separate worker pools for different task types:
|
|
237
|
+
```ruby
|
|
238
|
+
worker_pools: [
|
|
239
|
+
{ worker_class: FastWorker, num_workers: 6 },
|
|
240
|
+
{ worker_class: SlowWorker, num_workers: 2 },
|
|
241
|
+
]
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## Memory Issues
|
|
245
|
+
|
|
246
|
+
### Out of Memory
|
|
247
|
+
|
|
248
|
+
**Symptom**: Process crashes with `NoMemoryError` or system OOM killer.
|
|
249
|
+
|
|
250
|
+
**Solutions**:
|
|
251
|
+
|
|
252
|
+
1. **Process results incrementally**:
|
|
253
|
+
```ruby
|
|
254
|
+
# Instead of collecting all results
|
|
255
|
+
supervisor.run
|
|
256
|
+
all_results = supervisor.results.results # Uses lots of memory
|
|
257
|
+
|
|
258
|
+
# Use callbacks
|
|
259
|
+
supervisor.results.on_new_result do |result|
|
|
260
|
+
save_to_disk(result) # Process and discard
|
|
261
|
+
end
|
|
262
|
+
supervisor.run
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
2. **Configure cache limits**:
|
|
266
|
+
```ruby
|
|
267
|
+
cache = Fractor::ResultCache.new(
|
|
268
|
+
max_size: 1000, # Max entries
|
|
269
|
+
max_memory: 100_000_000 # 100MB max
|
|
270
|
+
)
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
3. **Use persistent queue**:
|
|
274
|
+
```ruby
|
|
275
|
+
queue = Fractor::PersistentWorkQueue.new(
|
|
276
|
+
queue_file: "/tmp/work_queue.db"
|
|
277
|
+
)
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Memory Leak
|
|
281
|
+
|
|
282
|
+
**Symptom**: Memory grows continuously during execution.
|
|
283
|
+
|
|
284
|
+
**Diagnosis**:
|
|
285
|
+
```ruby
|
|
286
|
+
# Monitor memory during execution
|
|
287
|
+
require "memory_profiler"
|
|
288
|
+
|
|
289
|
+
report = MemoryProfiler.report do
|
|
290
|
+
supervisor.run
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
report.pretty_print
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
**Possible Causes**:
|
|
297
|
+
1. Accumulating results without processing
|
|
298
|
+
2. Cache growing without limits
|
|
299
|
+
3. Workers retaining references to processed work
|
|
300
|
+
|
|
301
|
+
## Ruby Version Issues
|
|
302
|
+
|
|
303
|
+
### Ruby 3.x vs 4.0 Differences
|
|
304
|
+
|
|
305
|
+
**Symptom**: Code works differently on Ruby 3.x vs 4.0.
|
|
306
|
+
|
|
307
|
+
**Key Differences**:
|
|
308
|
+
|
|
309
|
+
1. **Ractor communication**:
|
|
310
|
+
```ruby
|
|
311
|
+
# Ruby 3.x uses Ractor.yield / Ractor.receive
|
|
312
|
+
# Ruby 4.0 uses Ractor::Port / Ractor.select
|
|
313
|
+
# Fractor handles this automatically via WrappedRactor
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
2. **Main loop handler**:
|
|
317
|
+
```ruby
|
|
318
|
+
# Ruby 3.x: MainLoopHandler
|
|
319
|
+
# Ruby 4.0: MainLoopHandler4
|
|
320
|
+
# Fractor selects the correct one automatically
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
**Solution**: Ensure you're using the latest Fractor version which handles version differences automatically.
|
|
324
|
+
|
|
325
|
+
## Debugging Tips
|
|
326
|
+
|
|
327
|
+
### Enable Debug Output
|
|
328
|
+
|
|
329
|
+
```ruby
|
|
330
|
+
supervisor = Fractor::Supervisor.new(
|
|
331
|
+
worker_pools: [{ worker_class: MyWorker }],
|
|
332
|
+
debug: true # Verbose output
|
|
333
|
+
)
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### Use Execution Tracer
|
|
337
|
+
|
|
338
|
+
```ruby
|
|
339
|
+
# Enable tracing
|
|
340
|
+
supervisor = Fractor::Supervisor.new(
|
|
341
|
+
worker_pools: [{ worker_class: MyWorker }],
|
|
342
|
+
tracer_enabled: true
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
supervisor.run
|
|
346
|
+
|
|
347
|
+
# Get trace
|
|
348
|
+
trace = supervisor.execution_tracer
|
|
349
|
+
trace.each do |event|
|
|
350
|
+
puts "#{event.type}: #{event.work_id}"
|
|
351
|
+
end
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### Check Error Statistics
|
|
355
|
+
|
|
356
|
+
```ruby
|
|
357
|
+
supervisor.run
|
|
358
|
+
|
|
359
|
+
# Get error report
|
|
360
|
+
error_reporter = supervisor.error_reporter
|
|
361
|
+
puts "Total errors: #{error_reporter.total_errors}"
|
|
362
|
+
puts "Error types: #{error_reporter.error_types}"
|
|
363
|
+
|
|
364
|
+
# Generate formatted report
|
|
365
|
+
formatter = Fractor::ErrorFormatter.new
|
|
366
|
+
puts formatter.format_summary(error_reporter)
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### Inspect Worker State
|
|
370
|
+
|
|
371
|
+
```ruby
|
|
372
|
+
# Check which workers are idle/busy
|
|
373
|
+
status = supervisor.workers_status
|
|
374
|
+
status[:pools].each do |pool|
|
|
375
|
+
puts "#{pool[:worker_class]}:"
|
|
376
|
+
pool[:workers].each do |worker|
|
|
377
|
+
state = worker[:idle] ? "idle" : "busy"
|
|
378
|
+
puts " #{worker[:name]}: #{state}"
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
### Test Workers in Isolation
|
|
384
|
+
|
|
385
|
+
```ruby
|
|
386
|
+
# Test your worker directly
|
|
387
|
+
class TestWorker < Fractor::Worker
|
|
388
|
+
def process(work)
|
|
389
|
+
result = expensive_operation(work.input)
|
|
390
|
+
Fractor::WorkResult.new(result: result, work: work)
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
# Test without Fractor overhead
|
|
395
|
+
work = MyWork.new(test_data)
|
|
396
|
+
result = TestWorker.new.process(work)
|
|
397
|
+
puts result
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
## Common Error Messages
|
|
401
|
+
|
|
402
|
+
### `No live workers left`
|
|
403
|
+
|
|
404
|
+
**Cause**: All workers have terminated due to errors.
|
|
405
|
+
|
|
406
|
+
**Solution**:
|
|
407
|
+
1. Check error messages with `error_reporter`
|
|
408
|
+
2. Fix worker errors
|
|
409
|
+
3. Consider using circuit breakers for failing services
|
|
410
|
+
|
|
411
|
+
### `Timeout::Error`
|
|
412
|
+
|
|
413
|
+
**Cause**: Worker exceeded timeout limit.
|
|
414
|
+
|
|
415
|
+
**Solution**:
|
|
416
|
+
```ruby
|
|
417
|
+
# Increase timeout
|
|
418
|
+
class MyWork < Fractor::Work
|
|
419
|
+
def initialize(data)
|
|
420
|
+
super({ value: data }, timeout: 300) # 5 minutes
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
### `ClosedError`
|
|
426
|
+
|
|
427
|
+
**Cause**: Attempting to send work to a closed ractor.
|
|
428
|
+
|
|
429
|
+
**Solution**: This is usually handled automatically by Fractor. If you see this error, it may indicate a bug in Fractor itself.
|
|
430
|
+
|
|
431
|
+
## Getting Help
|
|
432
|
+
|
|
433
|
+
If you're still stuck:
|
|
434
|
+
|
|
435
|
+
1. **Check the examples**: See `examples/` directory for working code
|
|
436
|
+
2. **Enable debug mode**: Set `debug: true` for verbose output
|
|
437
|
+
3. **Check GitHub issues**: Search for similar problems
|
|
438
|
+
4. **Create minimal reproduction**: Create a simple test case that demonstrates the issue
|
|
439
|
+
|
|
440
|
+
## Useful Debugging Commands
|
|
441
|
+
|
|
442
|
+
```ruby
|
|
443
|
+
# Check Ruby version
|
|
444
|
+
puts RUBY_VERSION # Should be 3.0+
|
|
445
|
+
|
|
446
|
+
# Check Ractor availability
|
|
447
|
+
puts Ractor.current # Should return current Ractor
|
|
448
|
+
|
|
449
|
+
# Check Fractor version
|
|
450
|
+
puts Fractor::VERSION
|
|
451
|
+
|
|
452
|
+
# Enable all debug output
|
|
453
|
+
ENV["FRACTOR_DEBUG"] = "1"
|
|
454
|
+
|
|
455
|
+
# Test basic functionality
|
|
456
|
+
supervisor = Fractor::Supervisor.new(
|
|
457
|
+
worker_pools: [{ worker_class: TestWorker }],
|
|
458
|
+
debug: true
|
|
459
|
+
)
|
|
460
|
+
supervisor.add_work_item(TestWork.new("test"))
|
|
461
|
+
supervisor.run
|
|
462
|
+
puts supervisor.results.results
|
|
463
|
+
```
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fractor
|
|
4
|
+
# Registry for managing work and error callbacks in Supervisor and related classes.
|
|
5
|
+
# Provides a clean interface for registering and invoking callbacks with proper
|
|
6
|
+
# error handling and isolation.
|
|
7
|
+
class CallbackRegistry
|
|
8
|
+
attr_reader :work_callbacks, :error_callbacks
|
|
9
|
+
|
|
10
|
+
# Initialize a new callback registry.
|
|
11
|
+
#
|
|
12
|
+
# @param debug [Boolean] Whether to enable debug logging
|
|
13
|
+
def initialize(debug: false)
|
|
14
|
+
@work_callbacks = []
|
|
15
|
+
@error_callbacks = []
|
|
16
|
+
@debug = debug
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Register a work source callback.
|
|
20
|
+
# The callback should return nil or empty array when no new work is available.
|
|
21
|
+
#
|
|
22
|
+
# @yield Block that returns work items or nil/empty array
|
|
23
|
+
# @example
|
|
24
|
+
# registry.register_work_source do
|
|
25
|
+
# fetch_more_work_from_queue
|
|
26
|
+
# end
|
|
27
|
+
def register_work_source(&block)
|
|
28
|
+
@work_callbacks << block
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Register an error callback.
|
|
32
|
+
# The callback receives (error_result, worker_name, worker_class).
|
|
33
|
+
#
|
|
34
|
+
# @yield Block that handles errors
|
|
35
|
+
# @example
|
|
36
|
+
# registry.register_error_callback do |err, worker, klass|
|
|
37
|
+
# logger.error("Error in #{klass}: #{err.error}")
|
|
38
|
+
# end
|
|
39
|
+
def register_error_callback(&block)
|
|
40
|
+
@error_callbacks << block
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Check if there are any work callbacks registered.
|
|
44
|
+
#
|
|
45
|
+
# @return [Boolean] true if work callbacks exist
|
|
46
|
+
def has_work_callbacks?
|
|
47
|
+
!@work_callbacks.empty?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Check if there are any error callbacks registered.
|
|
51
|
+
#
|
|
52
|
+
# @return [Boolean] true if error callbacks exist
|
|
53
|
+
def has_error_callbacks?
|
|
54
|
+
!@error_callbacks.empty?
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Process all work callbacks and return collected work items.
|
|
58
|
+
# Each callback is invoked and any returned work items are collected.
|
|
59
|
+
#
|
|
60
|
+
# @return [Array<Work>] Array of work items from all callbacks
|
|
61
|
+
def process_work_callbacks
|
|
62
|
+
new_work = []
|
|
63
|
+
@work_callbacks.each do |callback|
|
|
64
|
+
result = callback.call
|
|
65
|
+
next unless result
|
|
66
|
+
next if result.empty?
|
|
67
|
+
|
|
68
|
+
new_work.concat(Array(result))
|
|
69
|
+
rescue StandardError => e
|
|
70
|
+
puts "Error in work callback: #{e.message}" if @debug
|
|
71
|
+
end
|
|
72
|
+
new_work
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Invoke all error callbacks with the given error context.
|
|
76
|
+
# Errors in callbacks are caught and logged to prevent cascading failures.
|
|
77
|
+
#
|
|
78
|
+
# @param error_result [WorkResult] The error result
|
|
79
|
+
# @param worker_name [String] Name of the worker that encountered the error
|
|
80
|
+
# @param worker_class [Class] The worker class
|
|
81
|
+
def invoke_error_callbacks(error_result, worker_name, worker_class)
|
|
82
|
+
@error_callbacks.each do |callback|
|
|
83
|
+
callback.call(error_result, worker_name, worker_class)
|
|
84
|
+
rescue StandardError => e
|
|
85
|
+
puts "Error in error callback: #{e.message}" if @debug
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Clear all callbacks.
|
|
90
|
+
# Useful for cleanup or testing.
|
|
91
|
+
def clear
|
|
92
|
+
@work_callbacks.clear
|
|
93
|
+
@error_callbacks.clear
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Get the total number of registered callbacks.
|
|
97
|
+
#
|
|
98
|
+
# @return [Hash] Hash with :work and :error callback counts
|
|
99
|
+
def size
|
|
100
|
+
{
|
|
101
|
+
work: @work_callbacks.size,
|
|
102
|
+
error: @error_callbacks.size,
|
|
103
|
+
}
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|