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
@@ -0,0 +1,862 @@
1
+ ---
2
+ layout: default
3
+ title: Design principles
4
+ nav_order: 4
5
+ ---
6
+ = Design principles
7
+
8
+ == Overview
9
+
10
+ This guide explains the design philosophy, core principles, and key decisions that shaped Fractor's architecture. Understanding these principles helps you use Fractor effectively and make informed decisions when building applications.
11
+
12
+ == Core principles
13
+
14
+ === Function-driven architecture
15
+
16
+ Fractor is built around the concept of functions (workers) operating on data (work items), rather than objects managing state.
17
+
18
+ .Function-driven vs object-oriented state management
19
+ [source]
20
+ ----
21
+ Traditional OOP (Stateful Objects)
22
+ ┌────────────────────────────────┐
23
+ │ WorkerObject │
24
+ │ ┌──────────────────────────┐ │
25
+ │ │ State │ │
26
+ │ │ - @counter │ │
27
+ │ │ - @connections │ │
28
+ │ │ - @cache │ │
29
+ │ └──────────────────────────┘ │
30
+ │ │ │
31
+ │ │ Methods modify state │
32
+ │ └──────────────────────────── │
33
+ │ │
34
+ │ Problem: State shared across │
35
+ │ threads leads to race conditions
36
+ └────────────────────────────────┘
37
+
38
+ Fractor (Function-Driven)
39
+ ┌────────────────────────────────┐
40
+ │ Worker │
41
+ │ ┌──────────────────────────┐ │
42
+ │ │ process(work) │ │
43
+ │ │ └─> Transform input │ │
44
+ │ │ └─> Return result │ │
45
+ │ └──────────────────────────┘ │
46
+ │ │
47
+ │ Input → Function → Output │
48
+ │ No shared state │
49
+ │ Isolated in Ractor │
50
+ │ │
51
+ │ Benefit: True parallelism │
52
+ │ without race conditions │
53
+ └────────────────────────────────┘
54
+ ----
55
+
56
+ *Rationale*:
57
+
58
+ * Functions are naturally parallelizable
59
+ * No shared mutable state eliminates race conditions
60
+ * Easier to reason about and test
61
+ * Maps well to Ractor's isolation model
62
+
63
+ === Separation of concerns
64
+
65
+ Each component has a single, well-defined responsibility:
66
+
67
+ [cols="2,3,3"]
68
+ |===
69
+ |Component |Concern |What It Doesn't Do
70
+
71
+ |[`Work`](../../lib/fractor/work.rb)
72
+ |Hold input data
73
+ |Processing logic, validation, transformation
74
+
75
+ |[`Worker`](../../lib/fractor/worker.rb)
76
+ |Process work items
77
+ |Work distribution, result aggregation, lifecycle management
78
+
79
+ |[`Supervisor`](../../lib/fractor/supervisor.rb)
80
+ |Orchestrate execution
81
+ |Business logic, data transformation
82
+
83
+ |[`WrappedRactor`](../../lib/fractor/wrapped_ractor.rb)
84
+ |Manage Ractor lifecycle
85
+ |Work processing, distribution strategy
86
+
87
+ |[`ResultAggregator`](../../lib/fractor/result_aggregator.rb)
88
+ |Collect results
89
+ |Processing logic, distribution
90
+ |===
91
+
92
+ *Benefits*:
93
+
94
+ * Each component can be tested independently
95
+ * Changes are localized to affected components
96
+ * Easy to understand and maintain
97
+ * Clear interfaces between components
98
+
99
+ === Composability
100
+
101
+ Fractor components are designed to work together flexibly:
102
+
103
+ .Composition examples
104
+ [example]
105
+ ====
106
+ [source,ruby]
107
+ ----
108
+ # Basic composition: Work + Worker + Supervisor
109
+ class DataWork < Fractor::Work; end
110
+ class DataWorker < Fractor::Worker; end
111
+ supervisor = Fractor::Supervisor.new(
112
+ worker_pools: [{ worker_class: DataWorker }]
113
+ )
114
+
115
+ # Add queue for continuous processing
116
+ work_queue = Fractor::WorkQueue.new
117
+ work_queue.register_with_supervisor(supervisor)
118
+
119
+ # Wrap in ContinuousServer for production
120
+ server = Fractor::ContinuousServer.new(
121
+ worker_pools: [{ worker_class: DataWorker }],
122
+ work_queue: work_queue
123
+ )
124
+
125
+ # Add error handling
126
+ server.on_error do |error_result|
127
+ ErrorReporter.notify(error_result)
128
+ end
129
+
130
+ # Add result processing
131
+ server.on_result do |result|
132
+ ResultProcessor.handle(result)
133
+ end
134
+
135
+ # Compose into Workflow
136
+ class ProcessingPipeline < Fractor::Workflow
137
+ workflow "pipeline" do
138
+ job "extract", DataWorker
139
+ job "transform", TransformWorker, needs: "extract"
140
+ job "load", LoadWorker, needs: "transform"
141
+ end
142
+ end
143
+ ----
144
+ ====
145
+
146
+ *Design decision*: Components use interface-based composition rather than inheritance-based hierarchies. This makes it easy to combine components in new ways without modifying existing code.
147
+
148
+ === Extensibility
149
+
150
+ Fractor is designed to be extended without modifying core code:
151
+
152
+ ==== Extension points
153
+
154
+ [cols="2,3,3"]
155
+ |===
156
+ |Extension Point |How to Extend |Example Use Case
157
+
158
+ |Work classes
159
+ |Subclass `Fractor::Work`
160
+ |Add domain-specific convenience methods
161
+
162
+ |Worker classes
163
+ |Subclass `Fractor::Worker`
164
+ |Implement custom processing logic
165
+
166
+ |Result callbacks
167
+ |Register with `ResultAggregator`
168
+ |Custom logging, monitoring, notifications
169
+
170
+ |Work sources
171
+ |Register with `Supervisor`
172
+ |Pull from external queues, APIs, databases
173
+
174
+ |Workflow DSL
175
+ |Define jobs and dependencies
176
+ |Custom multi-step processing graphs
177
+
178
+ |Error handling
179
+ |Use `WorkResult` error metadata
180
+ |Smart retry, circuit breakers, DLQ
181
+ |===
182
+
183
+ .Extending with custom work source
184
+ [example]
185
+ ====
186
+ [source,ruby]
187
+ ----
188
+ # Custom work source from external API
189
+ supervisor.register_work_source do
190
+ # Poll external API
191
+ response = api_client.poll_for_work
192
+
193
+ # Transform to Work objects
194
+ response.items.map { |item| MyWork.new(item) }
195
+ end
196
+
197
+ # Or use WorkQueue for thread-safe pushing
198
+ queue = Fractor::WorkQueue.new
199
+ queue.register_with_supervisor(supervisor)
200
+
201
+ # External thread can push work safely
202
+ Thread.new do
203
+ loop do
204
+ item = external_source.next
205
+ queue << MyWork.new(item)
206
+ end
207
+ end
208
+ ----
209
+ ====
210
+
211
+ *Principle*: Open for extension, closed for modification. Users extend Fractor through well-defined interfaces without touching framework code.
212
+
213
+ == Design decisions
214
+
215
+ === Why Ractors over threads?
216
+
217
+ [cols="1,2,2"]
218
+ |===
219
+ |Aspect |Threads (with GIL) |Ractors (Fractor)
220
+
221
+ |Parallelism
222
+ |Pseudo-parallel (GIL limits)
223
+ |True parallel execution
224
+
225
+ |Memory model
226
+ |Shared mutable state
227
+ |Isolated memory spaces
228
+
229
+ |Race conditions
230
+ |Possible, requires locks
231
+ |Impossible (no shared state)
232
+
233
+ |CPU utilization
234
+ |Single core (Ruby GIL)
235
+ |All cores simultaneously
236
+
237
+ |Complexity
238
+ |Simple API, complex safety
239
+ |Structured messages, guaranteed safety
240
+ |===
241
+
242
+ .Performance comparison
243
+ [example]
244
+ ====
245
+ CPU-bound workload (10,000 calculations):
246
+
247
+ [source]
248
+ ----
249
+ Threads (GIL-limited):
250
+ 4 threads: ~10 seconds (no speedup due to GIL)
251
+ 8 threads: ~10 seconds (still limited by GIL)
252
+
253
+ Ractors (Fractor):
254
+ 4 ractors: ~2.5 seconds (4x speedup)
255
+ 8 ractors: ~1.25 seconds (8x speedup)
256
+ ----
257
+ ====
258
+
259
+ *Decision*: Use Ractors for true parallelism despite:
260
+
261
+ * Newer feature (Ruby 3.0+)
262
+ * More restrictive sharing rules
263
+ * Less ecosystem maturity
264
+
265
+ The benefits of true parallelism and guaranteed thread safety outweigh these limitations for Fractor's use cases.
266
+
267
+ === Why message passing?
268
+
269
+ Fractor uses message passing between Supervisor and Ractors instead of shared state.
270
+
271
+ .Communication approaches
272
+ [source]
273
+ ----
274
+ Shared State (Traditional)
275
+ ┌──────────┐ ┌──────────┐
276
+ │ Thread 1 │───▶│ Shared │◀───│ Thread 2 │
277
+ └──────────┘ │ Memory │ └──────────┘
278
+ │ │
279
+ │ Requires │
280
+ │ Locks │
281
+ └──────────┘
282
+
283
+ Problem: Race conditions,
284
+ deadlocks, complexity
285
+
286
+ Message Passing (Fractor)
287
+ ┌──────────┐ ┌──────────┐
288
+ │ Ractor 1 │────┐ ┌─▶│ Ractor 2 │
289
+ └──────────┘ │ │ └──────────┘
290
+ ▼ ▼
291
+ Message Queue
292
+ (Copy semantics)
293
+
294
+ Benefits: No locks,
295
+ no race conditions,
296
+ predictable behavior
297
+ ----
298
+
299
+ *Trade-offs*:
300
+
301
+ Advantages:
302
+
303
+ * No race conditions possible
304
+ * No deadlocks or lock contention
305
+ * Predictable, sequential message handling
306
+ * Natural fit for actor model
307
+
308
+ Disadvantages:
309
+
310
+ * Messages must be copied or shareable (immutable)
311
+ * Slightly higher memory usage per message
312
+ * Cannot share large mutable structures directly
313
+
314
+ *Decision*: Message passing selected because:
315
+
316
+ . Safety guarantees are paramount for parallel processing
317
+ . Message copying overhead is minimal for typical work items
318
+ . Immutability is a best practice anyway
319
+ . Enables reliable distributed systems patterns
320
+
321
+ === Why immutable Work objects?
322
+
323
+ Work objects must be shareable (immutable or frozen) to cross Ractor boundaries.
324
+
325
+ [source,ruby]
326
+ ----
327
+ # Why immutability is required
328
+ class MutableWork < Fractor::Work
329
+ def initialize(data)
330
+ @data = data # Mutable reference
331
+ end
332
+ end
333
+
334
+ # This fails - cannot share mutable objects
335
+ work = MutableWork.new({ value: 42 })
336
+ ractor.send(work) # Ractor::IsolationError
337
+
338
+ # Solution: Freeze data
339
+ class ImmutableWork < Fractor::Work
340
+ def initialize(data)
341
+ super(data.freeze) # Frozen, thus shareable
342
+ end
343
+ end
344
+
345
+ work = ImmutableWork.new({ value: 42 })
346
+ ractor.send(work) # ✓ Works - frozen data is shareable
347
+ ----
348
+
349
+ *Benefits of immutability*:
350
+
351
+ . *Thread safety*: Cannot be modified during processing
352
+ . *Predictability*: Input data unchanged throughout pipeline
353
+ . *Debugging*: Work object state never changes
354
+ . *Optimization*: Ruby can optimize frozen objects
355
+
356
+ *Trade-off*: Must create new objects for mutations instead of modifying in place. This is acceptable because:
357
+
358
+ * Work items are typically small
359
+ * Functional transformation is clearer
360
+ * Memory overhead is negligible
361
+ * GC handles short-lived objects efficiently
362
+
363
+ === API design rationale
364
+
365
+ Fractor's API follows these design principles:
366
+
367
+ ==== Explicit over implicit
368
+
369
+ [source,ruby]
370
+ ----
371
+ # Explicit: Clear what's happening
372
+ supervisor = Fractor::Supervisor.new(
373
+ worker_pools: [
374
+ { worker_class: MyWorker, num_workers: 4 }
375
+ ],
376
+ continuous_mode: false
377
+ )
378
+
379
+ # vs Implicit (rejected design)
380
+ # supervisor = Fractor.auto # Too much magic
381
+ ----
382
+
383
+ *Rationale*: Explicit configuration makes behavior predictable and debuggable.
384
+
385
+ ==== Sensible defaults with override capability
386
+
387
+ [source,ruby]
388
+ ----
389
+ # Defaults work well
390
+ supervisor = Fractor::Supervisor.new(
391
+ worker_pools: [{ worker_class: MyWorker }]
392
+ # num_workers: auto-detected from CPU count
393
+ # continuous_mode: false (pipeline mode)
394
+ )
395
+
396
+ # But can override when needed
397
+ supervisor = Fractor::Supervisor.new(
398
+ worker_pools: [
399
+ { worker_class: MyWorker, num_workers: 16 }
400
+ ],
401
+ continuous_mode: true
402
+ )
403
+ ----
404
+
405
+ *Principle*: Optimize for the common case, but allow customization.
406
+
407
+ ==== Consistent naming conventions
408
+
409
+ [cols="2,3,2"]
410
+ |===
411
+ |Pattern |Examples |Rationale
412
+
413
+ |Nouns for data
414
+ |`Work`, `WorkResult`, `WorkQueue`
415
+ |Represents state/data
416
+
417
+ |Verbs for actions
418
+ |`process()`, `execute()`, `run()`
419
+ |Represents operations
420
+
421
+ |Descriptive adjectives
422
+ |`PriorityWork`, `WrappedRactor`, `ContinuousServer`
423
+ |Clarifies purpose
424
+
425
+ |`on_*` for callbacks
426
+ |`on_result`, `on_error`, `on_add`
427
+ |Clear callback intent
428
+ |===
429
+
430
+ ==== Progressive disclosure
431
+
432
+ API complexity reveals itself as needed:
433
+
434
+ [source]
435
+ ----
436
+ Level 1: Simple pipeline
437
+ supervisor = Fractor::Supervisor.new(
438
+ worker_pools: [{ worker_class: Worker }]
439
+ )
440
+ supervisor.add_work_items(work_items)
441
+ supervisor.run
442
+
443
+ Level 2: Continuous mode
444
+ supervisor = Fractor::Supervisor.new(
445
+ worker_pools: [{ worker_class: Worker }],
446
+ continuous_mode: true
447
+ )
448
+ supervisor.register_work_source { fetch_work }
449
+ supervisor.run
450
+
451
+ Level 3: Production with server
452
+ server = Fractor::ContinuousServer.new(
453
+ worker_pools: [{ worker_class: Worker }],
454
+ work_queue: queue,
455
+ log_file: "fractor.log"
456
+ )
457
+ server.on_result { |r| process(r) }
458
+ server.on_error { |e| handle(e) }
459
+ server.run
460
+
461
+ Level 4: Workflows
462
+ class Pipeline < Fractor::Workflow
463
+ workflow "pipeline" do
464
+ job "a", WorkerA
465
+ job "b", WorkerB, needs: "a"
466
+ configure_dead_letter_queue(max_size: 1000)
467
+ end
468
+ end
469
+ result = Pipeline.new.execute(input: data)
470
+ ----
471
+
472
+ *Principle*: Start simple, add complexity only when needed.
473
+
474
+ == Trade-offs
475
+
476
+ === Performance vs simplicity
477
+
478
+ ==== Trade-off: Ractor overhead
479
+
480
+ *Performance cost*: Creating Ractors has overhead (~1-2ms per Ractor, ~1MB memory)
481
+
482
+ *Simplicity gain*: Automatic parallelism without manual thread management
483
+
484
+ *Decision*: Accept overhead because:
485
+
486
+ * One-time cost at startup
487
+ * Amortized over long-running processing
488
+ * Benefits of parallelism far outweigh cost for work items > 5ms
489
+
490
+ ==== Trade-off: Message copying
491
+
492
+ *Performance cost*: Objects copied when sent between Ractors
493
+
494
+ *Simplicity gain*: No shared state, no locks, guaranteed safety
495
+
496
+ *Decision*: Accept copying because:
497
+
498
+ * Work items should be small and focused
499
+ * Copying is fast for typical objects
500
+ * Safety guarantees are worth the cost
501
+ * Can use frozen/shareable objects to avoid some copying
502
+
503
+ === Flexibility vs constraints
504
+
505
+ ==== Trade-off: Structured message protocol
506
+
507
+ *Constraint*: Must use specific message format (`{ type:, result:, processor: }`)
508
+
509
+ *Flexibility loss*: Cannot send arbitrary data between Supervisor and Ractors
510
+
511
+ *Benefit*: Predictable, debuggable message flow
512
+
513
+ *Decision*: Structure is beneficial because:
514
+
515
+ * Makes debugging easier (can trace all messages)
516
+ * Enables future features (message logging, inspection)
517
+ * Cost is minimal (just a Hash wrapper)
518
+ * Structure prevents ad-hoc communication bugs
519
+
520
+ ==== Trade-off: Work/Worker class requirements
521
+
522
+ *Constraint*: Must subclass `Fractor::Work` and `Fractor::Worker`
523
+
524
+ *Flexibility loss*: Cannot use arbitrary objects/functions
525
+
526
+ *Benefit*: Type safety, validation, consistent interface
527
+
528
+ *Decision*: Classes provide:
529
+
530
+ * Clear contracts and interfaces
531
+ * Better error messages
532
+ * IDE support and autocomplete
533
+ * Extension points for future features
534
+
535
+ === Safety vs speed
536
+
537
+ ==== Trade-off: Immutable work objects
538
+
539
+ *Safety*: Frozen objects prevent modification bugs
540
+
541
+ *Speed cost*: Cannot modify in place, must create new objects
542
+
543
+ *Decision*: Safety prioritized because:
544
+
545
+ * Bugs from mutation are hard to debug
546
+ * Functional transformation is clearer
547
+ * Cost is minimal for typical use cases
548
+ * Can optimize hot paths if needed
549
+
550
+ ==== Trade-off: Error capture and wrapping
551
+
552
+ *Safety*: All errors captured in `WorkResult`, none silently swallowed
553
+
554
+ *Speed cost*: Exception handling overhead
555
+
556
+ *Decision*: Comprehensive error handling because:
557
+
558
+ * Silent failures are worse than slowdowns
559
+ * Debugging production issues requires error context
560
+ * Overhead is negligible (<1%)
561
+ * Enables sophisticated error handling (retry, DLQ, etc.)
562
+
563
+ == Design patterns employed
564
+
565
+ === Actor model
566
+
567
+ Fractor implements the actor model for concurrency:
568
+
569
+ [source]
570
+ ----
571
+ Actor = WrappedRactor + Worker
572
+
573
+ Actor Properties:
574
+ 1. Isolated state (each Ractor has own memory)
575
+ 2. Message-driven (communicate via send/receive)
576
+ 3. Asynchronous processing (non-blocking sends)
577
+ 4. Location transparent (don't care which Ractor)
578
+ ----
579
+
580
+ *Why actor model?*
581
+
582
+ * Proven pattern for concurrent systems
583
+ * Natural fit for Ractor capabilities
584
+ * Scales well (can add more actors easily)
585
+ * Fault tolerant (actor failures isolated)
586
+
587
+ === Supervisor pattern
588
+
589
+ [`Supervisor`](../../lib/fractor/supervisor.rb) monitors and manages worker Ractors:
590
+
591
+ [source]
592
+ ----
593
+ Supervisor Responsibilities:
594
+ ├── Start workers
595
+ ├── Distribute work
596
+ ├── Monitor health
597
+ ├── Collect results
598
+ ├── Handle signals
599
+ └── Graceful shutdown
600
+ ----
601
+
602
+ *Benefits*:
603
+
604
+ * Centralized lifecycle management
605
+ * Fault isolation and recovery
606
+ * Observable system state
607
+ * Coordinated shutdown
608
+
609
+ === Producer-consumer pattern
610
+
611
+ Work flows from producers to consumers via queues:
612
+
613
+ [source]
614
+ ----
615
+ Producers → WorkQueue → Supervisor → Workers (Consumers)
616
+
617
+ Multiple producers can feed one queue
618
+ Multiple workers can consume from one queue
619
+ Queue provides backpressure and buffering
620
+ ----
621
+
622
+ *Why this pattern?*
623
+
624
+ * Decouples production from consumption
625
+ * Enables different production/consumption rates
626
+ * Buffers temporary load spikes
627
+ * Natural fit for continuous mode
628
+
629
+ === Pipeline pattern
630
+
631
+ Workflows implement staged processing:
632
+
633
+ [source]
634
+ ----
635
+ Input → Stage 1 → Stage 2 → Stage 3 → Output
636
+
637
+ Each stage:
638
+ - Independent worker pool
639
+ - Consumes previous stage output
640
+ - Can run in parallel with other stages
641
+ ----
642
+
643
+ *Benefits*:
644
+
645
+ * Clear data flow
646
+ * Each stage independently scalable
647
+ * Easy to add/remove/reorder stages
648
+ * Type-safe stage boundaries
649
+
650
+ === Builder pattern
651
+
652
+ Workflows use builder pattern for construction:
653
+
654
+ [source,ruby]
655
+ ----
656
+ # Fluent API for workflow construction
657
+ workflow = Fractor::Workflow.chain("pipeline")
658
+ .step("parse", ParseWorker)
659
+ .step("validate", ValidateWorker)
660
+ .step("transform", TransformWorker)
661
+ .build
662
+
663
+ # Or using DSL
664
+ class Pipeline < Fractor::Workflow
665
+ workflow "pipeline" do
666
+ job "parse", ParseWorker
667
+ job "validate", ValidateWorker, needs: "parse"
668
+ end
669
+ end
670
+ ----
671
+
672
+ *Why builder pattern?*
673
+
674
+ * Expressive DSL
675
+ * Validation before execution
676
+ * Immutable after construction
677
+ * Clear separation: definition vs execution
678
+
679
+ == Design evolution
680
+
681
+ === Learning from production use
682
+
683
+ Fractor's design evolved based on real-world usage:
684
+
685
+ ==== Early design: Shared queue
686
+
687
+ *Original*: Workers directly accessed shared queue
688
+
689
+ [source,ruby]
690
+ ----
691
+ # Early design (rejected)
692
+ class Worker
693
+ def initialize(queue)
694
+ @queue = queue
695
+ end
696
+
697
+ def run
698
+ loop do
699
+ work = @queue.pop # Shared state!
700
+ process(work)
701
+ end
702
+ end
703
+ end
704
+ ----
705
+
706
+ *Problem*: Race conditions, complex locking, unclear ownership
707
+
708
+ *Evolution*: Supervisor owns queue and distributes work
709
+
710
+ [source,ruby]
711
+ ----
712
+ # Current design
713
+ class Supervisor
714
+ def run
715
+ loop do
716
+ ractor, message = Ractor.select(*ractors)
717
+ # Supervisor controls all work distribution
718
+ if work_available?
719
+ ractor.send(next_work)
720
+ end
721
+ end
722
+ end
723
+ end
724
+ ----
725
+
726
+ *Benefit*: Clear ownership, no shared state, predictable flow
727
+
728
+ ==== Evolution: Error handling
729
+
730
+ *Original*: Errors crashed Ractors
731
+
732
+ *Problem*: Lost work, no error context, hard to debug
733
+
734
+ *Evolution 1*: Catch and log errors
735
+
736
+ *Problem*: Errors logged but work lost, no retry possible
737
+
738
+ *Evolution 2*: `WorkResult` with error metadata
739
+
740
+ [source,ruby]
741
+ ----
742
+ WorkResult.new(
743
+ error: exception,
744
+ work: original_work, # Can retry!
745
+ error_category: :network, # Categorized
746
+ error_context: { ... } # Rich context
747
+ )
748
+ ----
749
+
750
+ *Benefit*: Errors don't lose work, rich context, enables smart retry
751
+
752
+ ==== Evolution: Priority handling
753
+
754
+ *Original*: FIFO queue only
755
+
756
+ *Problem*: Critical work delayed by low-priority work
757
+
758
+ *Evolution*: `PriorityWorkQueue` with aging
759
+
760
+ *Benefit*: Priority-based processing, no starvation
761
+
762
+ These evolutions demonstrate Fractor's commitment to learning from production use and continuously improving while maintaining backward compatibility.
763
+
764
+ == Philosophical foundations
765
+
766
+ === Favor composition over inheritance
767
+
768
+ Fractor uses composition to combine behavior:
769
+
770
+ [source,ruby]
771
+ ----
772
+ # Composition: ContinuousServer has-a Supervisor
773
+ class ContinuousServer
774
+ def initialize(worker_pools:)
775
+ @supervisor = Supervisor.new(
776
+ worker_pools: worker_pools,
777
+ continuous_mode: true
778
+ )
779
+ end
780
+ end
781
+
782
+ # Not inheritance: ContinuousServer is-a Supervisor
783
+ class ContinuousServer < Supervisor # ✗ Rejected
784
+ end
785
+ ----
786
+
787
+ *Rationale*: Composition is more flexible and doesn't create tight coupling.
788
+
789
+ === Make the right thing easy
790
+
791
+ API design encourages best practices:
792
+
793
+ [source,ruby]
794
+ ----
795
+ # Easy: Type-safe workflow
796
+ class Pipeline < Fractor::Workflow
797
+ workflow "pipeline" do
798
+ input_type InputModel
799
+ output_type OutputModel
800
+
801
+ job "process", ProcessWorker
802
+ end
803
+ end
804
+
805
+ # Hard: Skip type safety
806
+ # (Must explicitly avoid it)
807
+ ----
808
+
809
+ *Principle*: Default path should be the best path.
810
+
811
+ === Optimize for debugging
812
+
813
+ Production issues are inevitable, so Fractor optimizes for debuggability:
814
+
815
+ * All errors captured with context
816
+ * Optional execution tracing
817
+ * Clear error messages with suggestions
818
+ * Structured logging support
819
+ * Dead letter queue for failed items
820
+ * Workflow visualization
821
+
822
+ *Philosophy*: Time spent debugging production issues far exceeds time spent writing code, so invest in debuggability.
823
+
824
+ === Fail explicitly, not silently
825
+
826
+ Fractor never silently swallows errors:
827
+
828
+ [source,ruby]
829
+ ----
830
+ # Every error path is explicit
831
+ case result
832
+ when success?
833
+ process_success(result.result)
834
+ when failure?
835
+ handle_error(result.error) # Must handle
836
+ end
837
+
838
+ # No: result.result_or_nil # Silent failure
839
+ # No: rescue; nil; end # Swallow errors
840
+ ----
841
+
842
+ *Philosophy*: Explicit failures are easier to debug than silent corruption.
843
+
844
+ == Conclusion
845
+
846
+ Fractor's design reflects these core values:
847
+
848
+ * *Safety first*: Correctness over performance
849
+ * *Clarity*: Explicit over implicit
850
+ * *Composability*: Small pieces, loosely joined
851
+ * *Pragmatism*: Solve real problems simply
852
+ * *Evolution*: Learn and improve continuously
853
+
854
+ These principles guide all design decisions and help Fractor remain simple, safe, and effective for parallel processing in Ruby.
855
+
856
+ == Next steps
857
+
858
+ * Read link:core-concepts/[Core Concepts] for component details
859
+ * Read link:../architecture/[Architecture Guide] for system design
860
+ * Try link:../guides/pipeline-mode/[Pipeline Mode] to see principles in action
861
+ * Try link:../guides/continuous-mode/[Continuous Mode] for long-running services
862
+ * Study link:../reference/examples/[Examples] to see patterns applied