fractor 0.1.0
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 +8 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/README.adoc +755 -0
- data/Rakefile +12 -0
- data/examples/hierarchical_hasher.rb +158 -0
- data/examples/producer_subscriber.rb +300 -0
- data/lib/fractor/result_aggregator.rb +34 -0
- data/lib/fractor/supervisor.rb +174 -0
- data/lib/fractor/version.rb +6 -0
- data/lib/fractor/work.rb +17 -0
- data/lib/fractor/work_result.rb +35 -0
- data/lib/fractor/worker.rb +11 -0
- data/lib/fractor/wrapped_ractor.rb +140 -0
- data/lib/fractor.rb +17 -0
- data/sample.rb +64 -0
- data/sig/fractor.rbs +4 -0
- data/tests/sample.rb.bak +309 -0
- data/tests/sample_working.rb.bak +209 -0
- metadata +66 -0
data/README.adoc
ADDED
@@ -0,0 +1,755 @@
|
|
1
|
+
= Fractor: Function-driven Ractors framework
|
2
|
+
|
3
|
+
Fractor is a lightweight Ruby framework designed to simplify the process of
|
4
|
+
distributing computational work across multiple Ractors.
|
5
|
+
|
6
|
+
== Introduction
|
7
|
+
|
8
|
+
Fractor stands for *Function-driven Ractors framework*. It is a lightweight Ruby
|
9
|
+
framework designed to simplify the process of distributing computational work
|
10
|
+
across multiple Ractors (Ruby's actor-like concurrency model).
|
11
|
+
|
12
|
+
The primary goal of Fractor is to provide a structured way to define work,
|
13
|
+
process it in parallel using Ractors, and aggregate the results, while
|
14
|
+
abstracting away much of the boilerplate code involved in Ractor management and
|
15
|
+
communication.
|
16
|
+
|
17
|
+
== Installation
|
18
|
+
|
19
|
+
=== Using RubyGems
|
20
|
+
|
21
|
+
[source,sh]
|
22
|
+
----
|
23
|
+
gem install fractor
|
24
|
+
----
|
25
|
+
|
26
|
+
=== Using Bundler
|
27
|
+
|
28
|
+
Add this line to your application's Gemfile:
|
29
|
+
|
30
|
+
[source,ruby]
|
31
|
+
----
|
32
|
+
gem 'fractor'
|
33
|
+
----
|
34
|
+
|
35
|
+
And then execute:
|
36
|
+
|
37
|
+
[source,sh]
|
38
|
+
----
|
39
|
+
bundle install
|
40
|
+
----
|
41
|
+
|
42
|
+
|
43
|
+
=== Key concepts
|
44
|
+
|
45
|
+
* *Function-driven:* You define the core processing logic by subclassing
|
46
|
+
`Fractor::Worker` and implementing the `process` method.
|
47
|
+
|
48
|
+
* *Parallel execution:* Work items are automatically distributed to available
|
49
|
+
worker Ractors for concurrent processing.
|
50
|
+
|
51
|
+
* *Result aggregation:* The framework collects both successful results and
|
52
|
+
errors from the workers.
|
53
|
+
|
54
|
+
* *Separation of concerns:* Keeps the framework logic (`fractor.rb`) separate
|
55
|
+
from the client's specific implementation (`sample.rb`).
|
56
|
+
|
57
|
+
== Scope
|
58
|
+
|
59
|
+
This document describes the design, implementation, and usage of the Fractor
|
60
|
+
framework. It provides detailed information about the framework's components,
|
61
|
+
their interactions, and how to use them to implement parallel processing in Ruby
|
62
|
+
applications.
|
63
|
+
|
64
|
+
[bibliography]
|
65
|
+
== Normative references
|
66
|
+
|
67
|
+
* [[[ruby-ractor,Ruby Ractor Documentation]]], https://docs.ruby-lang.org/en/master/Ractor.html
|
68
|
+
|
69
|
+
== Terms and definitions
|
70
|
+
|
71
|
+
=== ractor
|
72
|
+
|
73
|
+
concurrent programming abstraction in Ruby that enables parallel execution
|
74
|
+
with thread safety
|
75
|
+
|
76
|
+
[.source]
|
77
|
+
<<ruby>>
|
78
|
+
|
79
|
+
=== worker
|
80
|
+
|
81
|
+
component that processes work items to produce work results
|
82
|
+
|
83
|
+
=== work item
|
84
|
+
|
85
|
+
unit of computation to be processed by a ractor
|
86
|
+
|
87
|
+
=== work result
|
88
|
+
|
89
|
+
result of processing a work item, either successful or an error
|
90
|
+
|
91
|
+
=== work item class
|
92
|
+
|
93
|
+
class that represents a work item, typically subclassing `Fractor::Work`
|
94
|
+
|
95
|
+
=== worker class
|
96
|
+
|
97
|
+
class that represents a worker, typically subclassing `Fractor::Worker`
|
98
|
+
|
99
|
+
=== wrapped ractor
|
100
|
+
|
101
|
+
component that manages a single ractor and its associated worker
|
102
|
+
|
103
|
+
=== supervisor
|
104
|
+
|
105
|
+
component that manages the pool of workers and distributes work items
|
106
|
+
|
107
|
+
=== result aggregator
|
108
|
+
|
109
|
+
component that collects and organizes work results from workers
|
110
|
+
|
111
|
+
|
112
|
+
|
113
|
+
|
114
|
+
== Core components
|
115
|
+
|
116
|
+
=== General
|
117
|
+
|
118
|
+
The Fractor framework consists of the following main classes, all residing
|
119
|
+
within the `Fractor` module.
|
120
|
+
|
121
|
+
|
122
|
+
=== Fractor::Worker
|
123
|
+
|
124
|
+
The abstract base class for defining how work should be processed.
|
125
|
+
|
126
|
+
Client code must subclass this and implement the `process(work)` method.
|
127
|
+
|
128
|
+
The `process` method receives a `Fractor::Work` object (or a subclass) and
|
129
|
+
should return a `Fractor::WorkResult` object.
|
130
|
+
|
131
|
+
=== Fractor::Work
|
132
|
+
|
133
|
+
The abstract base class for representing a unit of work.
|
134
|
+
|
135
|
+
Typically holds the input data needed by the `Worker`.
|
136
|
+
|
137
|
+
Client code should subclass this to define specific types of work items.
|
138
|
+
|
139
|
+
=== Fractor::WorkResult
|
140
|
+
|
141
|
+
A container object returned by the `Worker#process` method.
|
142
|
+
|
143
|
+
Holds either the successful `:result` of the computation or an `:error`
|
144
|
+
message if processing failed.
|
145
|
+
|
146
|
+
Includes a reference back to the original `:work` item.
|
147
|
+
|
148
|
+
Provides a `success?` method.
|
149
|
+
|
150
|
+
=== Fractor::ResultAggregator
|
151
|
+
|
152
|
+
Collects and stores all `WorkResult` objects generated by the workers.
|
153
|
+
|
154
|
+
Separates results into `results` (successful) and `errors` arrays.
|
155
|
+
|
156
|
+
=== Fractor::WrappedRactor
|
157
|
+
|
158
|
+
Manages an individual Ruby `Ractor`.
|
159
|
+
|
160
|
+
Instantiates the client-provided `Worker` subclass within the Ractor.
|
161
|
+
|
162
|
+
Handles receiving `Work` items, calling the `Worker#process` method, and
|
163
|
+
yielding `WorkResult` objects (or errors) back to the `Supervisor`.
|
164
|
+
|
165
|
+
=== Fractor::Supervisor
|
166
|
+
|
167
|
+
The main orchestrator of the framework.
|
168
|
+
|
169
|
+
Initializes and manages a pool of `WrappedRactor` instances.
|
170
|
+
|
171
|
+
Manages a `work_queue` of input data.
|
172
|
+
|
173
|
+
Distributes work items (wrapped in the client's `Work` subclass) to available
|
174
|
+
Ractors.
|
175
|
+
|
176
|
+
Listens for results and errors from Ractors using `Ractor.select`.
|
177
|
+
|
178
|
+
Uses `ResultAggregator` to store outcomes.
|
179
|
+
|
180
|
+
Handles graceful shutdown on `SIGINT` (Ctrl+C).
|
181
|
+
|
182
|
+
|
183
|
+
|
184
|
+
== Quick start guide
|
185
|
+
|
186
|
+
=== General
|
187
|
+
|
188
|
+
This quick start guide shows the minimum steps needed to get a simple parallel
|
189
|
+
execution working with Fractor.
|
190
|
+
|
191
|
+
=== Step 1: Create a minimal Work class
|
192
|
+
|
193
|
+
The Work class represents a unit of work to be processed by a Worker. It
|
194
|
+
encapsulates the input data needed for processing.
|
195
|
+
|
196
|
+
[source,ruby]
|
197
|
+
----
|
198
|
+
require 'fractor'
|
199
|
+
|
200
|
+
class MyWork < Fractor::Work
|
201
|
+
# The base class already provides input storage and basic functionality
|
202
|
+
# You can optionally override to_s for better debugging
|
203
|
+
|
204
|
+
def initialize(input)
|
205
|
+
super # This stores input in @input
|
206
|
+
# Add any additional initialization or replace @input with your own logic
|
207
|
+
end
|
208
|
+
|
209
|
+
def to_s
|
210
|
+
"MyWork: #{@input}"
|
211
|
+
end
|
212
|
+
end
|
213
|
+
----
|
214
|
+
|
215
|
+
A Work is instantiated with the input data it will process this way:
|
216
|
+
|
217
|
+
[source,ruby]
|
218
|
+
----
|
219
|
+
work_item = MyWork.new(42)
|
220
|
+
puts work_item.to_s # Output: MyWork: 42
|
221
|
+
----
|
222
|
+
|
223
|
+
|
224
|
+
=== Step 2: Create a minimal Worker class
|
225
|
+
|
226
|
+
The Worker class defines the processing logic for work items. Each Worker
|
227
|
+
instance runs within its own Ractor and processes Work objects sent to it.
|
228
|
+
|
229
|
+
It must implement the `process(work)` method, which takes a Work object as
|
230
|
+
input and returns a `Fractor::WorkResult` object.
|
231
|
+
|
232
|
+
The `process` method should handle both successful processing and error
|
233
|
+
conditions.
|
234
|
+
|
235
|
+
[source,ruby]
|
236
|
+
----
|
237
|
+
class MyWorker < Fractor::Worker
|
238
|
+
def process(work)
|
239
|
+
# Your processing logic here
|
240
|
+
result = work.input * 2
|
241
|
+
|
242
|
+
# Return a success result
|
243
|
+
Fractor::WorkResult.new(result: result, work: work)
|
244
|
+
rescue => e
|
245
|
+
# Return an error result if something goes wrong
|
246
|
+
Fractor::WorkResult.new(error: e.message, work: work)
|
247
|
+
end
|
248
|
+
end
|
249
|
+
----
|
250
|
+
|
251
|
+
The `process` method can perform any computation you need. In this example, it
|
252
|
+
multiplies the input by 2. If an error occurs, it catches the exception and
|
253
|
+
returns an error result.
|
254
|
+
|
255
|
+
=== Step 3: Set up and run the Supervisor
|
256
|
+
|
257
|
+
The Supervisor class orchestrates the entire framework, managing worker Ractors,
|
258
|
+
distributing work, and collecting results.
|
259
|
+
|
260
|
+
It initializes a pool of Ractors, each running an instance of the Worker
|
261
|
+
class. The Supervisor handles the communication between the main thread and
|
262
|
+
the Ractors, including sending work items and receiving results.
|
263
|
+
|
264
|
+
The Supervisor also manages the work queue and the ResultAggregator, which
|
265
|
+
collects and organizes all results from the workers.
|
266
|
+
|
267
|
+
To set up the Supervisor, you need to specify the Worker and Work classes you
|
268
|
+
created earlier. You can also specify the number of parallel Ractors to use.
|
269
|
+
The default is 2, but you can increase this for more parallelism.
|
270
|
+
|
271
|
+
[source,ruby]
|
272
|
+
----
|
273
|
+
# Create the supervisor
|
274
|
+
supervisor = Fractor::Supervisor.new(
|
275
|
+
worker_class: MyWorker,
|
276
|
+
work_class: MyWork,
|
277
|
+
num_workers: 4 # Number of parallel Ractors
|
278
|
+
)
|
279
|
+
|
280
|
+
# Add work items (raw data)
|
281
|
+
supervisor.add_work([1, 2, 3, 4, 5].map { |i| MyWork.new(i) })
|
282
|
+
|
283
|
+
# Run the processing
|
284
|
+
supervisor.run
|
285
|
+
|
286
|
+
# Access results
|
287
|
+
puts "Results: #{supervisor.results.results.map(&:result)}"
|
288
|
+
puts "Errors: #{supervisor.results.errors.size}"
|
289
|
+
----
|
290
|
+
|
291
|
+
That's it! With these three simple steps, you have a working parallel processing
|
292
|
+
system using Fractor.
|
293
|
+
|
294
|
+
|
295
|
+
== Detailed guides
|
296
|
+
|
297
|
+
=== Work class
|
298
|
+
|
299
|
+
==== Purpose and responsibilities
|
300
|
+
|
301
|
+
The `Fractor::Work` class represents a unit of work to be processed by a Worker.
|
302
|
+
Its primary responsibility is to encapsulate the input data needed for
|
303
|
+
processing.
|
304
|
+
|
305
|
+
==== Implementation requirements
|
306
|
+
|
307
|
+
At minimum, your Work subclass should:
|
308
|
+
|
309
|
+
. Inherit from `Fractor::Work`
|
310
|
+
. Pass the input data to the superclass constructor
|
311
|
+
|
312
|
+
[source,ruby]
|
313
|
+
----
|
314
|
+
class MyWork < Fractor::Work
|
315
|
+
def initialize(input)
|
316
|
+
super(input) # This stores input in @input
|
317
|
+
# Add any additional initialization if needed
|
318
|
+
end
|
319
|
+
end
|
320
|
+
----
|
321
|
+
|
322
|
+
==== Advanced usage
|
323
|
+
|
324
|
+
You can extend your Work class to include additional data or methods:
|
325
|
+
|
326
|
+
[source,ruby]
|
327
|
+
----
|
328
|
+
class ComplexWork < Fractor::Work
|
329
|
+
attr_reader :options
|
330
|
+
|
331
|
+
def initialize(input, options = {})
|
332
|
+
super(input)
|
333
|
+
@options = options
|
334
|
+
end
|
335
|
+
|
336
|
+
def high_priority?
|
337
|
+
@options[:priority] == :high
|
338
|
+
end
|
339
|
+
|
340
|
+
def to_s
|
341
|
+
"ComplexWork: #{@input} (#{@options[:priority]} priority)"
|
342
|
+
end
|
343
|
+
end
|
344
|
+
----
|
345
|
+
|
346
|
+
[TIP]
|
347
|
+
====
|
348
|
+
* Keep Work objects lightweight and serializable since they will be passed
|
349
|
+
between Ractors
|
350
|
+
* Implement a meaningful `to_s` method for better debugging
|
351
|
+
* Consider adding validation in the initializer to catch issues early
|
352
|
+
====
|
353
|
+
|
354
|
+
=== Worker class
|
355
|
+
|
356
|
+
==== Purpose and responsibilities
|
357
|
+
|
358
|
+
The `Fractor::Worker` class defines the processing logic for work items. Each
|
359
|
+
Worker instance runs within its own Ractor and processes Work objects sent to
|
360
|
+
it.
|
361
|
+
|
362
|
+
==== Implementation requirements
|
363
|
+
|
364
|
+
Your Worker subclass must:
|
365
|
+
|
366
|
+
. Inherit from `Fractor::Worker`
|
367
|
+
. Implement the `process(work)` method
|
368
|
+
. Return a `Fractor::WorkResult` object from the `process` method
|
369
|
+
. Handle both successful processing and error conditions
|
370
|
+
|
371
|
+
[source,ruby]
|
372
|
+
----
|
373
|
+
class MyWorker < Fractor::Worker
|
374
|
+
def process(work)
|
375
|
+
# Process the work
|
376
|
+
|
377
|
+
if work.input < 0
|
378
|
+
return Fractor::WorkResult.new(
|
379
|
+
error: "Cannot process negative numbers",
|
380
|
+
work: work
|
381
|
+
)
|
382
|
+
end
|
383
|
+
|
384
|
+
# Normal processing...
|
385
|
+
result = work.input * 2
|
386
|
+
|
387
|
+
# Return a WorkResult
|
388
|
+
Fractor::WorkResult.new(result: result, work: work)
|
389
|
+
end
|
390
|
+
end
|
391
|
+
----
|
392
|
+
|
393
|
+
|
394
|
+
==== Error handling
|
395
|
+
|
396
|
+
The Worker class should handle two types of errors.
|
397
|
+
|
398
|
+
|
399
|
+
===== Handled errors
|
400
|
+
|
401
|
+
These are expected error conditions that your code explicitly checks for.
|
402
|
+
|
403
|
+
[source,ruby]
|
404
|
+
----
|
405
|
+
def process(work)
|
406
|
+
if work.input < 0
|
407
|
+
return Fractor::WorkResult.new(
|
408
|
+
error: "Cannot process negative numbers",
|
409
|
+
work: work
|
410
|
+
)
|
411
|
+
end
|
412
|
+
|
413
|
+
# Normal processing...
|
414
|
+
Fractor::WorkResult.new(result: calculated_value, work: work)
|
415
|
+
end
|
416
|
+
----
|
417
|
+
|
418
|
+
=== Unexpected errors caught by rescue
|
419
|
+
|
420
|
+
These are unexpected exceptions that may occur during processing. You should
|
421
|
+
catch these and convert them into error results.
|
422
|
+
|
423
|
+
[source,ruby]
|
424
|
+
----
|
425
|
+
def process(work)
|
426
|
+
# Processing that might raise exceptions
|
427
|
+
result = complex_calculation(work.input)
|
428
|
+
|
429
|
+
Fractor::WorkResult.new(result: result, work: work)
|
430
|
+
rescue StandardError => e
|
431
|
+
# Catch and convert any unexpected exceptions to error results
|
432
|
+
Fractor::WorkResult.new(error: "An unexpected error occurred: #{e.message}", work: work)
|
433
|
+
end
|
434
|
+
----
|
435
|
+
|
436
|
+
[TIP]
|
437
|
+
* Keep the `process` method focused on a single responsibility
|
438
|
+
* Use meaningful error messages that help diagnose issues
|
439
|
+
* Consider adding logging within the `process` method for debugging
|
440
|
+
* Ensure all paths return a valid `WorkResult` object
|
441
|
+
|
442
|
+
=== WorkResult class
|
443
|
+
|
444
|
+
==== Purpose and responsibilities
|
445
|
+
|
446
|
+
The `Fractor::WorkResult` class is a container that holds either the successful
|
447
|
+
result of processing or an error message, along with a reference to the original
|
448
|
+
work item.
|
449
|
+
|
450
|
+
==== Creating results
|
451
|
+
|
452
|
+
To create a successful result:
|
453
|
+
|
454
|
+
[source,ruby]
|
455
|
+
----
|
456
|
+
# For successful processing
|
457
|
+
Fractor::WorkResult.new(result: calculated_value, work: work_object)
|
458
|
+
----
|
459
|
+
|
460
|
+
To create an error result:
|
461
|
+
|
462
|
+
[source,ruby]
|
463
|
+
----
|
464
|
+
# For error conditions
|
465
|
+
Fractor::WorkResult.new(error: "Error message", work: work_object)
|
466
|
+
----
|
467
|
+
|
468
|
+
==== Checking result status
|
469
|
+
|
470
|
+
You can check if a result was successful:
|
471
|
+
|
472
|
+
[source,ruby]
|
473
|
+
----
|
474
|
+
if work_result.success?
|
475
|
+
# Handle successful result
|
476
|
+
processed_value = work_result.result
|
477
|
+
else
|
478
|
+
# Handle error
|
479
|
+
error_message = work_result.error
|
480
|
+
end
|
481
|
+
----
|
482
|
+
|
483
|
+
==== Accessing original work
|
484
|
+
|
485
|
+
The original work item is always available:
|
486
|
+
|
487
|
+
[source,ruby]
|
488
|
+
----
|
489
|
+
original_work = work_result.work
|
490
|
+
input_value = original_work.input
|
491
|
+
----
|
492
|
+
|
493
|
+
=== ResultAggregator class
|
494
|
+
|
495
|
+
==== Purpose and responsibilities
|
496
|
+
|
497
|
+
The `Fractor::ResultAggregator` collects and organizes all results from the
|
498
|
+
workers, separating successful results from errors.
|
499
|
+
|
500
|
+
Completed work results may be order independent or order dependent.
|
501
|
+
|
502
|
+
* For order independent results, the results may be utilized (popped) as they
|
503
|
+
are received.
|
504
|
+
|
505
|
+
* For order dependent results, the results are aggregated in the order they
|
506
|
+
are received. The order of results is important for re-assembly or
|
507
|
+
further processing.
|
508
|
+
|
509
|
+
* For results that require aggregation, the `ResultsAggregator` is used to determine
|
510
|
+
whether the results are completed, which signify that all work items have
|
511
|
+
been processed and ready for further processing.
|
512
|
+
|
513
|
+
|
514
|
+
==== Accessing results
|
515
|
+
|
516
|
+
To access successful results:
|
517
|
+
|
518
|
+
[source,ruby]
|
519
|
+
----
|
520
|
+
# Get all successful results
|
521
|
+
successful_results = supervisor.results.results
|
522
|
+
|
523
|
+
# Extract just the result values
|
524
|
+
result_values = successful_results.map(&:result)
|
525
|
+
----
|
526
|
+
|
527
|
+
To access errors:
|
528
|
+
|
529
|
+
[source,ruby]
|
530
|
+
----
|
531
|
+
# Get all error results
|
532
|
+
error_results = supervisor.results.errors
|
533
|
+
|
534
|
+
# Extract error messages
|
535
|
+
error_messages = error_results.map(&:error)
|
536
|
+
|
537
|
+
# Get the work items that failed
|
538
|
+
failed_work_items = error_results.map(&:work)
|
539
|
+
----
|
540
|
+
|
541
|
+
|
542
|
+
[TIP]
|
543
|
+
====
|
544
|
+
* Check both successful results and errors after processing completes
|
545
|
+
* Consider implementing custom reporting based on the aggregated results
|
546
|
+
====
|
547
|
+
|
548
|
+
|
549
|
+
=== WrappedRactor class
|
550
|
+
|
551
|
+
==== Purpose and responsibilities
|
552
|
+
|
553
|
+
The `Fractor::WrappedRactor` class manages an individual Ruby Ractor, handling
|
554
|
+
the communication between the Supervisor and the Worker instance running inside
|
555
|
+
the Ractor.
|
556
|
+
|
557
|
+
==== Usage notes
|
558
|
+
|
559
|
+
This class is primarily used internally by the Supervisor, but understanding its
|
560
|
+
role helps with debugging:
|
561
|
+
|
562
|
+
* Each WrappedRactor creates and manages one Ractor
|
563
|
+
* The Worker instance lives inside the Ractor
|
564
|
+
* Work items are sent to the Ractor via the WrappedRactor's `send` method
|
565
|
+
* Results are yielded back to the Supervisor
|
566
|
+
|
567
|
+
==== Error propagation
|
568
|
+
|
569
|
+
The WrappedRactor handles error propagation in two ways:
|
570
|
+
|
571
|
+
. Errors from the Worker's `process` method are wrapped in a WorkResult and
|
572
|
+
yielded back
|
573
|
+
. Unexpected errors in the Ractor itself are caught and logged
|
574
|
+
|
575
|
+
=== Supervisor class
|
576
|
+
|
577
|
+
==== Purpose and responsibilities
|
578
|
+
|
579
|
+
The `Fractor::Supervisor` class orchestrates the entire framework, managing
|
580
|
+
worker Ractors, distributing work, and collecting results.
|
581
|
+
|
582
|
+
==== Configuration options
|
583
|
+
|
584
|
+
When creating a Supervisor, you can configure:
|
585
|
+
|
586
|
+
[source,ruby]
|
587
|
+
----
|
588
|
+
supervisor = Fractor::Supervisor.new(
|
589
|
+
worker_class: MyWorker, # Required: Your Worker subclass
|
590
|
+
work_class: MyWork, # Required: Your Work subclass
|
591
|
+
num_workers: 4 # Optional: Number of Ractors (default: 2)
|
592
|
+
)
|
593
|
+
----
|
594
|
+
|
595
|
+
==== Adding work
|
596
|
+
|
597
|
+
You can add work items individually or in batches:
|
598
|
+
|
599
|
+
[source,ruby]
|
600
|
+
----
|
601
|
+
# Add a single item
|
602
|
+
supervisor.add_work([42])
|
603
|
+
|
604
|
+
# Add multiple items
|
605
|
+
supervisor.add_work([1, 2, 3, 4, 5])
|
606
|
+
|
607
|
+
# Add complex items
|
608
|
+
supervisor.add_work([
|
609
|
+
{id: 1, data: "foo"},
|
610
|
+
{id: 2, data: "bar"}
|
611
|
+
])
|
612
|
+
----
|
613
|
+
|
614
|
+
==== Running and monitoring
|
615
|
+
|
616
|
+
To start processing:
|
617
|
+
|
618
|
+
[source,ruby]
|
619
|
+
----
|
620
|
+
# Start processing and block until complete
|
621
|
+
supervisor.run
|
622
|
+
----
|
623
|
+
|
624
|
+
The Supervisor automatically handles:
|
625
|
+
|
626
|
+
* Starting the worker Ractors
|
627
|
+
* Distributing work items to available workers
|
628
|
+
* Collecting results and errors
|
629
|
+
* Graceful shutdown on completion or interruption (Ctrl+C)
|
630
|
+
|
631
|
+
==== Accessing results
|
632
|
+
|
633
|
+
After processing completes:
|
634
|
+
|
635
|
+
[source,ruby]
|
636
|
+
----
|
637
|
+
# Get the ResultAggregator
|
638
|
+
aggregator = supervisor.results
|
639
|
+
|
640
|
+
# Check counts
|
641
|
+
puts "Processed #{aggregator.results.size} items successfully"
|
642
|
+
puts "Encountered #{aggregator.errors.size} errors"
|
643
|
+
|
644
|
+
# Access successful results
|
645
|
+
aggregator.results.each do |result|
|
646
|
+
puts "Work item #{result.work.input} produced #{result.result}"
|
647
|
+
end
|
648
|
+
|
649
|
+
# Access errors
|
650
|
+
aggregator.errors.each do |error_result|
|
651
|
+
puts "Work item #{error_result.work.input} failed: #{error_result.error}"
|
652
|
+
end
|
653
|
+
----
|
654
|
+
|
655
|
+
==== Advanced usage patterns
|
656
|
+
|
657
|
+
===== Custom work distribution
|
658
|
+
|
659
|
+
For more complex scenarios, you might want to prioritize certain work items:
|
660
|
+
|
661
|
+
[source,ruby]
|
662
|
+
----
|
663
|
+
# Add high-priority items first
|
664
|
+
supervisor.add_work(high_priority_items)
|
665
|
+
|
666
|
+
# Run with just enough workers for high-priority items
|
667
|
+
supervisor.run
|
668
|
+
|
669
|
+
# Add and process lower-priority items
|
670
|
+
supervisor.add_work(low_priority_items)
|
671
|
+
supervisor.run
|
672
|
+
----
|
673
|
+
|
674
|
+
===== Handling large datasets
|
675
|
+
|
676
|
+
For very large datasets, consider processing in batches:
|
677
|
+
|
678
|
+
[source,ruby]
|
679
|
+
----
|
680
|
+
large_dataset.each_slice(1000) do |batch|
|
681
|
+
supervisor.add_work(batch)
|
682
|
+
supervisor.run
|
683
|
+
|
684
|
+
# Process this batch's results before continuing
|
685
|
+
process_batch_results(supervisor.results)
|
686
|
+
end
|
687
|
+
----
|
688
|
+
|
689
|
+
== Running the example
|
690
|
+
|
691
|
+
. Install the gem as described in the Installation section.
|
692
|
+
|
693
|
+
. Create a new Ruby file (e.g., `my_fractor_example.rb`) with your implementation:
|
694
|
+
|
695
|
+
[source,ruby]
|
696
|
+
----
|
697
|
+
require 'fractor'
|
698
|
+
|
699
|
+
# Define your Work class
|
700
|
+
class MyWork < Fractor::Work
|
701
|
+
def to_s
|
702
|
+
"MyWork: #{@input}"
|
703
|
+
end
|
704
|
+
end
|
705
|
+
|
706
|
+
# Define your Worker class
|
707
|
+
class MyWorker < Fractor::Worker
|
708
|
+
def process(work)
|
709
|
+
if work.input == 5
|
710
|
+
# Return a Fractor::WorkResult for errors
|
711
|
+
return Fractor::WorkResult.new(error: "Error processing work #{work.input}", work: work)
|
712
|
+
end
|
713
|
+
|
714
|
+
calculated = work.input * 2
|
715
|
+
# Return a Fractor::WorkResult for success
|
716
|
+
Fractor::WorkResult.new(result: calculated, work: work)
|
717
|
+
end
|
718
|
+
end
|
719
|
+
|
720
|
+
# Create supervisor
|
721
|
+
supervisor = Fractor::Supervisor.new(
|
722
|
+
worker_class: MyWorker,
|
723
|
+
work_class: MyWork,
|
724
|
+
num_workers: 2
|
725
|
+
)
|
726
|
+
|
727
|
+
# Add work items (1..10)
|
728
|
+
supervisor.add_work((1..10).to_a)
|
729
|
+
|
730
|
+
# Run processing
|
731
|
+
supervisor.run
|
732
|
+
|
733
|
+
# Display results
|
734
|
+
puts "Results: #{supervisor.results.results.map(&:result).join(', ')}"
|
735
|
+
puts "Errors: #{supervisor.results.errors.map { |e| e.work.input }.join(', ')}"
|
736
|
+
----
|
737
|
+
|
738
|
+
. Run the example from your terminal:
|
739
|
+
|
740
|
+
[source,sh]
|
741
|
+
----
|
742
|
+
ruby my_fractor_example.rb
|
743
|
+
----
|
744
|
+
|
745
|
+
You will see output showing Ractors starting, receiving work, processing it, and
|
746
|
+
the final aggregated results, including any errors encountered. Press `Ctrl+C`
|
747
|
+
during execution to test the graceful shutdown.
|
748
|
+
|
749
|
+
|
750
|
+
|
751
|
+
== Copyright and license
|
752
|
+
|
753
|
+
Copyright Ribose.
|
754
|
+
|
755
|
+
Licensed under the MIT License.
|