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
@@ -0,0 +1,376 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../../lib/fractor"
5
+ require "net/http"
6
+ require "uri"
7
+ require "json"
8
+ require "time"
9
+
10
+ # API endpoint configuration
11
+ class APIEndpoint
12
+ attr_reader :name, :url, :timeout, :rate_limit_delay
13
+
14
+ def initialize(name:, url:, timeout: 5, rate_limit_delay: 0.1)
15
+ @name = name
16
+ @url = url
17
+ @timeout = timeout
18
+ @rate_limit_delay = rate_limit_delay
19
+ end
20
+
21
+ def to_s
22
+ "APIEndpoint(#{name}: #{url})"
23
+ end
24
+ end
25
+
26
+ # Mock API responses for demonstration
27
+ module MockAPIResponses
28
+ USERS_API = [
29
+ { id: 1, name: "Alice Johnson", email: "alice@example.com", role: "admin" },
30
+ { id: 2, name: "Bob Smith", email: "bob@example.com", role: "user" },
31
+ { id: 3, name: "Carol Williams", email: "carol@example.com", role: "user" }
32
+ ].freeze
33
+
34
+ PRODUCTS_API = [
35
+ { id: 101, name: "Laptop", price: 999.99, stock: 15 },
36
+ { id: 102, name: "Mouse", price: 29.99, stock: 50 },
37
+ { id: 103, name: "Keyboard", price: 79.99, stock: 30 }
38
+ ].freeze
39
+
40
+ ORDERS_API = [
41
+ { id: 1001, user_id: 1, product_id: 101, quantity: 1, status: "shipped" },
42
+ { id: 1002, user_id: 2, product_id: 102, quantity: 2, status: "pending" },
43
+ { id: 1003, user_id: 3, product_id: 103, quantity: 1, status: "delivered" }
44
+ ].freeze
45
+
46
+ ANALYTICS_API = {
47
+ total_users: 3,
48
+ total_products: 3,
49
+ total_orders: 3,
50
+ revenue: 1139.97,
51
+ timestamp: Time.now.iso8601
52
+ }.freeze
53
+
54
+ def self.get_response(endpoint_name, simulate_error: false, simulate_slow: false)
55
+ sleep(2) if simulate_slow
56
+
57
+ raise "Simulated API error" if simulate_error
58
+
59
+ case endpoint_name
60
+ when :users
61
+ { status: "success", data: USERS_API, timestamp: Time.now.iso8601 }
62
+ when :products
63
+ { status: "success", data: PRODUCTS_API, timestamp: Time.now.iso8601 }
64
+ when :orders
65
+ { status: "success", data: ORDERS_API, timestamp: Time.now.iso8601 }
66
+ when :analytics
67
+ { status: "success", data: ANALYTICS_API, timestamp: Time.now.iso8601 }
68
+ else
69
+ raise "Unknown endpoint: #{endpoint_name}"
70
+ end
71
+ end
72
+ end
73
+
74
+ # API aggregator - simplified without workflow for this example
75
+ class APIAggregator
76
+ attr_reader :endpoints, :results, :errors
77
+
78
+ def initialize
79
+ @endpoints = []
80
+ @results = {}
81
+ @errors = {}
82
+ @request_count = 0
83
+ @mutex = Mutex.new
84
+ end
85
+
86
+ def add_endpoint(endpoint)
87
+ @endpoints << endpoint
88
+ end
89
+
90
+ def fetch_all(simulate_errors: false, simulate_slow: false)
91
+ return {} if @endpoints.empty?
92
+
93
+ puts "Fetching data from #{@endpoints.size} API endpoints..."
94
+ puts "Retry enabled with exponential backoff (max 3 attempts)"
95
+ puts
96
+
97
+ @results = {}
98
+ @errors = {}
99
+
100
+ # Fetch from each endpoint with retry logic
101
+ @endpoints.each do |endpoint|
102
+ max_attempts = 3
103
+ attempt = 0
104
+
105
+ while attempt < max_attempts
106
+ attempt += 1
107
+
108
+ begin
109
+ data = fetch_endpoint(
110
+ endpoint,
111
+ {},
112
+ simulate_error: simulate_errors,
113
+ simulate_slow: simulate_slow
114
+ )
115
+
116
+ @results[endpoint.name.to_sym] = data
117
+ break
118
+ rescue StandardError => e
119
+ if attempt < max_attempts
120
+ delay = 0.5 * (2 ** (attempt - 1))
121
+ puts "[#{endpoint.name}] Retrying (attempt #{attempt + 1}/#{max_attempts}) after #{delay}s..."
122
+ sleep(delay)
123
+ else
124
+ puts "[#{endpoint.name}] All retry attempts exhausted"
125
+ @errors[endpoint.name.to_sym] = e.message
126
+ end
127
+ end
128
+ end
129
+ end
130
+
131
+ puts "\n=== Aggregation Complete ==="
132
+ puts "Successful fetches: #{@results.keys.size}"
133
+ puts "Failed fetches: #{@errors.keys.size}"
134
+ puts "Total API requests: #{@request_count}"
135
+ puts
136
+
137
+ aggregate_data(@results)
138
+ end
139
+
140
+ private
141
+
142
+ def fetch_endpoint(endpoint, context, simulate_error: false, simulate_slow: false)
143
+ @mutex.synchronize { @request_count += 1 }
144
+
145
+ puts "[#{endpoint.name}] Fetching from #{endpoint.url}..."
146
+
147
+ # Simulate rate limiting
148
+ sleep(endpoint.rate_limit_delay)
149
+
150
+ # Use mock API for demonstration
151
+ response = MockAPIResponses.get_response(
152
+ endpoint.name.to_sym,
153
+ simulate_error: simulate_error,
154
+ simulate_slow: simulate_slow
155
+ )
156
+
157
+ puts "[#{endpoint.name}] ✓ Success (#{response[:data].size rescue 'N/A'} items)"
158
+
159
+ response[:data]
160
+ rescue StandardError => e
161
+ puts "[#{endpoint.name}] ✗ Error: #{e.message}"
162
+ raise e
163
+ end
164
+
165
+ def aggregate_data(results)
166
+ aggregated = {
167
+ users: results[:users] || [],
168
+ products: results[:products] || [],
169
+ orders: results[:orders] || [],
170
+ analytics: results[:analytics] || {},
171
+ summary: {}
172
+ }
173
+
174
+ # Calculate summary statistics
175
+ aggregated[:summary] = {
176
+ total_users: aggregated[:users].size,
177
+ total_products: aggregated[:products].size,
178
+ total_orders: aggregated[:orders].size,
179
+ endpoints_successful: results.keys.size,
180
+ endpoints_failed: @errors.keys.size,
181
+ timestamp: Time.now.iso8601
182
+ }
183
+
184
+ # Enrich orders with user and product information
185
+ if aggregated[:orders].any?
186
+ aggregated[:enriched_orders] = enrich_orders(
187
+ aggregated[:orders],
188
+ aggregated[:users],
189
+ aggregated[:products]
190
+ )
191
+ end
192
+
193
+ aggregated
194
+ end
195
+
196
+ def enrich_orders(orders, users, products)
197
+ orders.map do |order|
198
+ user = users.find { |u| u[:id] == order[:user_id] }
199
+ product = products.find { |p| p[:id] == order[:product_id] }
200
+
201
+ order.merge(
202
+ user_name: user&.dig(:name),
203
+ user_email: user&.dig(:email),
204
+ product_name: product&.dig(:name),
205
+ product_price: product&.dig(:price),
206
+ total_price: (product&.dig(:price) || 0) * order[:quantity]
207
+ )
208
+ end
209
+ end
210
+ end
211
+
212
+ # Report generator for aggregated data
213
+ class AggregationReport
214
+ def self.generate(data, output_file = nil)
215
+ report = build_report(data)
216
+
217
+ if output_file
218
+ File.write(output_file, report)
219
+ puts "Report saved to #{output_file}"
220
+ else
221
+ puts report
222
+ end
223
+
224
+ report
225
+ end
226
+
227
+ def self.build_report(data)
228
+ lines = []
229
+ lines << "=" * 80
230
+ lines << "API AGGREGATION REPORT"
231
+ lines << "=" * 80
232
+ lines << ""
233
+
234
+ # Summary
235
+ if data[:summary]
236
+ lines << "SUMMARY"
237
+ lines << "-" * 80
238
+ lines << format("Total Users: %d", data[:summary][:total_users] || 0)
239
+ lines << format("Total Products: %d", data[:summary][:total_products] || 0)
240
+ lines << format("Total Orders: %d", data[:summary][:total_orders] || 0)
241
+ lines << format("Endpoints Successful: %d", data[:summary][:endpoints_successful] || 0)
242
+ lines << format("Endpoints Failed: %d", data[:summary][:endpoints_failed] || 0)
243
+ lines << format("Timestamp: %s", data[:summary][:timestamp] || "N/A")
244
+ lines << ""
245
+ end
246
+
247
+ # Users
248
+ if data[:users]&.any?
249
+ lines << "USERS (#{data[:users].size})"
250
+ lines << "-" * 80
251
+ data[:users].first(5).each do |user|
252
+ lines << format(" %d. %s <%s> [%s]",
253
+ user[:id], user[:name], user[:email], user[:role])
254
+ end
255
+ lines << " ..." if data[:users].size > 5
256
+ lines << ""
257
+ end
258
+
259
+ # Products
260
+ if data[:products]&.any?
261
+ lines << "PRODUCTS (#{data[:products].size})"
262
+ lines << "-" * 80
263
+ data[:products].first(5).each do |product|
264
+ lines << format(" %d. %s - $%.2f (Stock: %d)",
265
+ product[:id], product[:name], product[:price], product[:stock])
266
+ end
267
+ lines << " ..." if data[:products].size > 5
268
+ lines << ""
269
+ end
270
+
271
+ # Enriched Orders
272
+ if data[:enriched_orders]&.any?
273
+ lines << "ENRICHED ORDERS (#{data[:enriched_orders].size})"
274
+ lines << "-" * 80
275
+ data[:enriched_orders].each do |order|
276
+ lines << format(" Order #%d:", order[:id])
277
+ lines << format(" User: %s <%s>", order[:user_name], order[:user_email])
278
+ lines << format(" Product: %s", order[:product_name])
279
+ lines << format(" Quantity: %d × $%.2f = $%.2f",
280
+ order[:quantity], order[:product_price], order[:total_price])
281
+ lines << format(" Status: %s", order[:status])
282
+ lines << ""
283
+ end
284
+ end
285
+
286
+ # Analytics
287
+ if data[:analytics].is_a?(Hash) && data[:analytics].any?
288
+ lines << "ANALYTICS"
289
+ lines << "-" * 80
290
+ data[:analytics].each do |key, value|
291
+ lines << format(" %s: %s", key.to_s.gsub("_", " ").capitalize, value)
292
+ end
293
+ lines << ""
294
+ end
295
+
296
+ lines << "=" * 80
297
+
298
+ lines.join("\n")
299
+ end
300
+ end
301
+
302
+ # Run example if executed directly
303
+ if __FILE__ == $PROGRAM_NAME
304
+ require "optparse"
305
+
306
+ options = {
307
+ simulate_errors: false,
308
+ simulate_slow: false,
309
+ output: nil
310
+ }
311
+
312
+ OptionParser.new do |opts|
313
+ opts.banner = "Usage: api_aggregator.rb [options]"
314
+
315
+ opts.on("--simulate-errors", "Simulate API errors to test circuit breaker") do
316
+ options[:simulate_errors] = true
317
+ end
318
+
319
+ opts.on("--simulate-slow", "Simulate slow API responses") do
320
+ options[:simulate_slow] = true
321
+ end
322
+
323
+ opts.on("-o", "--output FILE", "Output report file") do |f|
324
+ options[:output] = f
325
+ end
326
+
327
+ opts.on("-h", "--help", "Show this message") do
328
+ puts opts
329
+ exit
330
+ end
331
+ end.parse!
332
+
333
+ puts "=== API Data Aggregator with Circuit Breaker ==="
334
+ puts
335
+
336
+ # Create aggregator
337
+ aggregator = APIAggregator.new
338
+
339
+ # Add API endpoints
340
+ aggregator.add_endpoint(APIEndpoint.new(
341
+ name: "users",
342
+ url: "https://api.example.com/users",
343
+ timeout: 5,
344
+ rate_limit_delay: 0.1
345
+ ))
346
+
347
+ aggregator.add_endpoint(APIEndpoint.new(
348
+ name: "products",
349
+ url: "https://api.example.com/products",
350
+ timeout: 5,
351
+ rate_limit_delay: 0.1
352
+ ))
353
+
354
+ aggregator.add_endpoint(APIEndpoint.new(
355
+ name: "orders",
356
+ url: "https://api.example.com/orders",
357
+ timeout: 5,
358
+ rate_limit_delay: 0.1
359
+ ))
360
+
361
+ aggregator.add_endpoint(APIEndpoint.new(
362
+ name: "analytics",
363
+ url: "https://api.example.com/analytics",
364
+ timeout: 5,
365
+ rate_limit_delay: 0.1
366
+ ))
367
+
368
+ # Fetch all data
369
+ data = aggregator.fetch_all(
370
+ simulate_errors: options[:simulate_errors],
371
+ simulate_slow: options[:simulate_slow]
372
+ )
373
+
374
+ # Generate report
375
+ AggregationReport.generate(data, options[:output])
376
+ end