fractor 0.1.6 → 0.1.8

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
@@ -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
- @compute_resources = { memory: 1024, cpu_cores: 4 }
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.is_a?(ComputeWork)
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
- @db_connection = { pool_size: 5, timeout: 30 }
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.is_a?(DatabaseWork)
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