fractor 0.1.4 → 0.1.7

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 (189) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop-https---raw-githubusercontent-com-riboseinc-oss-guides-main-ci-rubocop-yml +552 -0
  3. data/.rubocop.yml +14 -8
  4. data/.rubocop_todo.yml +284 -43
  5. data/README.adoc +111 -950
  6. data/docs/.lycheeignore +16 -0
  7. data/docs/Gemfile +24 -0
  8. data/docs/README.md +157 -0
  9. data/docs/_config.yml +151 -0
  10. data/docs/_features/error-handling.adoc +1192 -0
  11. data/docs/_features/index.adoc +80 -0
  12. data/docs/_features/monitoring.adoc +589 -0
  13. data/docs/_features/signal-handling.adoc +202 -0
  14. data/docs/_features/workflows.adoc +1235 -0
  15. data/docs/_guides/continuous-mode.adoc +736 -0
  16. data/docs/_guides/cookbook.adoc +1133 -0
  17. data/docs/_guides/index.adoc +55 -0
  18. data/docs/_guides/pipeline-mode.adoc +730 -0
  19. data/docs/_guides/troubleshooting.adoc +358 -0
  20. data/docs/_pages/architecture.adoc +1390 -0
  21. data/docs/_pages/core-concepts.adoc +1392 -0
  22. data/docs/_pages/design-principles.adoc +862 -0
  23. data/docs/_pages/getting-started.adoc +290 -0
  24. data/docs/_pages/installation.adoc +143 -0
  25. data/docs/_reference/api.adoc +1080 -0
  26. data/docs/_reference/error-reporting.adoc +670 -0
  27. data/docs/_reference/examples.adoc +181 -0
  28. data/docs/_reference/index.adoc +96 -0
  29. data/docs/_reference/troubleshooting.adoc +862 -0
  30. data/docs/_tutorials/complex-workflows.adoc +1022 -0
  31. data/docs/_tutorials/data-processing-pipeline.adoc +740 -0
  32. data/docs/_tutorials/first-application.adoc +384 -0
  33. data/docs/_tutorials/index.adoc +48 -0
  34. data/docs/_tutorials/long-running-services.adoc +931 -0
  35. data/docs/assets/images/favicon-16.png +0 -0
  36. data/docs/assets/images/favicon-32.png +0 -0
  37. data/docs/assets/images/favicon-48.png +0 -0
  38. data/docs/assets/images/favicon.ico +0 -0
  39. data/docs/assets/images/favicon.png +0 -0
  40. data/docs/assets/images/favicon.svg +45 -0
  41. data/docs/assets/images/fractor-icon.svg +49 -0
  42. data/docs/assets/images/fractor-logo.svg +61 -0
  43. data/docs/index.adoc +131 -0
  44. data/docs/lychee.toml +39 -0
  45. data/examples/api_aggregator/README.adoc +627 -0
  46. data/examples/api_aggregator/api_aggregator.rb +376 -0
  47. data/examples/auto_detection/README.adoc +407 -29
  48. data/examples/auto_detection/auto_detection.rb +9 -9
  49. data/examples/continuous_chat_common/message_protocol.rb +53 -0
  50. data/examples/continuous_chat_fractor/README.adoc +217 -0
  51. data/examples/continuous_chat_fractor/chat_client.rb +303 -0
  52. data/examples/continuous_chat_fractor/chat_common.rb +83 -0
  53. data/examples/continuous_chat_fractor/chat_server.rb +167 -0
  54. data/examples/continuous_chat_fractor/simulate.rb +345 -0
  55. data/examples/continuous_chat_server/README.adoc +135 -0
  56. data/examples/continuous_chat_server/chat_client.rb +303 -0
  57. data/examples/continuous_chat_server/chat_server.rb +359 -0
  58. data/examples/continuous_chat_server/simulate.rb +343 -0
  59. data/examples/error_reporting.rb +207 -0
  60. data/examples/file_processor/README.adoc +170 -0
  61. data/examples/file_processor/file_processor.rb +615 -0
  62. data/examples/file_processor/sample_files/invalid.csv +1 -0
  63. data/examples/file_processor/sample_files/orders.xml +24 -0
  64. data/examples/file_processor/sample_files/products.json +23 -0
  65. data/examples/file_processor/sample_files/users.csv +6 -0
  66. data/examples/hierarchical_hasher/README.adoc +629 -41
  67. data/examples/hierarchical_hasher/hierarchical_hasher.rb +12 -8
  68. data/examples/image_processor/README.adoc +610 -0
  69. data/examples/image_processor/image_processor.rb +349 -0
  70. data/examples/image_processor/processed_images/sample_10_processed.jpg.json +12 -0
  71. data/examples/image_processor/processed_images/sample_1_processed.jpg.json +12 -0
  72. data/examples/image_processor/processed_images/sample_2_processed.jpg.json +12 -0
  73. data/examples/image_processor/processed_images/sample_3_processed.jpg.json +12 -0
  74. data/examples/image_processor/processed_images/sample_4_processed.jpg.json +12 -0
  75. data/examples/image_processor/processed_images/sample_5_processed.jpg.json +12 -0
  76. data/examples/image_processor/processed_images/sample_6_processed.jpg.json +12 -0
  77. data/examples/image_processor/processed_images/sample_7_processed.jpg.json +12 -0
  78. data/examples/image_processor/processed_images/sample_8_processed.jpg.json +12 -0
  79. data/examples/image_processor/processed_images/sample_9_processed.jpg.json +12 -0
  80. data/examples/image_processor/test_images/sample_1.png +1 -0
  81. data/examples/image_processor/test_images/sample_10.png +1 -0
  82. data/examples/image_processor/test_images/sample_2.png +1 -0
  83. data/examples/image_processor/test_images/sample_3.png +1 -0
  84. data/examples/image_processor/test_images/sample_4.png +1 -0
  85. data/examples/image_processor/test_images/sample_5.png +1 -0
  86. data/examples/image_processor/test_images/sample_6.png +1 -0
  87. data/examples/image_processor/test_images/sample_7.png +1 -0
  88. data/examples/image_processor/test_images/sample_8.png +1 -0
  89. data/examples/image_processor/test_images/sample_9.png +1 -0
  90. data/examples/log_analyzer/README.adoc +662 -0
  91. data/examples/log_analyzer/log_analyzer.rb +579 -0
  92. data/examples/log_analyzer/sample_logs/apache.log +20 -0
  93. data/examples/log_analyzer/sample_logs/json.log +15 -0
  94. data/examples/log_analyzer/sample_logs/nginx.log +15 -0
  95. data/examples/log_analyzer/sample_logs/rails.log +29 -0
  96. data/examples/multi_work_type/README.adoc +576 -26
  97. data/examples/multi_work_type/multi_work_type.rb +30 -29
  98. data/examples/performance_monitoring.rb +120 -0
  99. data/examples/pipeline_processing/README.adoc +740 -26
  100. data/examples/pipeline_processing/pipeline_processing.rb +16 -16
  101. data/examples/priority_work_example.rb +155 -0
  102. data/examples/producer_subscriber/README.adoc +889 -46
  103. data/examples/producer_subscriber/producer_subscriber.rb +20 -16
  104. data/examples/scatter_gather/README.adoc +829 -27
  105. data/examples/scatter_gather/scatter_gather.rb +29 -28
  106. data/examples/simple/README.adoc +347 -0
  107. data/examples/simple/sample.rb +5 -5
  108. data/examples/specialized_workers/README.adoc +622 -26
  109. data/examples/specialized_workers/specialized_workers.rb +88 -45
  110. data/examples/stream_processor/README.adoc +206 -0
  111. data/examples/stream_processor/stream_processor.rb +284 -0
  112. data/examples/web_scraper/README.adoc +625 -0
  113. data/examples/web_scraper/web_scraper.rb +285 -0
  114. data/examples/workflow/README.adoc +406 -0
  115. data/examples/workflow/circuit_breaker/README.adoc +360 -0
  116. data/examples/workflow/circuit_breaker/circuit_breaker_workflow.rb +225 -0
  117. data/examples/workflow/conditional/README.adoc +483 -0
  118. data/examples/workflow/conditional/conditional_workflow.rb +215 -0
  119. data/examples/workflow/dead_letter_queue/README.adoc +374 -0
  120. data/examples/workflow/dead_letter_queue/dead_letter_queue_workflow.rb +217 -0
  121. data/examples/workflow/fan_out/README.adoc +381 -0
  122. data/examples/workflow/fan_out/fan_out_workflow.rb +202 -0
  123. data/examples/workflow/retry/README.adoc +248 -0
  124. data/examples/workflow/retry/retry_workflow.rb +195 -0
  125. data/examples/workflow/simple_linear/README.adoc +267 -0
  126. data/examples/workflow/simple_linear/simple_linear_workflow.rb +175 -0
  127. data/examples/workflow/simplified/README.adoc +329 -0
  128. data/examples/workflow/simplified/simplified_workflow.rb +222 -0
  129. data/exe/fractor +10 -0
  130. data/lib/fractor/cli.rb +288 -0
  131. data/lib/fractor/configuration.rb +307 -0
  132. data/lib/fractor/continuous_server.rb +183 -0
  133. data/lib/fractor/error_formatter.rb +72 -0
  134. data/lib/fractor/error_report_generator.rb +152 -0
  135. data/lib/fractor/error_reporter.rb +244 -0
  136. data/lib/fractor/error_statistics.rb +147 -0
  137. data/lib/fractor/execution_tracer.rb +162 -0
  138. data/lib/fractor/logger.rb +230 -0
  139. data/lib/fractor/main_loop_handler.rb +406 -0
  140. data/lib/fractor/main_loop_handler3.rb +135 -0
  141. data/lib/fractor/main_loop_handler4.rb +299 -0
  142. data/lib/fractor/performance_metrics_collector.rb +181 -0
  143. data/lib/fractor/performance_monitor.rb +215 -0
  144. data/lib/fractor/performance_report_generator.rb +202 -0
  145. data/lib/fractor/priority_work.rb +93 -0
  146. data/lib/fractor/priority_work_queue.rb +189 -0
  147. data/lib/fractor/result_aggregator.rb +33 -1
  148. data/lib/fractor/shutdown_handler.rb +168 -0
  149. data/lib/fractor/signal_handler.rb +80 -0
  150. data/lib/fractor/supervisor.rb +430 -144
  151. data/lib/fractor/supervisor_logger.rb +88 -0
  152. data/lib/fractor/version.rb +1 -1
  153. data/lib/fractor/work.rb +12 -0
  154. data/lib/fractor/work_distribution_manager.rb +151 -0
  155. data/lib/fractor/work_queue.rb +88 -0
  156. data/lib/fractor/work_result.rb +181 -9
  157. data/lib/fractor/worker.rb +75 -1
  158. data/lib/fractor/workflow/builder.rb +210 -0
  159. data/lib/fractor/workflow/chain_builder.rb +169 -0
  160. data/lib/fractor/workflow/circuit_breaker.rb +183 -0
  161. data/lib/fractor/workflow/circuit_breaker_orchestrator.rb +208 -0
  162. data/lib/fractor/workflow/circuit_breaker_registry.rb +112 -0
  163. data/lib/fractor/workflow/dead_letter_queue.rb +334 -0
  164. data/lib/fractor/workflow/execution_hooks.rb +39 -0
  165. data/lib/fractor/workflow/execution_strategy.rb +225 -0
  166. data/lib/fractor/workflow/execution_trace.rb +134 -0
  167. data/lib/fractor/workflow/helpers.rb +191 -0
  168. data/lib/fractor/workflow/job.rb +290 -0
  169. data/lib/fractor/workflow/job_dependency_validator.rb +120 -0
  170. data/lib/fractor/workflow/logger.rb +110 -0
  171. data/lib/fractor/workflow/pre_execution_context.rb +193 -0
  172. data/lib/fractor/workflow/retry_config.rb +156 -0
  173. data/lib/fractor/workflow/retry_orchestrator.rb +184 -0
  174. data/lib/fractor/workflow/retry_strategy.rb +93 -0
  175. data/lib/fractor/workflow/structured_logger.rb +30 -0
  176. data/lib/fractor/workflow/type_compatibility_validator.rb +222 -0
  177. data/lib/fractor/workflow/visualizer.rb +211 -0
  178. data/lib/fractor/workflow/workflow_context.rb +132 -0
  179. data/lib/fractor/workflow/workflow_executor.rb +669 -0
  180. data/lib/fractor/workflow/workflow_result.rb +55 -0
  181. data/lib/fractor/workflow/workflow_validator.rb +295 -0
  182. data/lib/fractor/workflow.rb +333 -0
  183. data/lib/fractor/wrapped_ractor.rb +66 -91
  184. data/lib/fractor/wrapped_ractor3.rb +161 -0
  185. data/lib/fractor/wrapped_ractor4.rb +242 -0
  186. data/lib/fractor.rb +93 -3
  187. metadata +192 -6
  188. data/tests/sample.rb.bak +0 -309
  189. data/tests/sample_working.rb.bak +0 -209
@@ -42,7 +42,7 @@ module ScatterGather
42
42
  error = ArgumentError.new("Unknown source: #{work.source}")
43
43
  return Fractor::WorkResult.new(
44
44
  error: error,
45
- work: work
45
+ work: work,
46
46
  )
47
47
  end
48
48
 
@@ -53,9 +53,9 @@ module ScatterGather
53
53
  query: work.query,
54
54
  hits: result[:hits],
55
55
  metadata: result[:metadata],
56
- timing: result[:timing]
56
+ timing: result[:timing],
57
57
  },
58
- work: work
58
+ work: work,
59
59
  )
60
60
  end
61
61
 
@@ -72,12 +72,12 @@ module ScatterGather
72
72
 
73
73
  # Generate simulated records
74
74
  record_count = rand(3..10)
75
- hits = record_count.times.map do |i|
75
+ hits = Array.new(record_count) do |i|
76
76
  {
77
77
  id: "db-#{i + 1}",
78
78
  title: "Database Result #{i + 1} for '#{work.query}'",
79
79
  content: "This is database content for #{work.query}",
80
- relevance: rand(0.1..1.0).round(2)
80
+ relevance: rand(0.1..1.0).round(2),
81
81
  }
82
82
  end
83
83
 
@@ -86,9 +86,9 @@ module ScatterGather
86
86
  metadata: {
87
87
  source_type: "PostgreSQL Database",
88
88
  total_available: record_count + rand(10..50),
89
- query_type: "Full-text search"
89
+ query_type: "Full-text search",
90
90
  },
91
- timing: rand(0.01..0.3).round(3)
91
+ timing: rand(0.01..0.3).round(3),
92
92
  }
93
93
  end
94
94
 
@@ -98,12 +98,12 @@ module ScatterGather
98
98
 
99
99
  # Generate simulated API results
100
100
  record_count = rand(2..8)
101
- hits = record_count.times.map do |i|
101
+ hits = Array.new(record_count) do |i|
102
102
  {
103
103
  id: "api-#{i + 1}",
104
104
  title: "API Result #{i + 1} for '#{work.query}'",
105
105
  content: "This is API content for #{work.query}",
106
- relevance: rand(0.1..1.0).round(2)
106
+ relevance: rand(0.1..1.0).round(2),
107
107
  }
108
108
  end
109
109
 
@@ -112,9 +112,9 @@ module ScatterGather
112
112
  metadata: {
113
113
  source_type: "External REST API",
114
114
  provider: %w[Google Bing DuckDuckGo].sample,
115
- response_code: 200
115
+ response_code: 200,
116
116
  },
117
- timing: rand(0.1..0.5).round(3)
117
+ timing: rand(0.1..0.5).round(3),
118
118
  }
119
119
  end
120
120
 
@@ -128,12 +128,12 @@ module ScatterGather
128
128
  if cache_hit
129
129
  # Cache hit - return cached results
130
130
  record_count = rand(1..5)
131
- hits = record_count.times.map do |i|
131
+ hits = Array.new(record_count) do |i|
132
132
  {
133
133
  id: "cache-#{i + 1}",
134
134
  title: "Cached Result #{i + 1} for '#{work.query}'",
135
135
  content: "This is cached content for #{work.query}",
136
- relevance: rand(0.1..1.0).round(2)
136
+ relevance: rand(0.1..1.0).round(2),
137
137
  }
138
138
  end
139
139
 
@@ -142,9 +142,9 @@ module ScatterGather
142
142
  metadata: {
143
143
  source_type: "In-memory Cache",
144
144
  cache_hit: true,
145
- age: rand(1..3600)
145
+ age: rand(1..3600),
146
146
  },
147
- timing: rand(0.001..0.05).round(3)
147
+ timing: rand(0.001..0.05).round(3),
148
148
  }
149
149
  else
150
150
  # Cache miss
@@ -152,9 +152,9 @@ module ScatterGather
152
152
  hits: [],
153
153
  metadata: {
154
154
  source_type: "In-memory Cache",
155
- cache_hit: false
155
+ cache_hit: false,
156
156
  },
157
- timing: rand(0.001..0.01).round(3)
157
+ timing: rand(0.001..0.01).round(3),
158
158
  }
159
159
  end
160
160
  end
@@ -165,13 +165,13 @@ module ScatterGather
165
165
 
166
166
  # Generate simulated file results
167
167
  record_count = rand(1..12)
168
- hits = record_count.times.map do |i|
168
+ hits = Array.new(record_count) do |i|
169
169
  {
170
170
  id: "file-#{i + 1}",
171
171
  title: "File Result #{i + 1} for '#{work.query}'",
172
172
  path: "/path/to/file_#{i + 1}.txt",
173
173
  content: "This is file content matching #{work.query}",
174
- relevance: rand(0.1..1.0).round(2)
174
+ relevance: rand(0.1..1.0).round(2),
175
175
  }
176
176
  end
177
177
 
@@ -180,9 +180,9 @@ module ScatterGather
180
180
  metadata: {
181
181
  source_type: "File System",
182
182
  directories_searched: rand(5..20),
183
- files_scanned: rand(50..500)
183
+ files_scanned: rand(50..500),
184
184
  },
185
- timing: rand(0.01..0.2).round(3)
185
+ timing: rand(0.01..0.2).round(3),
186
186
  }
187
187
  end
188
188
  end
@@ -194,8 +194,8 @@ module ScatterGather
194
194
  def initialize(worker_count = 4)
195
195
  @supervisor = Fractor::Supervisor.new(
196
196
  worker_pools: [
197
- { worker_class: SearchWorker, num_workers: worker_count }
198
- ]
197
+ { worker_class: SearchWorker, num_workers: worker_count },
198
+ ],
199
199
  )
200
200
 
201
201
  @merged_results = nil
@@ -204,10 +204,11 @@ module ScatterGather
204
204
  def search(query, sources = nil)
205
205
  # Define search sources with their parameters
206
206
  sources ||= [
207
- { source: :database, params: { max_results: 50, include_archived: false } },
207
+ { source: :database,
208
+ params: { max_results: 50, include_archived: false } },
208
209
  { source: :api, params: { format: "json", timeout: 5 } },
209
210
  { source: :cache, params: { max_age: 3600 } },
210
- { source: :filesystem, params: { extensions: %w[txt md pdf] } }
211
+ { source: :filesystem, params: { extensions: %w[txt md pdf] } },
211
212
  ]
212
213
 
213
214
  start_time = Time.now
@@ -262,7 +263,7 @@ module ScatterGather
262
263
  content: hit[:content],
263
264
  source: source,
264
265
  original_relevance: hit[:relevance],
265
- weighted_relevance: hit[:relevance] * source_weight
266
+ weighted_relevance: hit[:relevance] * source_weight,
266
267
  }
267
268
  end
268
269
  end
@@ -277,7 +278,7 @@ module ScatterGather
277
278
  execution_time: total_time,
278
279
  sources: results_by_source.keys,
279
280
  ranked_results: ranked_hits,
280
- source_details: results_by_source
281
+ source_details: results_by_source,
281
282
  }
282
283
  end
283
284
  end
@@ -309,7 +310,7 @@ if __FILE__ == $PROGRAM_NAME
309
310
  puts "Query: #{results[:query]}"
310
311
  puts "Total hits: #{results[:total_hits]}"
311
312
  puts "Total execution time: #{results[:execution_time].round(3)} seconds"
312
- puts "Sources searched: #{results[:sources].join(", ")}"
313
+ puts "Sources searched: #{results[:sources].join(', ')}"
313
314
  puts
314
315
 
315
316
  puts "Top 5 Results (by relevance):"
@@ -0,0 +1,347 @@
1
+ = Simple Example - Getting Started with Fractor
2
+
3
+ == Purpose
4
+
5
+ Provides the most basic introduction to Fractor, demonstrating fundamental concepts with minimal complexity to help new users get started quickly.
6
+
7
+ == Focus
8
+
9
+ This example focuses on the essential building blocks:
10
+
11
+ * Creating custom `Work` classes to encapsulate work items
12
+ * Creating custom `Worker` classes to process work
13
+ * Setting up a `Supervisor` to manage parallel processing
14
+ * Adding work items and running the supervisor
15
+ * Processing results and handling errors
16
+ * Auto-detection of available processors
17
+
18
+ == Architecture
19
+
20
+ .Basic Fractor Architecture
21
+ [source]
22
+ ----
23
+ Main Program
24
+
25
+ ├─→ Create Work Items (MyWork)
26
+ │ │
27
+ │ └─→ MyWork { value: 1 }
28
+ │ MyWork { value: 2 }
29
+ │ ...
30
+ │ MyWork { value: 10 }
31
+
32
+ ├─→ Create Supervisor
33
+ │ │
34
+ │ └─→ Worker Pool Configuration
35
+ │ │
36
+ │ └─→ MyWorker (auto-detect CPU count)
37
+
38
+ ├─→ Add Work Items
39
+ │ │
40
+ │ └─→ supervisor.add_work_items(work_items)
41
+
42
+ ├─→ Run Supervisor
43
+ │ │
44
+ │ └─→ Parallel Processing
45
+ │ │
46
+ │ ├─→ Ractor 1 (MyWorker) ─→ Process work
47
+ │ ├─→ Ractor 2 (MyWorker) ─→ Process work
48
+ │ ├─→ Ractor 3 (MyWorker) ─→ Process work
49
+ │ └─→ Ractor N (MyWorker) ─→ Process work
50
+ │ │
51
+ │ └─→ Results Aggregator
52
+ │ │
53
+ │ ├─→ Successful Results
54
+ │ └─→ Error Results
55
+
56
+ └─→ Display Results
57
+
58
+ ├─→ supervisor.results (all results)
59
+ └─→ supervisor.results.errors (failed items)
60
+ ----
61
+
62
+ .Data Flow
63
+ [source]
64
+ ----
65
+ Work Items Worker Processing Results
66
+ ──────────────────────────────────────────────────────────────────
67
+
68
+ MyWork(1) ─┐
69
+ MyWork(2) ─┤
70
+ MyWork(3) ─┤
71
+ MyWork(4) ─┼─→ Work Queue ─→ Ractor Pool ─→ WorkResult(2)
72
+ MyWork(5) ─┤ (Supervisor) (Workers) WorkResult(4)
73
+ MyWork(6) ─┤ WorkResult(6)
74
+ MyWork(7) ─┤ WorkResult(8)
75
+ MyWork(8) ─┤ ...
76
+ MyWork(9) ─┤ WorkError(5)
77
+ MyWork(10) ─┘ WorkResult(20)
78
+ ----
79
+
80
+ == Key Components
81
+
82
+ === Work Class
83
+
84
+ The `Work` class encapsulates input data for processing:
85
+
86
+ [source,ruby]
87
+ ----
88
+ class MyWork < Fractor::Work
89
+ def initialize(value)
90
+ super({ value: value }) # <1>
91
+ end
92
+
93
+ def value
94
+ input[:value] # <2>
95
+ end
96
+
97
+ def to_s
98
+ "MyWork: #{value}" # <3>
99
+ end
100
+ end
101
+ ----
102
+ <1> Store data in input hash using `super`
103
+ <2> Access stored data via `input` hash
104
+ <3> Provide string representation for debugging
105
+
106
+ Key points:
107
+
108
+ * Inherit from `Fractor::Work`
109
+ * Store all data in the `input` hash
110
+ * Provide accessor methods for convenience
111
+ * Include `to_s` method for debugging
112
+
113
+ === Worker Class
114
+
115
+ The `Worker` class processes work items:
116
+
117
+ [source,ruby]
118
+ ----
119
+ class MyWorker < Fractor::Worker
120
+ def process(work) # <1>
121
+ # Check work type
122
+ if work.is_a?(MyWork)
123
+ # Handle known error case
124
+ if work.value == 5
125
+ error = StandardError.new('Cannot process value 5')
126
+ return Fractor::WorkResult.new(error: error, work: work) # <2>
127
+ end
128
+
129
+ # Process successfully
130
+ calculated = work.value * 2
131
+ Fractor::WorkResult.new(result: calculated, work: work) # <3>
132
+
133
+ else
134
+ # Handle unexpected work type
135
+ error = TypeError.new("Unsupported work type: #{work.class}")
136
+ Fractor::WorkResult.new(error: error, work: work)
137
+ end
138
+ end
139
+ end
140
+ ----
141
+ <1> Implement `process(work)` method - called by Ractor
142
+ <2> Return `WorkResult` with error for failures
143
+ <3> Return `WorkResult` with result for success
144
+
145
+ Key points:
146
+
147
+ * Inherit from `Fractor::Worker`
148
+ * Implement `process(work)` method
149
+ * Return `Fractor::WorkResult` objects
150
+ * Handle errors gracefully (return error results, don't raise)
151
+ * Support multiple work types if needed
152
+
153
+ === Supervisor Setup
154
+
155
+ The `Supervisor` manages worker Ractors and distributes work:
156
+
157
+ [source,ruby]
158
+ ----
159
+ # Create supervisor with worker pool
160
+ supervisor = Fractor::Supervisor.new(
161
+ worker_pools: [
162
+ { worker_class: MyWorker } # <1>
163
+ ]
164
+ )
165
+
166
+ # Create and add work items
167
+ work_items = (1..10).map { |i| MyWork.new(i) } # <2>
168
+ supervisor.add_work_items(work_items) # <3>
169
+
170
+ # Run the supervisor
171
+ supervisor.run # <4>
172
+
173
+ # Access results
174
+ supervisor.results # <5>
175
+ supervisor.results.errors # <6>
176
+ ----
177
+ <1> Define worker pool (auto-detects CPU count)
178
+ <2> Create array of work items
179
+ <3> Add work items to supervisor
180
+ <4> Start processing (blocks until complete)
181
+ <5> Access all results (successful and failed)
182
+ <6> Access only failed results
183
+
184
+ Key points:
185
+
186
+ * Worker pools default to auto-detected CPU count
187
+ * Can specify `num_workers` explicitly if needed
188
+ * `run` blocks until all work is complete
189
+ * Results are aggregated in `supervisor.results`
190
+
191
+ == Usage
192
+
193
+ Run the example from the project root:
194
+
195
+ [source,shell]
196
+ ----
197
+ ruby examples/simple/sample.rb
198
+
199
+ # With debug output
200
+ FRACTOR_DEBUG=1 ruby examples/simple/sample.rb
201
+ ----
202
+
203
+ == Expected Output
204
+
205
+ [example]
206
+ ====
207
+ [source]
208
+ ----
209
+ Processing complete.
210
+ Final Aggregated Results:
211
+ #<Fractor::ResultAggregator:0x... @results=[
212
+ #<Fractor::WorkResult @result=2, @work=#<MyWork: 1>>,
213
+ #<Fractor::WorkResult @result=4, @work=#<MyWork: 2>>,
214
+ #<Fractor::WorkResult @result=6, @work=#<MyWork: 3>>,
215
+ #<Fractor::WorkResult @result=8, @work=#<MyWork: 4>>,
216
+ #<Fractor::WorkResult @result=12, @work=#<MyWork: 6>>,
217
+ #<Fractor::WorkResult @result=14, @work=#<MyWork: 7>>,
218
+ #<Fractor::WorkResult @result=16, @work=#<MyWork: 8>>,
219
+ #<Fractor::WorkResult @result=18, @work=#<MyWork: 9>>,
220
+ #<Fractor::WorkResult @result=20, @work=#<MyWork: 10>>
221
+ ], @errors=[...]>
222
+
223
+ Failed Work Items (1):
224
+ Work: MyWork: 5
225
+ Error: StandardError: Cannot process value 5
226
+ ----
227
+ ====
228
+
229
+ == Learning Points
230
+
231
+ === Parallel Processing
232
+
233
+ * Fractor automatically distributes work across multiple Ractors
234
+ * Number of Ractors defaults to available CPU cores
235
+ * Work is processed in parallel, improving throughput
236
+ * Order of completion is non-deterministic
237
+
238
+ === Work Encapsulation
239
+
240
+ * Each work item is a separate object with its input data
241
+ * Work items are isolated from each other
242
+ * Workers process one work item at a time
243
+ * Work items can be of different types (polymorphic)
244
+
245
+ === Error Handling
246
+
247
+ * Errors don't stop the entire processing
248
+ * Failed work items are tracked separately
249
+ * Workers return error results, not exceptions
250
+ * System continues processing remaining work
251
+
252
+ === Auto-Detection
253
+
254
+ * When `num_workers` is not specified, Fractor auto-detects CPU count
255
+ * Uses `Etc.nprocessors` to determine available cores
256
+ * Optimal for CPU-bound tasks
257
+ * Can be overridden if needed:
258
+ +
259
+ [source,ruby]
260
+ ----
261
+ supervisor = Fractor::Supervisor.new(
262
+ worker_pools: [
263
+ { worker_class: MyWorker, num_workers: 4 }
264
+ ]
265
+ )
266
+ ----
267
+
268
+ === Result Aggregation
269
+
270
+ * All results are collected in `supervisor.results`
271
+ * Successful results accessible via `results.results`
272
+ * Failed results accessible via `results.errors`
273
+ * Each result contains both the output and original work item
274
+
275
+ == Common Patterns
276
+
277
+ === Multiple Work Types
278
+
279
+ Process different types of work with the same worker:
280
+
281
+ [source,ruby]
282
+ ----
283
+ class MyWorker < Fractor::Worker
284
+ def process(work)
285
+ if work.is_a?(MyWork)
286
+ # Process MyWork
287
+ Fractor::WorkResult.new(result: work.value * 2, work: work)
288
+ elsif work.is_a?(OtherWork)
289
+ # Process OtherWork differently
290
+ Fractor::WorkResult.new(result: "Processed: #{work.value}", work: work)
291
+ else
292
+ # Handle unknown types
293
+ error = TypeError.new("Unsupported work type: #{work.class}")
294
+ Fractor::WorkResult.new(error: error, work: work)
295
+ end
296
+ end
297
+ end
298
+ ----
299
+
300
+ === Conditional Processing
301
+
302
+ Make decisions based on work item data:
303
+
304
+ [source,ruby]
305
+ ----
306
+ def process(work)
307
+ if work.value < 0
308
+ error = ArgumentError.new('Value must be positive')
309
+ return Fractor::WorkResult.new(error: error, work: work)
310
+ end
311
+
312
+ if work.value > 100
313
+ # Heavy processing for large values
314
+ result = complex_calculation(work.value)
315
+ else
316
+ # Simple processing for small values
317
+ result = work.value * 2
318
+ end
319
+
320
+ Fractor::WorkResult.new(result: result, work: work)
321
+ end
322
+ ----
323
+
324
+ === Debugging
325
+
326
+ Use `ENV['FRACTOR_DEBUG']` to enable debug output:
327
+
328
+ [source,ruby]
329
+ ----
330
+ def process(work)
331
+ puts "Working on '#{work.inspect}'" if ENV['FRACTOR_DEBUG']
332
+
333
+ # Processing logic...
334
+
335
+ Fractor::WorkResult.new(result: result, work: work)
336
+ end
337
+ ----
338
+
339
+ == Next Steps
340
+
341
+ After understanding the basics, explore more advanced examples:
342
+
343
+ * link:../auto_detection/README.adoc[Auto Detection] - CPU auto-detection in detail
344
+ * link:../multi_work_type/README.adoc[Multi Work Type] - Handling multiple work types
345
+ * link:../specialized_workers/README.adoc[Specialized Workers] - Worker pools for different tasks
346
+ * link:../pipeline_processing/README.adoc[Pipeline Processing] - Sequential processing stages
347
+ * link:../workflow/README.adoc[Workflow System] - GitHub Actions-style workflows
@@ -32,7 +32,7 @@
32
32
  #
33
33
  # =============================================================================
34
34
 
35
- require_relative "../../lib/fractor"
35
+ require_relative '../../lib/fractor'
36
36
 
37
37
  # Client-specific work item implementation inheriting from Fractor::Work
38
38
  class MyWork < Fractor::Work
@@ -72,14 +72,14 @@ class MyWorker < Fractor::Worker
72
72
  # It should return a Fractor::WorkResult object
73
73
  def process(work)
74
74
  # Only print debug information if FRACTOR_DEBUG is enabled
75
- puts "Working on '#{work.inspect}'" if ENV["FRACTOR_DEBUG"]
75
+ puts "Working on '#{work.inspect}'" if ENV['FRACTOR_DEBUG']
76
76
 
77
77
  # Check work type and handle accordingly
78
78
  if work.is_a?(MyWork)
79
79
  if work.value == 5
80
80
  # Return a Fractor::WorkResult for errors
81
81
  # Store the error object, not just the string
82
- error = StandardError.new("Cannot process value 5")
82
+ error = StandardError.new('Cannot process value 5')
83
83
  return Fractor::WorkResult.new(error: error, work: work)
84
84
  end
85
85
 
@@ -116,8 +116,8 @@ if __FILE__ == $PROGRAM_NAME
116
116
  # Run the supervisor to start processing work
117
117
  supervisor.run
118
118
 
119
- puts "Processing complete."
120
- puts "Final Aggregated Results:"
119
+ puts 'Processing complete.'
120
+ puts 'Final Aggregated Results:'
121
121
  # Access the results aggregator from the supervisor
122
122
  puts supervisor.results.inspect
123
123