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.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +227 -102
- data/README.adoc +113 -1940
- data/docs/.lycheeignore +16 -0
- data/docs/Gemfile +24 -0
- data/docs/README.md +157 -0
- data/docs/_config.yml +151 -0
- data/docs/_features/error-handling.adoc +1192 -0
- data/docs/_features/index.adoc +80 -0
- data/docs/_features/monitoring.adoc +589 -0
- data/docs/_features/signal-handling.adoc +202 -0
- data/docs/_features/workflows.adoc +1235 -0
- data/docs/_guides/continuous-mode.adoc +736 -0
- data/docs/_guides/cookbook.adoc +1133 -0
- data/docs/_guides/index.adoc +55 -0
- data/docs/_guides/pipeline-mode.adoc +730 -0
- data/docs/_guides/troubleshooting.adoc +358 -0
- data/docs/_pages/architecture.adoc +1390 -0
- data/docs/_pages/core-concepts.adoc +1392 -0
- data/docs/_pages/design-principles.adoc +862 -0
- data/docs/_pages/getting-started.adoc +290 -0
- data/docs/_pages/installation.adoc +143 -0
- data/docs/_reference/api.adoc +1080 -0
- data/docs/_reference/error-reporting.adoc +670 -0
- data/docs/_reference/examples.adoc +181 -0
- data/docs/_reference/index.adoc +96 -0
- data/docs/_reference/troubleshooting.adoc +862 -0
- data/docs/_tutorials/complex-workflows.adoc +1022 -0
- data/docs/_tutorials/data-processing-pipeline.adoc +740 -0
- data/docs/_tutorials/first-application.adoc +384 -0
- data/docs/_tutorials/index.adoc +48 -0
- data/docs/_tutorials/long-running-services.adoc +931 -0
- data/docs/assets/images/favicon-16.png +0 -0
- data/docs/assets/images/favicon-32.png +0 -0
- data/docs/assets/images/favicon-48.png +0 -0
- data/docs/assets/images/favicon.ico +0 -0
- data/docs/assets/images/favicon.png +0 -0
- data/docs/assets/images/favicon.svg +45 -0
- data/docs/assets/images/fractor-icon.svg +49 -0
- data/docs/assets/images/fractor-logo.svg +61 -0
- data/docs/index.adoc +131 -0
- data/docs/lychee.toml +39 -0
- data/examples/api_aggregator/README.adoc +627 -0
- data/examples/api_aggregator/api_aggregator.rb +376 -0
- data/examples/auto_detection/README.adoc +407 -29
- data/examples/continuous_chat_common/message_protocol.rb +1 -1
- data/examples/error_reporting.rb +207 -0
- data/examples/file_processor/README.adoc +170 -0
- data/examples/file_processor/file_processor.rb +615 -0
- data/examples/file_processor/sample_files/invalid.csv +1 -0
- data/examples/file_processor/sample_files/orders.xml +24 -0
- data/examples/file_processor/sample_files/products.json +23 -0
- data/examples/file_processor/sample_files/users.csv +6 -0
- data/examples/hierarchical_hasher/README.adoc +629 -41
- data/examples/image_processor/README.adoc +610 -0
- data/examples/image_processor/image_processor.rb +349 -0
- data/examples/image_processor/processed_images/sample_10_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_1_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_2_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_3_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_4_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_5_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_6_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_7_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_8_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_9_processed.jpg.json +12 -0
- data/examples/image_processor/test_images/sample_1.png +1 -0
- data/examples/image_processor/test_images/sample_10.png +1 -0
- data/examples/image_processor/test_images/sample_2.png +1 -0
- data/examples/image_processor/test_images/sample_3.png +1 -0
- data/examples/image_processor/test_images/sample_4.png +1 -0
- data/examples/image_processor/test_images/sample_5.png +1 -0
- data/examples/image_processor/test_images/sample_6.png +1 -0
- data/examples/image_processor/test_images/sample_7.png +1 -0
- data/examples/image_processor/test_images/sample_8.png +1 -0
- data/examples/image_processor/test_images/sample_9.png +1 -0
- data/examples/log_analyzer/README.adoc +662 -0
- data/examples/log_analyzer/log_analyzer.rb +579 -0
- data/examples/log_analyzer/sample_logs/apache.log +20 -0
- data/examples/log_analyzer/sample_logs/json.log +15 -0
- data/examples/log_analyzer/sample_logs/nginx.log +15 -0
- data/examples/log_analyzer/sample_logs/rails.log +29 -0
- data/examples/multi_work_type/README.adoc +576 -26
- data/examples/performance_monitoring.rb +120 -0
- data/examples/pipeline_processing/README.adoc +740 -26
- data/examples/pipeline_processing/pipeline_processing.rb +2 -2
- data/examples/priority_work_example.rb +155 -0
- data/examples/producer_subscriber/README.adoc +889 -46
- data/examples/scatter_gather/README.adoc +829 -27
- data/examples/simple/README.adoc +347 -0
- data/examples/specialized_workers/README.adoc +622 -26
- data/examples/specialized_workers/specialized_workers.rb +44 -8
- data/examples/stream_processor/README.adoc +206 -0
- data/examples/stream_processor/stream_processor.rb +284 -0
- data/examples/web_scraper/README.adoc +625 -0
- data/examples/web_scraper/web_scraper.rb +285 -0
- data/examples/workflow/README.adoc +406 -0
- data/examples/workflow/circuit_breaker/README.adoc +360 -0
- data/examples/workflow/circuit_breaker/circuit_breaker_workflow.rb +225 -0
- data/examples/workflow/conditional/README.adoc +483 -0
- data/examples/workflow/conditional/conditional_workflow.rb +215 -0
- data/examples/workflow/dead_letter_queue/README.adoc +374 -0
- data/examples/workflow/dead_letter_queue/dead_letter_queue_workflow.rb +217 -0
- data/examples/workflow/fan_out/README.adoc +381 -0
- data/examples/workflow/fan_out/fan_out_workflow.rb +202 -0
- data/examples/workflow/retry/README.adoc +248 -0
- data/examples/workflow/retry/retry_workflow.rb +195 -0
- data/examples/workflow/simple_linear/README.adoc +267 -0
- data/examples/workflow/simple_linear/simple_linear_workflow.rb +175 -0
- data/examples/workflow/simplified/README.adoc +329 -0
- data/examples/workflow/simplified/simplified_workflow.rb +222 -0
- data/exe/fractor +10 -0
- data/lib/fractor/cli.rb +288 -0
- data/lib/fractor/configuration.rb +307 -0
- data/lib/fractor/continuous_server.rb +60 -65
- data/lib/fractor/error_formatter.rb +72 -0
- data/lib/fractor/error_report_generator.rb +152 -0
- data/lib/fractor/error_reporter.rb +244 -0
- data/lib/fractor/error_statistics.rb +147 -0
- data/lib/fractor/execution_tracer.rb +162 -0
- data/lib/fractor/logger.rb +230 -0
- data/lib/fractor/main_loop_handler.rb +406 -0
- data/lib/fractor/main_loop_handler3.rb +135 -0
- data/lib/fractor/main_loop_handler4.rb +299 -0
- data/lib/fractor/performance_metrics_collector.rb +181 -0
- data/lib/fractor/performance_monitor.rb +215 -0
- data/lib/fractor/performance_report_generator.rb +202 -0
- data/lib/fractor/priority_work.rb +93 -0
- data/lib/fractor/priority_work_queue.rb +189 -0
- data/lib/fractor/result_aggregator.rb +32 -0
- data/lib/fractor/shutdown_handler.rb +168 -0
- data/lib/fractor/signal_handler.rb +80 -0
- data/lib/fractor/supervisor.rb +382 -269
- data/lib/fractor/supervisor_logger.rb +88 -0
- data/lib/fractor/version.rb +1 -1
- data/lib/fractor/work.rb +12 -0
- data/lib/fractor/work_distribution_manager.rb +151 -0
- data/lib/fractor/work_queue.rb +20 -0
- data/lib/fractor/work_result.rb +181 -9
- data/lib/fractor/worker.rb +73 -0
- data/lib/fractor/workflow/builder.rb +210 -0
- data/lib/fractor/workflow/chain_builder.rb +169 -0
- data/lib/fractor/workflow/circuit_breaker.rb +183 -0
- data/lib/fractor/workflow/circuit_breaker_orchestrator.rb +208 -0
- data/lib/fractor/workflow/circuit_breaker_registry.rb +112 -0
- data/lib/fractor/workflow/dead_letter_queue.rb +334 -0
- data/lib/fractor/workflow/execution_hooks.rb +39 -0
- data/lib/fractor/workflow/execution_strategy.rb +225 -0
- data/lib/fractor/workflow/execution_trace.rb +134 -0
- data/lib/fractor/workflow/helpers.rb +191 -0
- data/lib/fractor/workflow/job.rb +290 -0
- data/lib/fractor/workflow/job_dependency_validator.rb +120 -0
- data/lib/fractor/workflow/logger.rb +110 -0
- data/lib/fractor/workflow/pre_execution_context.rb +193 -0
- data/lib/fractor/workflow/retry_config.rb +156 -0
- data/lib/fractor/workflow/retry_orchestrator.rb +184 -0
- data/lib/fractor/workflow/retry_strategy.rb +93 -0
- data/lib/fractor/workflow/structured_logger.rb +30 -0
- data/lib/fractor/workflow/type_compatibility_validator.rb +222 -0
- data/lib/fractor/workflow/visualizer.rb +211 -0
- data/lib/fractor/workflow/workflow_context.rb +132 -0
- data/lib/fractor/workflow/workflow_executor.rb +669 -0
- data/lib/fractor/workflow/workflow_result.rb +55 -0
- data/lib/fractor/workflow/workflow_validator.rb +295 -0
- data/lib/fractor/workflow.rb +333 -0
- data/lib/fractor/wrapped_ractor.rb +66 -101
- data/lib/fractor/wrapped_ractor3.rb +161 -0
- data/lib/fractor/wrapped_ractor4.rb +242 -0
- data/lib/fractor.rb +92 -4
- metadata +179 -6
- data/tests/sample.rb.bak +0 -309
- 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.
|