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
|
@@ -5,11 +5,14 @@ require_relative "../../lib/fractor"
|
|
|
5
5
|
module SpecializedWorkers
|
|
6
6
|
# First work type: Compute-intensive operations
|
|
7
7
|
class ComputeWork < Fractor::Work
|
|
8
|
+
attr_reader :work_type
|
|
9
|
+
|
|
8
10
|
def initialize(data, operation = :default, parameters = {})
|
|
9
11
|
super({
|
|
10
12
|
data: data,
|
|
11
13
|
operation: operation,
|
|
12
14
|
parameters: parameters,
|
|
15
|
+
work_type: :compute, # Add work type identifier for Ractor compatibility
|
|
13
16
|
})
|
|
14
17
|
end
|
|
15
18
|
|
|
@@ -25,6 +28,10 @@ module SpecializedWorkers
|
|
|
25
28
|
input[:parameters]
|
|
26
29
|
end
|
|
27
30
|
|
|
31
|
+
def work_type
|
|
32
|
+
input[:work_type]
|
|
33
|
+
end
|
|
34
|
+
|
|
28
35
|
def to_s
|
|
29
36
|
"ComputeWork: operation=#{operation}, parameters=#{parameters}"
|
|
30
37
|
end
|
|
@@ -32,6 +39,8 @@ module SpecializedWorkers
|
|
|
32
39
|
|
|
33
40
|
# Second work type: Database operations
|
|
34
41
|
class DatabaseWork < Fractor::Work
|
|
42
|
+
attr_reader :work_type
|
|
43
|
+
|
|
35
44
|
def initialize(data = "", query_type = :select, table = "unknown",
|
|
36
45
|
conditions = {})
|
|
37
46
|
super({
|
|
@@ -39,6 +48,7 @@ conditions = {})
|
|
|
39
48
|
query_type: query_type,
|
|
40
49
|
table: table,
|
|
41
50
|
conditions: conditions,
|
|
51
|
+
work_type: :database, # Add work type identifier for Ractor compatibility
|
|
42
52
|
})
|
|
43
53
|
end
|
|
44
54
|
|
|
@@ -58,6 +68,10 @@ conditions = {})
|
|
|
58
68
|
input[:conditions]
|
|
59
69
|
end
|
|
60
70
|
|
|
71
|
+
def work_type
|
|
72
|
+
input[:work_type]
|
|
73
|
+
end
|
|
74
|
+
|
|
61
75
|
def to_s
|
|
62
76
|
"DatabaseWork: query_type=#{query_type}, table=#{table}, conditions=#{conditions}"
|
|
63
77
|
end
|
|
@@ -65,14 +79,16 @@ conditions = {})
|
|
|
65
79
|
|
|
66
80
|
# First worker type: Handles compute-intensive operations
|
|
67
81
|
class ComputeWorker < Fractor::Worker
|
|
68
|
-
def initialize
|
|
82
|
+
def initialize(name: nil, **options)
|
|
83
|
+
super
|
|
69
84
|
# Setup resources needed for computation
|
|
70
|
-
|
|
85
|
+
# Use Ractor.make_shareable to make the hash shareable across Ractors
|
|
86
|
+
@compute_resources = Ractor.make_shareable({ memory: 1024, cpu_cores: 4 })
|
|
71
87
|
end
|
|
72
88
|
|
|
73
89
|
def process(work)
|
|
74
|
-
# Only handle ComputeWork
|
|
75
|
-
unless work.
|
|
90
|
+
# Only handle ComputeWork - check work_type for Ractor compatibility
|
|
91
|
+
unless work.respond_to?(:work_type) && work.work_type == :compute
|
|
76
92
|
return Fractor::WorkResult.new(
|
|
77
93
|
error: "ComputeWorker can only process ComputeWork, got: #{work.class}",
|
|
78
94
|
work: work,
|
|
@@ -133,14 +149,16 @@ conditions = {})
|
|
|
133
149
|
|
|
134
150
|
# Second worker type: Handles database operations
|
|
135
151
|
class DatabaseWorker < Fractor::Worker
|
|
136
|
-
def initialize
|
|
152
|
+
def initialize(name: nil, **options)
|
|
153
|
+
super
|
|
137
154
|
# Setup database connection and resources
|
|
138
|
-
|
|
155
|
+
# Use Ractor.make_shareable to make the hash shareable across Ractors
|
|
156
|
+
@db_connection = Ractor.make_shareable({ pool_size: 5, timeout: 30 })
|
|
139
157
|
end
|
|
140
158
|
|
|
141
159
|
def process(work)
|
|
142
|
-
# Only handle DatabaseWork
|
|
143
|
-
unless work.
|
|
160
|
+
# Only handle DatabaseWork - check work_type for Ractor compatibility
|
|
161
|
+
unless work.respond_to?(:work_type) && work.work_type == :database
|
|
144
162
|
return Fractor::WorkResult.new(
|
|
145
163
|
error: "DatabaseWorker can only process DatabaseWork, got: #{work.class}",
|
|
146
164
|
work: work,
|
|
@@ -255,14 +273,20 @@ conditions = {})
|
|
|
255
273
|
compute_work_items = compute_tasks.map do |task|
|
|
256
274
|
ComputeWork.new(task[:data], task[:operation], task[:parameters])
|
|
257
275
|
end
|
|
276
|
+
puts "Created #{compute_work_items.size} compute work items"
|
|
277
|
+
puts "First compute work item: #{compute_work_items.first.inspect}" if compute_work_items.any?
|
|
258
278
|
@compute_supervisor.add_work_items(compute_work_items)
|
|
279
|
+
puts "Added compute work items to supervisor"
|
|
259
280
|
|
|
260
281
|
# Create and add database work items
|
|
261
282
|
db_work_items = db_tasks.map do |task|
|
|
262
283
|
DatabaseWork.new(task[:data], task[:query_type], task[:table],
|
|
263
284
|
task[:conditions])
|
|
264
285
|
end
|
|
286
|
+
puts "Created #{db_work_items.size} database work items"
|
|
287
|
+
puts "First db work item: #{db_work_items.first.inspect}" if db_work_items.any?
|
|
265
288
|
@db_supervisor.add_work_items(db_work_items)
|
|
289
|
+
puts "Added database work items to supervisor"
|
|
266
290
|
|
|
267
291
|
# Run the supervisors directly - this is more reliable
|
|
268
292
|
@compute_supervisor.run
|
|
@@ -273,7 +297,19 @@ conditions = {})
|
|
|
273
297
|
db_results_agg = @db_supervisor.results
|
|
274
298
|
|
|
275
299
|
puts "Received compute results: #{compute_results_agg.results.size} items"
|
|
300
|
+
puts "Received compute errors: #{compute_results_agg.errors.size} items"
|
|
301
|
+
if compute_results_agg.errors.any?
|
|
302
|
+
compute_results_agg.errors.each do |e|
|
|
303
|
+
puts " Error: #{e.error}"
|
|
304
|
+
end
|
|
305
|
+
end
|
|
276
306
|
puts "Received database results: #{db_results_agg.results.size} items"
|
|
307
|
+
puts "Received database errors: #{db_results_agg.errors.size} items"
|
|
308
|
+
if db_results_agg.errors.any?
|
|
309
|
+
db_results_agg.errors.each do |e|
|
|
310
|
+
puts " Error: #{e.error}"
|
|
311
|
+
end
|
|
312
|
+
end
|
|
277
313
|
|
|
278
314
|
# Format and store results
|
|
279
315
|
@compute_results = format_compute_results(compute_results_agg)
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
= Real-Time Data Stream Processor Example
|
|
2
|
+
:toc:
|
|
3
|
+
:toclevels: 3
|
|
4
|
+
|
|
5
|
+
Real-time event stream processor using Fractor::ContinuousServer for processing continuous data streams with windowing, metrics tracking, and graceful shutdown.
|
|
6
|
+
|
|
7
|
+
== Purpose
|
|
8
|
+
|
|
9
|
+
This example demonstrates:
|
|
10
|
+
|
|
11
|
+
* Continuous event processing with Fractor::ContinuousServer
|
|
12
|
+
* Sliding window aggregation (5-second windows)
|
|
13
|
+
* Real-time metrics tracking
|
|
14
|
+
* Backpressure handling
|
|
15
|
+
* Graceful shutdown with SIGINT/SIGTERM
|
|
16
|
+
* Performance monitoring
|
|
17
|
+
|
|
18
|
+
== Features
|
|
19
|
+
|
|
20
|
+
=== Continuous Processing
|
|
21
|
+
|
|
22
|
+
Processes events as they arrive without stopping:
|
|
23
|
+
|
|
24
|
+
* Non-blocking event ingestion
|
|
25
|
+
* Parallel processing across workers
|
|
26
|
+
* Result collection in background thread
|
|
27
|
+
* Automatic workload distribution
|
|
28
|
+
|
|
29
|
+
=== Sliding Window Aggregation
|
|
30
|
+
|
|
31
|
+
Maintains a sliding time window of recent events:
|
|
32
|
+
|
|
33
|
+
* Configurable window size (default: 5 seconds)
|
|
34
|
+
* Automatic cleanup of old events
|
|
35
|
+
* Window-based metrics calculation
|
|
36
|
+
* Real-time window size tracking
|
|
37
|
+
|
|
38
|
+
=== Performance Metrics
|
|
39
|
+
|
|
40
|
+
Tracks and displays real-time metrics:
|
|
41
|
+
|
|
42
|
+
* Total events processed
|
|
43
|
+
* Events per second
|
|
44
|
+
* Average processing time
|
|
45
|
+
* Current window size
|
|
46
|
+
|
|
47
|
+
=== Graceful Shutdown
|
|
48
|
+
|
|
49
|
+
Handles termination signals properly:
|
|
50
|
+
|
|
51
|
+
* SIGINT (Ctrl+C) for manual shutdown
|
|
52
|
+
* SIGTERM for system shutdown
|
|
53
|
+
* Preserves in-flight work
|
|
54
|
+
* Final summary report
|
|
55
|
+
|
|
56
|
+
== Usage
|
|
57
|
+
|
|
58
|
+
=== Basic Usage
|
|
59
|
+
|
|
60
|
+
Run with default settings (30 seconds, 10 events/second):
|
|
61
|
+
|
|
62
|
+
[source,bash]
|
|
63
|
+
----
|
|
64
|
+
ruby stream_processor.rb
|
|
65
|
+
----
|
|
66
|
+
|
|
67
|
+
=== Custom Duration
|
|
68
|
+
|
|
69
|
+
Process for 60 seconds:
|
|
70
|
+
|
|
71
|
+
[source,bash]
|
|
72
|
+
----
|
|
73
|
+
ruby stream_processor.rb -d 60
|
|
74
|
+
----
|
|
75
|
+
|
|
76
|
+
=== Custom Event Rate
|
|
77
|
+
|
|
78
|
+
Generate 50 events per second:
|
|
79
|
+
|
|
80
|
+
[source,bash]
|
|
81
|
+
----
|
|
82
|
+
ruby stream_processor.rb -r 50
|
|
83
|
+
----
|
|
84
|
+
|
|
85
|
+
=== Custom Workers
|
|
86
|
+
|
|
87
|
+
Use 8 worker ractors:
|
|
88
|
+
|
|
89
|
+
[source,bash]
|
|
90
|
+
----
|
|
91
|
+
ruby stream_processor.rb -w 8
|
|
92
|
+
----
|
|
93
|
+
|
|
94
|
+
=== Custom Window Size
|
|
95
|
+
|
|
96
|
+
Use 10-second window:
|
|
97
|
+
|
|
98
|
+
[source,bash]
|
|
99
|
+
----
|
|
100
|
+
ruby stream_processor.rb --window 10
|
|
101
|
+
----
|
|
102
|
+
|
|
103
|
+
=== Combined Options
|
|
104
|
+
|
|
105
|
+
[source,bash]
|
|
106
|
+
----
|
|
107
|
+
ruby stream_processor.rb -w 8 -d 120 -r 100 --window 10
|
|
108
|
+
----
|
|
109
|
+
|
|
110
|
+
== Examples
|
|
111
|
+
|
|
112
|
+
=== Example 1: Default Settings
|
|
113
|
+
|
|
114
|
+
[source,bash]
|
|
115
|
+
----
|
|
116
|
+
$ ruby stream_processor.rb
|
|
117
|
+
|
|
118
|
+
Starting Stream Processor...
|
|
119
|
+
Window size: 5 seconds
|
|
120
|
+
Workers: 4
|
|
121
|
+
Press Ctrl+C to stop gracefully
|
|
122
|
+
|
|
123
|
+
Generating event stream for 30 seconds at 10 events/second...
|
|
124
|
+
|
|
125
|
+
Events: 300 | Processed: 298 | Rate: 9.93 e/s | Window: 47 | Avg Time: 0.12 ms
|
|
126
|
+
|
|
127
|
+
Generated 300 events
|
|
128
|
+
|
|
129
|
+
Stopping Stream Processor...
|
|
130
|
+
|
|
131
|
+
============================================================
|
|
132
|
+
FINAL SUMMARY
|
|
133
|
+
============================================================
|
|
134
|
+
Total Events: 300
|
|
135
|
+
Processed: 300
|
|
136
|
+
Errors: 0
|
|
137
|
+
Duration: 30.25 seconds
|
|
138
|
+
Average Rate: 9.92 events/second
|
|
139
|
+
============================================================
|
|
140
|
+
----
|
|
141
|
+
|
|
142
|
+
=== Example 2: High Throughput
|
|
143
|
+
|
|
144
|
+
[source,bash]
|
|
145
|
+
----
|
|
146
|
+
$ ruby stream_processor.rb -w 8 -r 100 -d 10
|
|
147
|
+
|
|
148
|
+
Starting Stream Processor...
|
|
149
|
+
Window size: 5 seconds
|
|
150
|
+
Workers: 8
|
|
151
|
+
|
|
152
|
+
Events: 1000 | Processed: 998 | Rate: 99.8 e/s | Window: 492 | Avg Time: 0.08 ms
|
|
153
|
+
|
|
154
|
+
Generated 1000 events
|
|
155
|
+
|
|
156
|
+
FINAL SUMMARY
|
|
157
|
+
============================================================
|
|
158
|
+
Total Events: 1000
|
|
159
|
+
Processed: 1000
|
|
160
|
+
Duration: 10.05 seconds
|
|
161
|
+
Average Rate: 99.50 events/second
|
|
162
|
+
----
|
|
163
|
+
|
|
164
|
+
=== Example 3: Graceful Shutdown
|
|
165
|
+
|
|
166
|
+
Press Ctrl+C during processing:
|
|
167
|
+
|
|
168
|
+
[source,bash]
|
|
169
|
+
----
|
|
170
|
+
$ ruby stream_processor.rb
|
|
171
|
+
|
|
172
|
+
Events: 150 | Processed: 148 | Rate: 10.1 e/s | Window: 48 | Avg Time: 0.10 ms
|
|
173
|
+
^C
|
|
174
|
+
Stopping Stream Processor...
|
|
175
|
+
|
|
176
|
+
FINAL SUMMARY
|
|
177
|
+
============================================================
|
|
178
|
+
Total Events: 150
|
|
179
|
+
Processed: 150
|
|
180
|
+
Duration: 14.87 seconds
|
|
181
|
+
Average Rate: 10.09 events/second
|
|
182
|
+
----
|
|
183
|
+
|
|
184
|
+
== Architecture
|
|
185
|
+
|
|
186
|
+
The processor consists of:
|
|
187
|
+
|
|
188
|
+
* **Event**: Work item representing a stream event
|
|
189
|
+
* **EventProcessorWorker**: Worker that processes events
|
|
190
|
+
* **StreamProcessor**: Main processor managing the continuous server
|
|
191
|
+
* **EventGenerator**: Test utility for generating event streams
|
|
192
|
+
|
|
193
|
+
== Testing
|
|
194
|
+
|
|
195
|
+
Run the test suite:
|
|
196
|
+
|
|
197
|
+
[source,bash]
|
|
198
|
+
----
|
|
199
|
+
bundle exec rspec spec/examples/stream_processor_spec.rb
|
|
200
|
+
----
|
|
201
|
+
|
|
202
|
+
== See Also
|
|
203
|
+
|
|
204
|
+
* link:../../README.adoc[Fractor Main Documentation]
|
|
205
|
+
* link:../../docs/continuous-mode.adoc[Continuous Mode Guide]
|
|
206
|
+
* link:../log_analyzer/README.adoc[Log Analyzer Example]
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../../lib/fractor"
|
|
5
|
+
require "json"
|
|
6
|
+
require "time"
|
|
7
|
+
|
|
8
|
+
# Event data structure
|
|
9
|
+
class Event < Fractor::Work
|
|
10
|
+
def initialize(type:, data:, timestamp: Time.now)
|
|
11
|
+
super({
|
|
12
|
+
type: type,
|
|
13
|
+
data: data,
|
|
14
|
+
timestamp: timestamp
|
|
15
|
+
})
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def type
|
|
19
|
+
input[:type]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def data
|
|
23
|
+
input[:data]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def timestamp
|
|
27
|
+
input[:timestamp]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def to_s
|
|
31
|
+
"Event(#{type}, #{timestamp.iso8601})"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Worker for processing events
|
|
36
|
+
class EventProcessorWorker < Fractor::Worker
|
|
37
|
+
def process(work)
|
|
38
|
+
return nil unless work.is_a?(Event)
|
|
39
|
+
|
|
40
|
+
# Process event
|
|
41
|
+
{
|
|
42
|
+
type: work.type,
|
|
43
|
+
data: work.data,
|
|
44
|
+
timestamp: work.timestamp,
|
|
45
|
+
processed_at: Time.now,
|
|
46
|
+
processing_time: (Time.now - work.timestamp) * 1000 # ms
|
|
47
|
+
}
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Real-time stream processor (simplified for testing)
|
|
52
|
+
class StreamProcessor
|
|
53
|
+
attr_reader :processed_count, :error_count, :window_size, :metrics
|
|
54
|
+
|
|
55
|
+
def initialize(window_size: 5, num_workers: 4)
|
|
56
|
+
@window_size = window_size
|
|
57
|
+
@num_workers = num_workers
|
|
58
|
+
@processed_count = 0
|
|
59
|
+
@error_count = 0
|
|
60
|
+
@events_in_window = []
|
|
61
|
+
@metrics = {
|
|
62
|
+
total_events: 0,
|
|
63
|
+
events_per_second: 0.0,
|
|
64
|
+
average_processing_time: 0.0,
|
|
65
|
+
current_window_count: 0
|
|
66
|
+
}
|
|
67
|
+
@start_time = Time.now
|
|
68
|
+
@mutex = Mutex.new
|
|
69
|
+
@supervisor = nil
|
|
70
|
+
@running = false
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def start
|
|
74
|
+
puts "Starting Stream Processor..."
|
|
75
|
+
puts "Window size: #{@window_size} seconds"
|
|
76
|
+
puts "Workers: #{@num_workers}"
|
|
77
|
+
puts
|
|
78
|
+
|
|
79
|
+
@supervisor = Fractor::Supervisor.new(
|
|
80
|
+
worker_pools: [
|
|
81
|
+
{ worker_class: EventProcessorWorker, num_workers: @num_workers }
|
|
82
|
+
]
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
@running = true
|
|
86
|
+
@start_time = Time.now
|
|
87
|
+
|
|
88
|
+
self
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def add_event(event)
|
|
92
|
+
return unless @supervisor && @running
|
|
93
|
+
|
|
94
|
+
@supervisor.add_work_item(event)
|
|
95
|
+
|
|
96
|
+
@mutex.synchronize do
|
|
97
|
+
@metrics[:total_events] += 1
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def process_events
|
|
102
|
+
return unless @supervisor && @running
|
|
103
|
+
|
|
104
|
+
@supervisor.run
|
|
105
|
+
|
|
106
|
+
results_obj = @supervisor.results
|
|
107
|
+
all_results = results_obj.results + results_obj.errors
|
|
108
|
+
|
|
109
|
+
all_results.each do |work_result|
|
|
110
|
+
result = work_result.respond_to?(:result) ? work_result.result : work_result
|
|
111
|
+
|
|
112
|
+
next unless result.is_a?(Hash)
|
|
113
|
+
|
|
114
|
+
process_result(result)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def stop
|
|
119
|
+
puts "\nStopping Stream Processor..."
|
|
120
|
+
@running = false
|
|
121
|
+
print_final_summary
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
private
|
|
125
|
+
|
|
126
|
+
def process_result(result)
|
|
127
|
+
@mutex.synchronize do
|
|
128
|
+
@processed_count += 1
|
|
129
|
+
|
|
130
|
+
# Add to current window
|
|
131
|
+
@events_in_window << result
|
|
132
|
+
|
|
133
|
+
# Remove events outside window
|
|
134
|
+
cutoff_time = Time.now - @window_size
|
|
135
|
+
@events_in_window.reject! do |r|
|
|
136
|
+
r[:processed_at] < cutoff_time
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Update metrics
|
|
140
|
+
@metrics[:current_window_count] = @events_in_window.size
|
|
141
|
+
|
|
142
|
+
if @processed_count > 0
|
|
143
|
+
elapsed = Time.now - @start_time
|
|
144
|
+
@metrics[:events_per_second] = @processed_count / elapsed
|
|
145
|
+
|
|
146
|
+
total_time = @events_in_window.sum { |r| r[:processing_time] }
|
|
147
|
+
@metrics[:average_processing_time] =
|
|
148
|
+
@events_in_window.empty? ? 0.0 : total_time / @events_in_window.size
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def print_metrics
|
|
154
|
+
@mutex.synchronize do
|
|
155
|
+
print "\r"
|
|
156
|
+
print "Events: #{@metrics[:total_events]} | "
|
|
157
|
+
print "Processed: #{@processed_count} | "
|
|
158
|
+
print "Rate: #{@metrics[:events_per_second].round(2)} e/s | "
|
|
159
|
+
print "Window: #{@metrics[:current_window_count]} | "
|
|
160
|
+
print "Avg Time: #{@metrics[:average_processing_time].round(2)} ms"
|
|
161
|
+
$stdout.flush
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def print_final_summary
|
|
166
|
+
puts "\n"
|
|
167
|
+
puts "=" * 60
|
|
168
|
+
puts "FINAL SUMMARY"
|
|
169
|
+
puts "=" * 60
|
|
170
|
+
puts format("Total Events: %d", @metrics[:total_events])
|
|
171
|
+
puts format("Processed: %d", @processed_count)
|
|
172
|
+
puts format("Errors: %d", @error_count)
|
|
173
|
+
elapsed = Time.now - @start_time
|
|
174
|
+
puts format("Duration: %.2f seconds", elapsed)
|
|
175
|
+
rate = @processed_count > 0 ? @processed_count / elapsed : 0.0
|
|
176
|
+
puts format("Average Rate: %.2f events/second", rate)
|
|
177
|
+
puts "=" * 60
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Event generator for testing
|
|
182
|
+
class EventGenerator
|
|
183
|
+
def self.generate_stream(processor, duration: 10, rate: 10)
|
|
184
|
+
puts "Generating event stream for #{duration} seconds at #{rate} events/second..."
|
|
185
|
+
|
|
186
|
+
start_time = Time.now
|
|
187
|
+
event_count = 0
|
|
188
|
+
|
|
189
|
+
while (Time.now - start_time) < duration
|
|
190
|
+
event = Event.new(
|
|
191
|
+
type: [:click, :view, :purchase, :signup].sample,
|
|
192
|
+
data: {
|
|
193
|
+
user_id: rand(1..1000),
|
|
194
|
+
value: rand(1.0..100.0).round(2)
|
|
195
|
+
}
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
processor.add_event(event)
|
|
199
|
+
event_count += 1
|
|
200
|
+
|
|
201
|
+
# Control rate
|
|
202
|
+
sleep(1.0 / rate)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
puts "\nGenerated #{event_count} events"
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Run example if executed directly
|
|
210
|
+
if __FILE__ == $PROGRAM_NAME
|
|
211
|
+
require "optparse"
|
|
212
|
+
|
|
213
|
+
options = {
|
|
214
|
+
workers: 4,
|
|
215
|
+
window_size: 5,
|
|
216
|
+
duration: 30,
|
|
217
|
+
rate: 10
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
OptionParser.new do |opts|
|
|
221
|
+
opts.banner = "Usage: stream_processor.rb [options]"
|
|
222
|
+
|
|
223
|
+
opts.on("-w", "--workers NUM", Integer, "Number of workers (default: 4)") do |n|
|
|
224
|
+
options[:workers] = n
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
opts.on("--window SIZE", Integer, "Window size in seconds (default: 5)") do |s|
|
|
228
|
+
options[:window_size] = s
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
opts.on("-d", "--duration SECONDS", Integer, "Test duration (default: 30)") do |d|
|
|
232
|
+
options[:duration] = d
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
opts.on("-r", "--rate NUM", Integer, "Events per second (default: 10)") do |r|
|
|
236
|
+
options[:rate] = r
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
opts.on("-h", "--help", "Show this message") do
|
|
240
|
+
puts opts
|
|
241
|
+
exit
|
|
242
|
+
end
|
|
243
|
+
end.parse!
|
|
244
|
+
|
|
245
|
+
# Create processor
|
|
246
|
+
processor = StreamProcessor.new(
|
|
247
|
+
window_size: options[:window_size],
|
|
248
|
+
num_workers: options[:workers]
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
processor.start
|
|
252
|
+
|
|
253
|
+
# Handle graceful shutdown
|
|
254
|
+
trap("INT") do
|
|
255
|
+
processor.stop
|
|
256
|
+
exit
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Generate test events in background
|
|
260
|
+
generator_thread = Thread.new do
|
|
261
|
+
EventGenerator.generate_stream(
|
|
262
|
+
processor,
|
|
263
|
+
duration: options[:duration],
|
|
264
|
+
rate: options[:rate]
|
|
265
|
+
)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Process events periodically
|
|
269
|
+
metrics_thread = Thread.new do
|
|
270
|
+
while processor.instance_variable_get(:@running)
|
|
271
|
+
sleep(1)
|
|
272
|
+
processor.send(:print_metrics)
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Wait for generation to complete
|
|
277
|
+
generator_thread.join
|
|
278
|
+
|
|
279
|
+
# Process remaining events
|
|
280
|
+
processor.process_events
|
|
281
|
+
|
|
282
|
+
metrics_thread.kill
|
|
283
|
+
processor.stop
|
|
284
|
+
end
|