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.
@@ -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