fractor 0.1.6 → 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 (172) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +227 -102
  3. data/README.adoc +113 -1940
  4. data/docs/.lycheeignore +16 -0
  5. data/docs/Gemfile +24 -0
  6. data/docs/README.md +157 -0
  7. data/docs/_config.yml +151 -0
  8. data/docs/_features/error-handling.adoc +1192 -0
  9. data/docs/_features/index.adoc +80 -0
  10. data/docs/_features/monitoring.adoc +589 -0
  11. data/docs/_features/signal-handling.adoc +202 -0
  12. data/docs/_features/workflows.adoc +1235 -0
  13. data/docs/_guides/continuous-mode.adoc +736 -0
  14. data/docs/_guides/cookbook.adoc +1133 -0
  15. data/docs/_guides/index.adoc +55 -0
  16. data/docs/_guides/pipeline-mode.adoc +730 -0
  17. data/docs/_guides/troubleshooting.adoc +358 -0
  18. data/docs/_pages/architecture.adoc +1390 -0
  19. data/docs/_pages/core-concepts.adoc +1392 -0
  20. data/docs/_pages/design-principles.adoc +862 -0
  21. data/docs/_pages/getting-started.adoc +290 -0
  22. data/docs/_pages/installation.adoc +143 -0
  23. data/docs/_reference/api.adoc +1080 -0
  24. data/docs/_reference/error-reporting.adoc +670 -0
  25. data/docs/_reference/examples.adoc +181 -0
  26. data/docs/_reference/index.adoc +96 -0
  27. data/docs/_reference/troubleshooting.adoc +862 -0
  28. data/docs/_tutorials/complex-workflows.adoc +1022 -0
  29. data/docs/_tutorials/data-processing-pipeline.adoc +740 -0
  30. data/docs/_tutorials/first-application.adoc +384 -0
  31. data/docs/_tutorials/index.adoc +48 -0
  32. data/docs/_tutorials/long-running-services.adoc +931 -0
  33. data/docs/assets/images/favicon-16.png +0 -0
  34. data/docs/assets/images/favicon-32.png +0 -0
  35. data/docs/assets/images/favicon-48.png +0 -0
  36. data/docs/assets/images/favicon.ico +0 -0
  37. data/docs/assets/images/favicon.png +0 -0
  38. data/docs/assets/images/favicon.svg +45 -0
  39. data/docs/assets/images/fractor-icon.svg +49 -0
  40. data/docs/assets/images/fractor-logo.svg +61 -0
  41. data/docs/index.adoc +131 -0
  42. data/docs/lychee.toml +39 -0
  43. data/examples/api_aggregator/README.adoc +627 -0
  44. data/examples/api_aggregator/api_aggregator.rb +376 -0
  45. data/examples/auto_detection/README.adoc +407 -29
  46. data/examples/continuous_chat_common/message_protocol.rb +1 -1
  47. data/examples/error_reporting.rb +207 -0
  48. data/examples/file_processor/README.adoc +170 -0
  49. data/examples/file_processor/file_processor.rb +615 -0
  50. data/examples/file_processor/sample_files/invalid.csv +1 -0
  51. data/examples/file_processor/sample_files/orders.xml +24 -0
  52. data/examples/file_processor/sample_files/products.json +23 -0
  53. data/examples/file_processor/sample_files/users.csv +6 -0
  54. data/examples/hierarchical_hasher/README.adoc +629 -41
  55. data/examples/image_processor/README.adoc +610 -0
  56. data/examples/image_processor/image_processor.rb +349 -0
  57. data/examples/image_processor/processed_images/sample_10_processed.jpg.json +12 -0
  58. data/examples/image_processor/processed_images/sample_1_processed.jpg.json +12 -0
  59. data/examples/image_processor/processed_images/sample_2_processed.jpg.json +12 -0
  60. data/examples/image_processor/processed_images/sample_3_processed.jpg.json +12 -0
  61. data/examples/image_processor/processed_images/sample_4_processed.jpg.json +12 -0
  62. data/examples/image_processor/processed_images/sample_5_processed.jpg.json +12 -0
  63. data/examples/image_processor/processed_images/sample_6_processed.jpg.json +12 -0
  64. data/examples/image_processor/processed_images/sample_7_processed.jpg.json +12 -0
  65. data/examples/image_processor/processed_images/sample_8_processed.jpg.json +12 -0
  66. data/examples/image_processor/processed_images/sample_9_processed.jpg.json +12 -0
  67. data/examples/image_processor/test_images/sample_1.png +1 -0
  68. data/examples/image_processor/test_images/sample_10.png +1 -0
  69. data/examples/image_processor/test_images/sample_2.png +1 -0
  70. data/examples/image_processor/test_images/sample_3.png +1 -0
  71. data/examples/image_processor/test_images/sample_4.png +1 -0
  72. data/examples/image_processor/test_images/sample_5.png +1 -0
  73. data/examples/image_processor/test_images/sample_6.png +1 -0
  74. data/examples/image_processor/test_images/sample_7.png +1 -0
  75. data/examples/image_processor/test_images/sample_8.png +1 -0
  76. data/examples/image_processor/test_images/sample_9.png +1 -0
  77. data/examples/log_analyzer/README.adoc +662 -0
  78. data/examples/log_analyzer/log_analyzer.rb +579 -0
  79. data/examples/log_analyzer/sample_logs/apache.log +20 -0
  80. data/examples/log_analyzer/sample_logs/json.log +15 -0
  81. data/examples/log_analyzer/sample_logs/nginx.log +15 -0
  82. data/examples/log_analyzer/sample_logs/rails.log +29 -0
  83. data/examples/multi_work_type/README.adoc +576 -26
  84. data/examples/performance_monitoring.rb +120 -0
  85. data/examples/pipeline_processing/README.adoc +740 -26
  86. data/examples/pipeline_processing/pipeline_processing.rb +2 -2
  87. data/examples/priority_work_example.rb +155 -0
  88. data/examples/producer_subscriber/README.adoc +889 -46
  89. data/examples/scatter_gather/README.adoc +829 -27
  90. data/examples/simple/README.adoc +347 -0
  91. data/examples/specialized_workers/README.adoc +622 -26
  92. data/examples/specialized_workers/specialized_workers.rb +44 -8
  93. data/examples/stream_processor/README.adoc +206 -0
  94. data/examples/stream_processor/stream_processor.rb +284 -0
  95. data/examples/web_scraper/README.adoc +625 -0
  96. data/examples/web_scraper/web_scraper.rb +285 -0
  97. data/examples/workflow/README.adoc +406 -0
  98. data/examples/workflow/circuit_breaker/README.adoc +360 -0
  99. data/examples/workflow/circuit_breaker/circuit_breaker_workflow.rb +225 -0
  100. data/examples/workflow/conditional/README.adoc +483 -0
  101. data/examples/workflow/conditional/conditional_workflow.rb +215 -0
  102. data/examples/workflow/dead_letter_queue/README.adoc +374 -0
  103. data/examples/workflow/dead_letter_queue/dead_letter_queue_workflow.rb +217 -0
  104. data/examples/workflow/fan_out/README.adoc +381 -0
  105. data/examples/workflow/fan_out/fan_out_workflow.rb +202 -0
  106. data/examples/workflow/retry/README.adoc +248 -0
  107. data/examples/workflow/retry/retry_workflow.rb +195 -0
  108. data/examples/workflow/simple_linear/README.adoc +267 -0
  109. data/examples/workflow/simple_linear/simple_linear_workflow.rb +175 -0
  110. data/examples/workflow/simplified/README.adoc +329 -0
  111. data/examples/workflow/simplified/simplified_workflow.rb +222 -0
  112. data/exe/fractor +10 -0
  113. data/lib/fractor/cli.rb +288 -0
  114. data/lib/fractor/configuration.rb +307 -0
  115. data/lib/fractor/continuous_server.rb +60 -65
  116. data/lib/fractor/error_formatter.rb +72 -0
  117. data/lib/fractor/error_report_generator.rb +152 -0
  118. data/lib/fractor/error_reporter.rb +244 -0
  119. data/lib/fractor/error_statistics.rb +147 -0
  120. data/lib/fractor/execution_tracer.rb +162 -0
  121. data/lib/fractor/logger.rb +230 -0
  122. data/lib/fractor/main_loop_handler.rb +406 -0
  123. data/lib/fractor/main_loop_handler3.rb +135 -0
  124. data/lib/fractor/main_loop_handler4.rb +299 -0
  125. data/lib/fractor/performance_metrics_collector.rb +181 -0
  126. data/lib/fractor/performance_monitor.rb +215 -0
  127. data/lib/fractor/performance_report_generator.rb +202 -0
  128. data/lib/fractor/priority_work.rb +93 -0
  129. data/lib/fractor/priority_work_queue.rb +189 -0
  130. data/lib/fractor/result_aggregator.rb +32 -0
  131. data/lib/fractor/shutdown_handler.rb +168 -0
  132. data/lib/fractor/signal_handler.rb +80 -0
  133. data/lib/fractor/supervisor.rb +382 -269
  134. data/lib/fractor/supervisor_logger.rb +88 -0
  135. data/lib/fractor/version.rb +1 -1
  136. data/lib/fractor/work.rb +12 -0
  137. data/lib/fractor/work_distribution_manager.rb +151 -0
  138. data/lib/fractor/work_queue.rb +20 -0
  139. data/lib/fractor/work_result.rb +181 -9
  140. data/lib/fractor/worker.rb +73 -0
  141. data/lib/fractor/workflow/builder.rb +210 -0
  142. data/lib/fractor/workflow/chain_builder.rb +169 -0
  143. data/lib/fractor/workflow/circuit_breaker.rb +183 -0
  144. data/lib/fractor/workflow/circuit_breaker_orchestrator.rb +208 -0
  145. data/lib/fractor/workflow/circuit_breaker_registry.rb +112 -0
  146. data/lib/fractor/workflow/dead_letter_queue.rb +334 -0
  147. data/lib/fractor/workflow/execution_hooks.rb +39 -0
  148. data/lib/fractor/workflow/execution_strategy.rb +225 -0
  149. data/lib/fractor/workflow/execution_trace.rb +134 -0
  150. data/lib/fractor/workflow/helpers.rb +191 -0
  151. data/lib/fractor/workflow/job.rb +290 -0
  152. data/lib/fractor/workflow/job_dependency_validator.rb +120 -0
  153. data/lib/fractor/workflow/logger.rb +110 -0
  154. data/lib/fractor/workflow/pre_execution_context.rb +193 -0
  155. data/lib/fractor/workflow/retry_config.rb +156 -0
  156. data/lib/fractor/workflow/retry_orchestrator.rb +184 -0
  157. data/lib/fractor/workflow/retry_strategy.rb +93 -0
  158. data/lib/fractor/workflow/structured_logger.rb +30 -0
  159. data/lib/fractor/workflow/type_compatibility_validator.rb +222 -0
  160. data/lib/fractor/workflow/visualizer.rb +211 -0
  161. data/lib/fractor/workflow/workflow_context.rb +132 -0
  162. data/lib/fractor/workflow/workflow_executor.rb +669 -0
  163. data/lib/fractor/workflow/workflow_result.rb +55 -0
  164. data/lib/fractor/workflow/workflow_validator.rb +295 -0
  165. data/lib/fractor/workflow.rb +333 -0
  166. data/lib/fractor/wrapped_ractor.rb +66 -101
  167. data/lib/fractor/wrapped_ractor3.rb +161 -0
  168. data/lib/fractor/wrapped_ractor4.rb +242 -0
  169. data/lib/fractor.rb +92 -4
  170. metadata +179 -6
  171. data/tests/sample.rb.bak +0 -309
  172. data/tests/sample_working.rb.bak +0 -209
@@ -0,0 +1,736 @@
1
+ ---
2
+ layout: default
3
+ title: Continuous mode (long-running servers)
4
+ nav_order: 2
5
+ ---
6
+ == Continuous mode (long-running servers)
7
+
8
+ == General
9
+
10
+ Continuous mode is designed for applications that need to run indefinitely, processing work items as they arrive.
11
+
12
+ Characteristics:
13
+
14
+ * Runs continuously without a predetermined end
15
+ * Processes work items dynamically as they become available
16
+ * Workers idle efficiently when no work is available
17
+ * Results are processed via callbacks, not batch collection
18
+ * Supports graceful shutdown and runtime monitoring
19
+
20
+ Common use cases:
21
+
22
+ * Chat servers and messaging systems
23
+ * Background job processors
24
+ * Real-time data stream processing
25
+ * Web servers handling concurrent requests
26
+ * Monitoring and alerting systems
27
+ * Event-driven architectures
28
+
29
+ == Quick start
30
+
31
+ === General
32
+
33
+ This quick start guide shows how to build a long-running server using Fractor's high-level primitives for continuous mode. These primitives eliminate boilerplate code for thread management, queuing, and results processing.
34
+
35
+ === Step 1: Create Work and Worker classes
36
+
37
+ Just like pipeline mode, you need Work and Worker classes:
38
+
39
+ [source,ruby]
40
+ ----
41
+ require 'fractor'
42
+
43
+ class MessageWork < Fractor::Work
44
+ def initialize(client_id, message)
45
+ super({ client_id: client_id, message: message })
46
+ end
47
+
48
+ def client_id
49
+ input[:client_id]
50
+ end
51
+
52
+ def message
53
+ input[:message]
54
+ end
55
+ end
56
+
57
+ class MessageWorker < Fractor::Worker
58
+ def process(work)
59
+ # Process the message
60
+ processed = "Echo: #{work.message}"
61
+
62
+ Fractor::WorkResult.new(
63
+ result: { client_id: work.client_id, response: processed },
64
+ work: work
65
+ )
66
+ rescue => e
67
+ Fractor::WorkResult.new(error: e.message, work: work)
68
+ end
69
+ end
70
+ ----
71
+
72
+ === Step 2: Set up WorkQueue
73
+
74
+ Create a thread-safe work queue that will hold incoming work items:
75
+
76
+ [source,ruby]
77
+ ----
78
+ # Create a thread-safe work queue
79
+ work_queue = Fractor::WorkQueue.new
80
+ ----
81
+
82
+ === Step 3: Set up ContinuousServer with callbacks
83
+
84
+ The ContinuousServer handles all the boilerplate: thread management, signal handling, and results processing.
85
+
86
+ [source,ruby]
87
+ ----
88
+ # Create the continuous server
89
+ server = Fractor::ContinuousServer.new(
90
+ worker_pools: [
91
+ { worker_class: MessageWorker, num_workers: 4 }
92
+ ],
93
+ work_queue: work_queue, # Auto-registers as work source
94
+ log_file: 'logs/server.log' # Optional logging
95
+ )
96
+
97
+ # Define how to handle successful results
98
+ server.on_result do |result|
99
+ client_id = result.result[:client_id]
100
+ response = result.result[:response]
101
+ puts "Sending to client #{client_id}: #{response}"
102
+ # Send response to client here
103
+ end
104
+
105
+ # Define how to handle errors
106
+ server.on_error do |error_result|
107
+ puts "Error processing work: #{error_result.error}"
108
+ end
109
+ ----
110
+
111
+ === Step 4: Run and add work dynamically
112
+
113
+ Start the server and add work items as they arrive:
114
+
115
+ [source,ruby]
116
+ ----
117
+ # Start the server in a background thread
118
+ server_thread = Thread.new { server.run }
119
+
120
+ # Your application can now push work items dynamically
121
+ # For example, when a client sends a message:
122
+ work_queue << MessageWork.new(client_id: 1, message: "Hello")
123
+ work_queue << MessageWork.new(client_id: 2, message: "World")
124
+
125
+ # The server runs indefinitely, processing work as it arrives
126
+ # Use Ctrl+C or send SIGTERM for graceful shutdown
127
+
128
+ # Or stop programmatically
129
+ sleep 10
130
+ server.stop
131
+ server_thread.join
132
+ ----
133
+
134
+ That's it! The ContinuousServer handles all thread management, signal handling, and graceful shutdown automatically.
135
+
136
+ == Continuous mode components
137
+
138
+ === General
139
+
140
+ This section describes the components and their detailed usage specifically for continuous mode (long-running servers). For pipeline mode, see the link:pipeline-mode/[Pipeline Mode] documentation.
141
+
142
+ Continuous mode offers two approaches: a low-level API for manual control, and high-level primitives that eliminate boilerplate code.
143
+
144
+ === Low-level components
145
+
146
+ ==== General
147
+
148
+ The low-level API provides manual control over continuous mode operation. This approach is useful when you need fine-grained control over threading, work sources, or results processing.
149
+
150
+ Use the low-level API when:
151
+
152
+ * You need custom thread management
153
+ * Your work source logic is complex
154
+ * You require precise control over the supervisor lifecycle
155
+ * You're integrating with existing thread pools or event loops
156
+
157
+ For most applications, the high-level primitives (described in the next section) are recommended as they eliminate significant boilerplate code.
158
+
159
+ ==== Supervisor with continuous_mode: true
160
+
161
+ To enable continuous mode, set the `continuous_mode` option:
162
+
163
+ [source,ruby]
164
+ ----
165
+ supervisor = Fractor::Supervisor.new(
166
+ worker_pools: [
167
+ { worker_class: MyWorker, num_workers: 2 }
168
+ ],
169
+ continuous_mode: true # Enable continuous mode
170
+ )
171
+ ----
172
+
173
+ ==== Work source callbacks
174
+
175
+ Register a callback that provides new work on demand:
176
+
177
+ [source,ruby]
178
+ ----
179
+ supervisor.register_work_source do
180
+ # Return nil or empty array if no work is available
181
+ # Return a work item or array of work items when available
182
+ items = get_next_work_items
183
+ if items && !items.empty?
184
+ # Convert to Work objects if needed
185
+ items.map { |item| MyWork.new(item) }
186
+ else
187
+ nil
188
+ end
189
+ end
190
+ ----
191
+
192
+ The callback is polled every 100ms by an internal timer thread.
193
+
194
+ ==== Manual thread management
195
+
196
+ You must manually manage threads and results processing:
197
+
198
+ [source,ruby]
199
+ ----
200
+ # Start supervisor in a background thread
201
+ supervisor_thread = Thread.new { supervisor.run }
202
+
203
+ # Start results processing thread
204
+ results_thread = Thread.new do
205
+ loop do
206
+ # Process results
207
+ while (result = supervisor.results.results.shift)
208
+ handle_result(result)
209
+ end
210
+
211
+ # Process errors
212
+ while (error = supervisor.results.errors.shift)
213
+ handle_error(error)
214
+ end
215
+
216
+ sleep 0.1
217
+ end
218
+ end
219
+
220
+ # Ensure cleanup on shutdown
221
+ begin
222
+ supervisor_thread.join
223
+ rescue Interrupt
224
+ supervisor.stop
225
+ ensure
226
+ results_thread.kill
227
+ supervisor_thread.join
228
+ end
229
+ ----
230
+
231
+ === High-level components
232
+
233
+ ==== General
234
+
235
+ Fractor provides high-level primitives that dramatically simplify continuous mode applications by eliminating boilerplate code.
236
+
237
+ These primitives solve common problems:
238
+
239
+ * *Thread management*: Automatic supervisor and results processing threads
240
+ * *Queue synchronization*: Thread-safe work queue with automatic integration
241
+ * *Results processing*: Callback-based handling instead of manual loops
242
+ * *Signal handling*: Built-in support for SIGINT, SIGTERM, SIGUSR1/SIGBREAK
243
+ * *Graceful shutdown*: Coordinated cleanup across all threads
244
+
245
+ Real-world benefits:
246
+
247
+ * The chat server example reduced from 279 lines to 167 lines (40% reduction)
248
+ * Eliminates ~112 lines of thread, queue, and signal handling boilerplate
249
+ * Simpler, more maintainable code with fewer error-prone details
250
+
251
+ ==== Fractor::WorkQueue
252
+
253
+ ===== Purpose and responsibilities
254
+
255
+ `Fractor::WorkQueue` provides a thread-safe queue for continuous mode applications. It handles work item storage and integrates automatically with the supervisor's work source mechanism.
256
+
257
+ ===== Thread-safety
258
+
259
+ The WorkQueue is *thread-safe* but not *Ractor-safe*:
260
+
261
+ * *Thread-safe*: Multiple threads can safely push work items concurrently
262
+ * *Not Ractor-safe*: The queue lives in the main process and cannot be shared across Ractor boundaries
263
+
264
+ This design is intentional. The WorkQueue operates in the main process where your application code runs. Work items are retrieved by the Supervisor (also in the main process) and then sent to worker Ractors.
265
+
266
+ .WorkQueue architecture
267
+ [source]
268
+ ----
269
+ Main Process
270
+ ├─→ Your application threads (push to WorkQueue)
271
+ ├─→ WorkQueue (thread-safe, lives here)
272
+ ├─→ Supervisor (polls WorkQueue)
273
+ │ └─→ Sends work to Worker Ractors
274
+ └─→ Worker Ractors (receive frozen/shareable work items)
275
+ ----
276
+
277
+ ===== Creating a WorkQueue
278
+
279
+ [source,ruby]
280
+ ----
281
+ work_queue = Fractor::WorkQueue.new
282
+ ----
283
+
284
+ ===== Adding work items
285
+
286
+ Use the `<<` operator for thread-safe push operations:
287
+
288
+ [source,ruby]
289
+ ----
290
+ # From any thread in your application
291
+ work_queue << MyWork.new(data)
292
+
293
+ # Thread-safe even from multiple threads
294
+ threads = 10.times.map do |i|
295
+ Thread.new do
296
+ 100.times do |j|
297
+ work_queue << MyWork.new("thread-#{i}-item-#{j}")
298
+ end
299
+ end
300
+ end
301
+ threads.each(&:join)
302
+ ----
303
+
304
+ ===== Checking queue status
305
+
306
+ [source,ruby]
307
+ ----
308
+ # Check if queue is empty
309
+ if work_queue.empty?
310
+ puts "No work available"
311
+ end
312
+
313
+ # Get current queue size
314
+ puts "Queue has #{work_queue.size} items"
315
+ ----
316
+
317
+ ===== Integration with Supervisor
318
+
319
+ The WorkQueue integrates automatically with ContinuousServer (see next section). For manual integration with a Supervisor:
320
+
321
+ [source,ruby]
322
+ ----
323
+ supervisor = Fractor::Supervisor.new(
324
+ worker_pools: [{ worker_class: MyWorker }],
325
+ continuous_mode: true
326
+ )
327
+
328
+ # Register the work queue as a work source
329
+ work_queue.register_with_supervisor(supervisor)
330
+
331
+ # Now the supervisor will automatically poll the queue for work
332
+ ----
333
+
334
+ ==== Fractor::ContinuousServer
335
+
336
+ ===== Purpose and responsibilities
337
+
338
+ `Fractor::ContinuousServer` is a high-level wrapper that handles all the complexity of running a continuous mode application. It manages:
339
+
340
+ * Supervisor thread lifecycle
341
+ * Results processing thread with callback system
342
+ * Signal handling (SIGINT, SIGTERM, SIGUSR1/SIGBREAK)
343
+ * Graceful shutdown coordination
344
+ * Optional logging
345
+
346
+ ===== Creating a ContinuousServer
347
+
348
+ [source,ruby]
349
+ ----
350
+ server = Fractor::ContinuousServer.new(
351
+ worker_pools: [
352
+ { worker_class: MessageWorker, num_workers: 4 }
353
+ ],
354
+ work_queue: work_queue, # Optional, auto-registers if provided
355
+ log_file: 'logs/server.log' # Optional
356
+ )
357
+ ----
358
+
359
+ Parameters:
360
+
361
+ * `worker_pools` (required): Array of worker pool configurations
362
+ * `work_queue` (optional): A Fractor::WorkQueue instance to auto-register
363
+ * `log_file` (optional): Path for log output
364
+
365
+ ===== Registering callbacks
366
+
367
+ Define how to handle results and errors:
368
+
369
+ [source,ruby]
370
+ ----
371
+ # Handle successful results
372
+ server.on_result do |result|
373
+ # result is a Fractor::WorkResult with result.result containing your data
374
+ puts "Success: #{result.result}"
375
+ # Send response to client, update database, etc.
376
+ end
377
+
378
+ # Handle errors
379
+ server.on_error do |error_result|
380
+ # error_result is a Fractor::WorkResult with error_result.error containing the message
381
+ puts "Error: #{error_result.error}"
382
+ # Log error, send notification, etc.
383
+ end
384
+ ----
385
+
386
+ ===== Running the server
387
+
388
+ [source,ruby]
389
+ ----
390
+ # Blocking: Run the server (blocks until shutdown signal)
391
+ server.run
392
+
393
+ # Non-blocking: Run in background thread
394
+ server_thread = Thread.new { server.run }
395
+
396
+ # Your application continues here...
397
+ # Add work to queue as needed
398
+ work_queue << MyWork.new(data)
399
+
400
+ # Later, stop the server
401
+ server.stop
402
+ server_thread.join
403
+ ----
404
+
405
+ ===== Signal handling
406
+
407
+ The ContinuousServer automatically handles:
408
+
409
+ * *SIGINT* (Ctrl+C): Graceful shutdown
410
+ * *SIGTERM*: Graceful shutdown (production deployment)
411
+ * *SIGUSR1* (Unix) / *SIGBREAK* (Windows): Status output
412
+
413
+ No additional code needed - signals work automatically.
414
+
415
+ ===== Graceful shutdown
416
+
417
+ When a shutdown signal is received:
418
+
419
+ . Stops accepting new work from the work queue
420
+ . Allows in-progress work to complete (within ~2 seconds)
421
+ . Processes remaining results through callbacks
422
+ . Cleans up all threads and resources
423
+ . Returns from the `run` method
424
+
425
+ ===== Programmatic shutdown
426
+
427
+ [source,ruby]
428
+ ----
429
+ # Stop the server programmatically
430
+ server.stop
431
+
432
+ # The run method will return shortly after
433
+ ----
434
+
435
+ ==== Integration architecture
436
+
437
+ The high-level components work together seamlessly:
438
+
439
+ .Complete architecture diagram
440
+ [source]
441
+ ----
442
+ ┌───────────────────────────────────────────────────────────┐
443
+ │ Main Process │
444
+ │ │
445
+ │ ┌──────────────┐ ┌──────────────────────────────┐ │
446
+ │ │ Your App │────>│ WorkQueue (thread-safe) │ │
447
+ │ │ (any thread) │ │ - Thread::Queue internally │ │
448
+ │ └──────────────┘ └──────────────────────────────┘ │
449
+ │ │ │
450
+ │ │ polled every 100ms │
451
+ │ ▼ │
452
+ │ ┌────────────────────────────────────────────────────┐ │
453
+ │ │ ContinuousServer │ │
454
+ │ │ ┌─────────────────────────────────────────────┐ │ │
455
+ │ │ │ Supervisor Thread │ │ │
456
+ │ │ │ - Manages worker Ractors │ │ │
457
+ │ │ │ - Distributes work │ │ │
458
+ │ │ │ - Coordinates shutdown │ │ │
459
+ │ │ └─────────────────────────────────────────────┘ │ │
460
+ │ │ │ │ │
461
+ │ │ ▼ │ │
462
+ │ │ ┌─────────────────────────────────────────────┐ │ │
463
+ │ │ │ Worker Ractors (parallel execution) │ │ │
464
+ │ │ │ - Ractor 1: WorkerInstance.process(work) │ │ │
465
+ │ │ │ - Ractor 2: WorkerInstance.process(work) │ │ │
466
+ │ │ │ - Ractor N: WorkerInstance.process(work) │ │ │
467
+ │ │ └─────────────────────────────────────────────┘ │ │
468
+ │ │ │ │ │
469
+ │ │ ▼ (WorkResults) │ │
470
+ │ │ ┌─────────────────────────────────────────────┐ │ │
471
+ │ │ │ Results Processing Thread │ │ │
472
+ │ │ │ - on_result callback for successes │ │ │
473
+ │ │ │ - on_error callback for failures │ │ │
474
+ │ │ └─────────────────────────────────────────────┘ │ │
475
+ │ │ │ │
476
+ │ │ ┌─────────────────────────────────────────────┐ │ │
477
+ │ │ │ Signal Handler Thread │ │ │
478
+ │ │ │ - SIGINT/SIGTERM: Shutdown │ │ │
479
+ │ │ │ - SIGUSR1/SIGBREAK: Status │ │ │
480
+ │ │ └─────────────────────────────────────────────┘ │ │
481
+ │ └────────────────────────────────────────────────────┘ │
482
+ └───────────────────────────────────────────────────────────┘
483
+ ----
484
+
485
+ Key points:
486
+
487
+ * WorkQueue lives in main process (thread-safe, not Ractor-safe)
488
+ * Supervisor polls WorkQueue and distributes to Ractors
489
+ * Work items must be frozen/shareable to cross Ractor boundary
490
+ * Results come back through callbacks, not batch collection
491
+ * All thread management is automatic
492
+
493
+ == Continuous mode patterns
494
+
495
+ === Basic server with callbacks
496
+
497
+ The most common pattern uses WorkQueue + ContinuousServer:
498
+
499
+ [source,ruby]
500
+ ----
501
+ require 'fractor'
502
+
503
+ # Define work and worker
504
+ class RequestWork < Fractor::Work
505
+ def initialize(request_id, data)
506
+ super({ request_id: request_id, data: data })
507
+ end
508
+ end
509
+
510
+ class RequestWorker < Fractor::Worker
511
+ def process(work)
512
+ # Process the request
513
+ result = perform_computation(work.input[:data])
514
+
515
+ Fractor::WorkResult.new(
516
+ result: { request_id: work.input[:request_id], response: result },
517
+ work: work
518
+ )
519
+ rescue => e
520
+ Fractor::WorkResult.new(error: e.message, work: work)
521
+ end
522
+
523
+ private
524
+
525
+ def perform_computation(data)
526
+ # Your business logic here
527
+ data.upcase
528
+ end
529
+ end
530
+
531
+ # Set up server
532
+ work_queue = Fractor::WorkQueue.new
533
+
534
+ server = Fractor::ContinuousServer.new(
535
+ worker_pools: [{ worker_class: RequestWorker, num_workers: 4 }],
536
+ work_queue: work_queue
537
+ )
538
+
539
+ server.on_result { |result| puts "Success: #{result.result}" }
540
+ server.on_error { |error| puts "Error: #{error.error}" }
541
+
542
+ # Run server (blocks until shutdown)
543
+ Thread.new { server.run }
544
+
545
+ # Application logic adds work as needed
546
+ work_queue << RequestWork.new(1, "hello")
547
+ work_queue << RequestWork.new(2, "world")
548
+
549
+ sleep # Keep main thread alive
550
+ ----
551
+
552
+ === Event-driven processing
553
+
554
+ Process events from external sources as they arrive:
555
+
556
+ [source,ruby]
557
+ ----
558
+ # Event source (could be webhooks, message queue, etc.)
559
+ event_source = EventSource.new
560
+
561
+ # Set up work queue and server
562
+ work_queue = Fractor::WorkQueue.new
563
+ server = Fractor::ContinuousServer.new(
564
+ worker_pools: [{ worker_class: EventWorker, num_workers: 8 }],
565
+ work_queue: work_queue
566
+ )
567
+
568
+ server.on_result do |result|
569
+ # Publish result to subscribers
570
+ publish_event(result.result)
571
+ end
572
+
573
+ # Event loop adds work to queue
574
+ event_source.on_event do |event|
575
+ work_queue << EventWork.new(event)
576
+ end
577
+
578
+ # Start server
579
+ server.run
580
+ ----
581
+
582
+ === Dynamic work sources
583
+
584
+ Combine multiple work sources:
585
+
586
+ [source,ruby]
587
+ ----
588
+ work_queue = Fractor::WorkQueue.new
589
+
590
+ # Source 1: HTTP requests
591
+ http_server.on_request do |request|
592
+ work_queue << HttpWork.new(request)
593
+ end
594
+
595
+ # Source 2: Message queue
596
+ message_queue.subscribe do |message|
597
+ work_queue << MessageWork.new(message)
598
+ end
599
+
600
+ # Source 3: Scheduled tasks
601
+ scheduler.every('1m') do
602
+ work_queue << ScheduledWork.new(Time.now)
603
+ end
604
+
605
+ # Single server processes all work types
606
+ server = Fractor::ContinuousServer.new(
607
+ worker_pools: [
608
+ { worker_class: HttpWorker, num_workers: 4 },
609
+ { worker_class: MessageWorker, num_workers: 2 },
610
+ { worker_class: ScheduledWorker, num_workers: 1 }
611
+ ],
612
+ work_queue: work_queue
613
+ )
614
+
615
+ server.run
616
+ ----
617
+
618
+ === Graceful shutdown strategies
619
+
620
+ ==== Signal-based shutdown (production)
621
+
622
+ [source,ruby]
623
+ ----
624
+ # Server automatically handles SIGTERM
625
+ server = Fractor::ContinuousServer.new(
626
+ worker_pools: [{ worker_class: MyWorker }],
627
+ work_queue: work_queue,
628
+ log_file: '/var/log/myapp/server.log'
629
+ )
630
+
631
+ # Just run the server - signals handled automatically
632
+ server.run
633
+
634
+ # In production:
635
+ # systemctl stop myapp # Sends SIGTERM
636
+ # docker stop container # Sends SIGTERM
637
+ # kill -TERM <pid> # Manual SIGTERM
638
+ ----
639
+
640
+ ==== Time-based shutdown
641
+
642
+ [source,ruby]
643
+ ----
644
+ server_thread = Thread.new { server.run }
645
+
646
+ # Run for specific duration
647
+ sleep 3600 # Run for 1 hour
648
+ server.stop
649
+ server_thread.join
650
+ ----
651
+
652
+ ==== Condition-based shutdown
653
+
654
+ [source,ruby]
655
+ ----
656
+ server_thread = Thread.new { server.run }
657
+
658
+ # Monitor thread checks conditions
659
+ monitor = Thread.new do
660
+ loop do
661
+ if should_shutdown?
662
+ server.stop
663
+ break
664
+ end
665
+ sleep 10
666
+ end
667
+ end
668
+
669
+ server_thread.join
670
+ monitor.kill
671
+ ----
672
+
673
+ === Before/after comparison
674
+
675
+ The chat server example demonstrates the real-world impact of using the high-level primitives.
676
+
677
+ ==== Before: Low-level API (279 lines)
678
+
679
+ Required manual management of:
680
+
681
+ * Supervisor thread creation and lifecycle (~15 lines)
682
+ * Results processing thread with loops (~50 lines)
683
+ * Queue creation and synchronization (~10 lines)
684
+ * Signal handling setup (~15 lines)
685
+ * Thread coordination and shutdown (~20 lines)
686
+ * IO.select event loop (~110 lines)
687
+ * Manual error handling throughout (~59 lines)
688
+
689
+ ==== After: High-level primitives (167 lines)
690
+
691
+ Eliminated boilerplate:
692
+
693
+ * WorkQueue handles queue and synchronization (automatic)
694
+ * ContinuousServer manages all threads (automatic)
695
+ * Callbacks replace manual results loops (automatic)
696
+ * Signal handling built-in (automatic)
697
+ * Graceful shutdown coordinated (automatic)
698
+
699
+ Result: *40% code reduction* (112 fewer lines), simpler architecture, fewer error-prone details.
700
+
701
+ See link:../examples/continuous_chat_fractor/chat_server.rb[the refactored chat server] for the complete example.
702
+
703
+ == Continuous mode examples
704
+
705
+ === Plain socket implementation
706
+
707
+ The plain socket implementation (link:../examples/continuous_chat_server/[examples/continuous_chat_server/]) provides a baseline chat server using plain TCP sockets without Fractor. This serves as a comparison point to understand the benefits of using Fractor for continuous processing.
708
+
709
+ === Fractor-based implementation
710
+
711
+ The Fractor-based implementation (link:../examples/continuous_chat_fractor/[examples/continuous_chat_fractor/]) demonstrates how to build a production-ready chat server using Fractor's continuous mode with high-level primitives.
712
+
713
+ Key features:
714
+
715
+ * *Continuous mode operation*: Server runs indefinitely processing messages as they arrive
716
+ * *High-level primitives*: Uses WorkQueue and ContinuousServer to eliminate boilerplate
717
+ * *Graceful shutdown*: Production-ready signal handling (SIGINT, SIGTERM, SIGUSR1/SIGBREAK)
718
+ * *Callback-based results*: Clean separation of concerns with on_result and on_error callbacks
719
+ * *Cross-platform support*: Works on Unix/Linux/macOS and Windows
720
+ * *Process monitoring*: Runtime status checking via signals
721
+ * *40% code reduction*: 167 lines vs 279 lines with low-level API
722
+
723
+ The implementation includes:
724
+
725
+ * `chat_common.rb`: Work and Worker class definitions for chat message processing
726
+ * `chat_server.rb`: Main server using high-level primitives
727
+ * `simulate.rb`: Test client simulator
728
+
729
+ This example demonstrates production deployment patterns including:
730
+
731
+ * Systemd service integration
732
+ * Docker container deployment
733
+ * Process monitoring and health checks
734
+ * Graceful restart procedures
735
+
736
+ See link:../examples/continuous_chat_fractor/README/[the chat server README] for detailed implementation documentation.