fractor 0.1.4 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop-https---raw-githubusercontent-com-riboseinc-oss-guides-main-ci-rubocop-yml +552 -0
  3. data/.rubocop.yml +14 -8
  4. data/.rubocop_todo.yml +284 -43
  5. data/README.adoc +111 -950
  6. data/docs/.lycheeignore +16 -0
  7. data/docs/Gemfile +24 -0
  8. data/docs/README.md +157 -0
  9. data/docs/_config.yml +151 -0
  10. data/docs/_features/error-handling.adoc +1192 -0
  11. data/docs/_features/index.adoc +80 -0
  12. data/docs/_features/monitoring.adoc +589 -0
  13. data/docs/_features/signal-handling.adoc +202 -0
  14. data/docs/_features/workflows.adoc +1235 -0
  15. data/docs/_guides/continuous-mode.adoc +736 -0
  16. data/docs/_guides/cookbook.adoc +1133 -0
  17. data/docs/_guides/index.adoc +55 -0
  18. data/docs/_guides/pipeline-mode.adoc +730 -0
  19. data/docs/_guides/troubleshooting.adoc +358 -0
  20. data/docs/_pages/architecture.adoc +1390 -0
  21. data/docs/_pages/core-concepts.adoc +1392 -0
  22. data/docs/_pages/design-principles.adoc +862 -0
  23. data/docs/_pages/getting-started.adoc +290 -0
  24. data/docs/_pages/installation.adoc +143 -0
  25. data/docs/_reference/api.adoc +1080 -0
  26. data/docs/_reference/error-reporting.adoc +670 -0
  27. data/docs/_reference/examples.adoc +181 -0
  28. data/docs/_reference/index.adoc +96 -0
  29. data/docs/_reference/troubleshooting.adoc +862 -0
  30. data/docs/_tutorials/complex-workflows.adoc +1022 -0
  31. data/docs/_tutorials/data-processing-pipeline.adoc +740 -0
  32. data/docs/_tutorials/first-application.adoc +384 -0
  33. data/docs/_tutorials/index.adoc +48 -0
  34. data/docs/_tutorials/long-running-services.adoc +931 -0
  35. data/docs/assets/images/favicon-16.png +0 -0
  36. data/docs/assets/images/favicon-32.png +0 -0
  37. data/docs/assets/images/favicon-48.png +0 -0
  38. data/docs/assets/images/favicon.ico +0 -0
  39. data/docs/assets/images/favicon.png +0 -0
  40. data/docs/assets/images/favicon.svg +45 -0
  41. data/docs/assets/images/fractor-icon.svg +49 -0
  42. data/docs/assets/images/fractor-logo.svg +61 -0
  43. data/docs/index.adoc +131 -0
  44. data/docs/lychee.toml +39 -0
  45. data/examples/api_aggregator/README.adoc +627 -0
  46. data/examples/api_aggregator/api_aggregator.rb +376 -0
  47. data/examples/auto_detection/README.adoc +407 -29
  48. data/examples/auto_detection/auto_detection.rb +9 -9
  49. data/examples/continuous_chat_common/message_protocol.rb +53 -0
  50. data/examples/continuous_chat_fractor/README.adoc +217 -0
  51. data/examples/continuous_chat_fractor/chat_client.rb +303 -0
  52. data/examples/continuous_chat_fractor/chat_common.rb +83 -0
  53. data/examples/continuous_chat_fractor/chat_server.rb +167 -0
  54. data/examples/continuous_chat_fractor/simulate.rb +345 -0
  55. data/examples/continuous_chat_server/README.adoc +135 -0
  56. data/examples/continuous_chat_server/chat_client.rb +303 -0
  57. data/examples/continuous_chat_server/chat_server.rb +359 -0
  58. data/examples/continuous_chat_server/simulate.rb +343 -0
  59. data/examples/error_reporting.rb +207 -0
  60. data/examples/file_processor/README.adoc +170 -0
  61. data/examples/file_processor/file_processor.rb +615 -0
  62. data/examples/file_processor/sample_files/invalid.csv +1 -0
  63. data/examples/file_processor/sample_files/orders.xml +24 -0
  64. data/examples/file_processor/sample_files/products.json +23 -0
  65. data/examples/file_processor/sample_files/users.csv +6 -0
  66. data/examples/hierarchical_hasher/README.adoc +629 -41
  67. data/examples/hierarchical_hasher/hierarchical_hasher.rb +12 -8
  68. data/examples/image_processor/README.adoc +610 -0
  69. data/examples/image_processor/image_processor.rb +349 -0
  70. data/examples/image_processor/processed_images/sample_10_processed.jpg.json +12 -0
  71. data/examples/image_processor/processed_images/sample_1_processed.jpg.json +12 -0
  72. data/examples/image_processor/processed_images/sample_2_processed.jpg.json +12 -0
  73. data/examples/image_processor/processed_images/sample_3_processed.jpg.json +12 -0
  74. data/examples/image_processor/processed_images/sample_4_processed.jpg.json +12 -0
  75. data/examples/image_processor/processed_images/sample_5_processed.jpg.json +12 -0
  76. data/examples/image_processor/processed_images/sample_6_processed.jpg.json +12 -0
  77. data/examples/image_processor/processed_images/sample_7_processed.jpg.json +12 -0
  78. data/examples/image_processor/processed_images/sample_8_processed.jpg.json +12 -0
  79. data/examples/image_processor/processed_images/sample_9_processed.jpg.json +12 -0
  80. data/examples/image_processor/test_images/sample_1.png +1 -0
  81. data/examples/image_processor/test_images/sample_10.png +1 -0
  82. data/examples/image_processor/test_images/sample_2.png +1 -0
  83. data/examples/image_processor/test_images/sample_3.png +1 -0
  84. data/examples/image_processor/test_images/sample_4.png +1 -0
  85. data/examples/image_processor/test_images/sample_5.png +1 -0
  86. data/examples/image_processor/test_images/sample_6.png +1 -0
  87. data/examples/image_processor/test_images/sample_7.png +1 -0
  88. data/examples/image_processor/test_images/sample_8.png +1 -0
  89. data/examples/image_processor/test_images/sample_9.png +1 -0
  90. data/examples/log_analyzer/README.adoc +662 -0
  91. data/examples/log_analyzer/log_analyzer.rb +579 -0
  92. data/examples/log_analyzer/sample_logs/apache.log +20 -0
  93. data/examples/log_analyzer/sample_logs/json.log +15 -0
  94. data/examples/log_analyzer/sample_logs/nginx.log +15 -0
  95. data/examples/log_analyzer/sample_logs/rails.log +29 -0
  96. data/examples/multi_work_type/README.adoc +576 -26
  97. data/examples/multi_work_type/multi_work_type.rb +30 -29
  98. data/examples/performance_monitoring.rb +120 -0
  99. data/examples/pipeline_processing/README.adoc +740 -26
  100. data/examples/pipeline_processing/pipeline_processing.rb +16 -16
  101. data/examples/priority_work_example.rb +155 -0
  102. data/examples/producer_subscriber/README.adoc +889 -46
  103. data/examples/producer_subscriber/producer_subscriber.rb +20 -16
  104. data/examples/scatter_gather/README.adoc +829 -27
  105. data/examples/scatter_gather/scatter_gather.rb +29 -28
  106. data/examples/simple/README.adoc +347 -0
  107. data/examples/simple/sample.rb +5 -5
  108. data/examples/specialized_workers/README.adoc +622 -26
  109. data/examples/specialized_workers/specialized_workers.rb +88 -45
  110. data/examples/stream_processor/README.adoc +206 -0
  111. data/examples/stream_processor/stream_processor.rb +284 -0
  112. data/examples/web_scraper/README.adoc +625 -0
  113. data/examples/web_scraper/web_scraper.rb +285 -0
  114. data/examples/workflow/README.adoc +406 -0
  115. data/examples/workflow/circuit_breaker/README.adoc +360 -0
  116. data/examples/workflow/circuit_breaker/circuit_breaker_workflow.rb +225 -0
  117. data/examples/workflow/conditional/README.adoc +483 -0
  118. data/examples/workflow/conditional/conditional_workflow.rb +215 -0
  119. data/examples/workflow/dead_letter_queue/README.adoc +374 -0
  120. data/examples/workflow/dead_letter_queue/dead_letter_queue_workflow.rb +217 -0
  121. data/examples/workflow/fan_out/README.adoc +381 -0
  122. data/examples/workflow/fan_out/fan_out_workflow.rb +202 -0
  123. data/examples/workflow/retry/README.adoc +248 -0
  124. data/examples/workflow/retry/retry_workflow.rb +195 -0
  125. data/examples/workflow/simple_linear/README.adoc +267 -0
  126. data/examples/workflow/simple_linear/simple_linear_workflow.rb +175 -0
  127. data/examples/workflow/simplified/README.adoc +329 -0
  128. data/examples/workflow/simplified/simplified_workflow.rb +222 -0
  129. data/exe/fractor +10 -0
  130. data/lib/fractor/cli.rb +288 -0
  131. data/lib/fractor/configuration.rb +307 -0
  132. data/lib/fractor/continuous_server.rb +183 -0
  133. data/lib/fractor/error_formatter.rb +72 -0
  134. data/lib/fractor/error_report_generator.rb +152 -0
  135. data/lib/fractor/error_reporter.rb +244 -0
  136. data/lib/fractor/error_statistics.rb +147 -0
  137. data/lib/fractor/execution_tracer.rb +162 -0
  138. data/lib/fractor/logger.rb +230 -0
  139. data/lib/fractor/main_loop_handler.rb +406 -0
  140. data/lib/fractor/main_loop_handler3.rb +135 -0
  141. data/lib/fractor/main_loop_handler4.rb +299 -0
  142. data/lib/fractor/performance_metrics_collector.rb +181 -0
  143. data/lib/fractor/performance_monitor.rb +215 -0
  144. data/lib/fractor/performance_report_generator.rb +202 -0
  145. data/lib/fractor/priority_work.rb +93 -0
  146. data/lib/fractor/priority_work_queue.rb +189 -0
  147. data/lib/fractor/result_aggregator.rb +33 -1
  148. data/lib/fractor/shutdown_handler.rb +168 -0
  149. data/lib/fractor/signal_handler.rb +80 -0
  150. data/lib/fractor/supervisor.rb +430 -144
  151. data/lib/fractor/supervisor_logger.rb +88 -0
  152. data/lib/fractor/version.rb +1 -1
  153. data/lib/fractor/work.rb +12 -0
  154. data/lib/fractor/work_distribution_manager.rb +151 -0
  155. data/lib/fractor/work_queue.rb +88 -0
  156. data/lib/fractor/work_result.rb +181 -9
  157. data/lib/fractor/worker.rb +75 -1
  158. data/lib/fractor/workflow/builder.rb +210 -0
  159. data/lib/fractor/workflow/chain_builder.rb +169 -0
  160. data/lib/fractor/workflow/circuit_breaker.rb +183 -0
  161. data/lib/fractor/workflow/circuit_breaker_orchestrator.rb +208 -0
  162. data/lib/fractor/workflow/circuit_breaker_registry.rb +112 -0
  163. data/lib/fractor/workflow/dead_letter_queue.rb +334 -0
  164. data/lib/fractor/workflow/execution_hooks.rb +39 -0
  165. data/lib/fractor/workflow/execution_strategy.rb +225 -0
  166. data/lib/fractor/workflow/execution_trace.rb +134 -0
  167. data/lib/fractor/workflow/helpers.rb +191 -0
  168. data/lib/fractor/workflow/job.rb +290 -0
  169. data/lib/fractor/workflow/job_dependency_validator.rb +120 -0
  170. data/lib/fractor/workflow/logger.rb +110 -0
  171. data/lib/fractor/workflow/pre_execution_context.rb +193 -0
  172. data/lib/fractor/workflow/retry_config.rb +156 -0
  173. data/lib/fractor/workflow/retry_orchestrator.rb +184 -0
  174. data/lib/fractor/workflow/retry_strategy.rb +93 -0
  175. data/lib/fractor/workflow/structured_logger.rb +30 -0
  176. data/lib/fractor/workflow/type_compatibility_validator.rb +222 -0
  177. data/lib/fractor/workflow/visualizer.rb +211 -0
  178. data/lib/fractor/workflow/workflow_context.rb +132 -0
  179. data/lib/fractor/workflow/workflow_executor.rb +669 -0
  180. data/lib/fractor/workflow/workflow_result.rb +55 -0
  181. data/lib/fractor/workflow/workflow_validator.rb +295 -0
  182. data/lib/fractor/workflow.rb +333 -0
  183. data/lib/fractor/wrapped_ractor.rb +66 -91
  184. data/lib/fractor/wrapped_ractor3.rb +161 -0
  185. data/lib/fractor/wrapped_ractor4.rb +242 -0
  186. data/lib/fractor.rb +93 -3
  187. metadata +192 -6
  188. data/tests/sample.rb.bak +0 -309
  189. data/tests/sample_working.rb.bak +0 -209
@@ -0,0 +1,343 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "fileutils"
5
+ require "optparse"
6
+ require "json"
7
+
8
+ module ContinuousChat
9
+ # Simulation controller that manages the server and clients
10
+ class Simulation
11
+ attr_reader :server_port, :log_dir
12
+
13
+ def initialize(server_port = 3000, duration = 10, log_dir = "logs")
14
+ @server_port = server_port
15
+ @duration = duration
16
+ @log_dir = log_dir
17
+ @server_pid = nil
18
+ @client_pids = {}
19
+ @running = false
20
+
21
+ # Create log directory if it doesn't exist
22
+ FileUtils.mkdir_p(@log_dir)
23
+ end
24
+
25
+ # Start the simulation
26
+ def start
27
+ puts "Starting chat simulation on port #{@server_port}"
28
+ puts "Logs will be saved to #{@log_dir}"
29
+
30
+ # Start the server
31
+ start_server
32
+
33
+ # Give the server time to initialize
34
+ puts "Waiting for server to initialize..."
35
+ sleep(2)
36
+
37
+ # Start the clients
38
+ start_clients
39
+
40
+ @running = true
41
+ puts "Chat simulation started"
42
+
43
+ # Wait for the specified duration
44
+ puts "Simulation will run for #{@duration} seconds"
45
+
46
+ # Give clients time to connect
47
+ sleep(2)
48
+ puts "Clients should be connecting now..."
49
+
50
+ # Wait for messages to be processed
51
+ remaining_time = @duration - 4
52
+ if remaining_time.positive?
53
+ puts "Waiting #{remaining_time} more seconds for processing..."
54
+ sleep(remaining_time)
55
+ end
56
+
57
+ puts "Simulation time complete, stopping..."
58
+
59
+ # Stop the simulation
60
+ stop
61
+
62
+ # Analyze the logs
63
+ analyze_logs
64
+
65
+ true
66
+ rescue StandardError => e
67
+ puts "Failed to start simulation: #{e.message}"
68
+ stop
69
+ false
70
+ end
71
+
72
+ # Stop the simulation
73
+ def stop
74
+ puts "Stopping chat simulation..."
75
+
76
+ # Stop all clients
77
+ stop_clients
78
+
79
+ # Stop the server
80
+ stop_server
81
+
82
+ @running = false
83
+ puts "Chat simulation stopped"
84
+ end
85
+
86
+ private
87
+
88
+ # Start the server process
89
+ def start_server
90
+ server_log_file = File.join(@log_dir, "server_messages.log")
91
+
92
+ # Get the directory where this script is located
93
+ script_dir = File.dirname(__FILE__)
94
+ server_script = File.join(script_dir, "chat_server.rb")
95
+
96
+ server_cmd = "ruby #{server_script} #{@server_port} #{server_log_file}"
97
+
98
+ puts "Starting server: #{server_cmd}"
99
+
100
+ # Start the server process as a fork
101
+ @server_pid = fork do
102
+ exec(server_cmd)
103
+ end
104
+
105
+ puts "Server started with PID #{@server_pid}"
106
+ end
107
+
108
+ # Stop the server process
109
+ def stop_server
110
+ return unless @server_pid
111
+
112
+ puts "Stopping server (PID #{@server_pid})..."
113
+
114
+ # Send SIGINT to the server process
115
+ begin
116
+ Process.kill("INT", @server_pid)
117
+ # Give it a moment to shut down gracefully
118
+ sleep(1)
119
+
120
+ # Force kill if still running
121
+ Process.kill("KILL", @server_pid) if process_running?(@server_pid)
122
+ rescue Errno::ESRCH
123
+ # Process already gone
124
+ end
125
+
126
+ @server_pid = nil
127
+ puts "Server stopped"
128
+ end
129
+
130
+ # Start client processes
131
+ def start_clients
132
+ # Define the client usernames and their messages
133
+ clients = {
134
+ "alice" => [
135
+ { content: "Hello everyone!", recipient: "all" },
136
+ { content: "I'm working on a Ruby project using sockets",
137
+ recipient: "all" },
138
+ { content: "It's a simple chat server and client", recipient: "all" },
139
+ ],
140
+ "bob" => [
141
+ { content: "Hi Alice!", recipient: "alice" },
142
+ { content: "That sounds interesting. What kind of project?",
143
+ recipient: "alice" },
144
+ { content: "Cool! I love Ruby's socket features",
145
+ recipient: "alice" },
146
+ ],
147
+ "charlie" => [
148
+ { content: "How's everyone doing today?", recipient: "all" },
149
+ { content: "Are you using any specific libraries?",
150
+ recipient: "alice" },
151
+ { content: "Non-blocking IO in chat clients is efficient",
152
+ recipient: "all" },
153
+ ],
154
+ }
155
+
156
+ puts "Starting #{clients.size} clients: #{clients.keys.join(', ')}"
157
+
158
+ # Start each client in a separate process
159
+ clients.each do |username, messages|
160
+ start_client(username, messages)
161
+ end
162
+ end
163
+
164
+ # Start a single client process
165
+ def start_client(username, messages)
166
+ client_log_file = File.join(@log_dir, "client_#{username}_messages.log")
167
+ messages_file = File.join(@log_dir,
168
+ "client_#{username}_send_messages.json")
169
+
170
+ # Write the messages to a JSON file
171
+ File.write(messages_file, JSON.generate(messages))
172
+
173
+ # Get the directory where this script is located
174
+ script_dir = File.dirname(__FILE__)
175
+ client_script = File.join(script_dir, "chat_client.rb")
176
+
177
+ # Build the client command
178
+ client_cmd = "ruby #{client_script} #{username} #{@server_port} #{client_log_file}"
179
+
180
+ puts "Starting client #{username}"
181
+
182
+ # Start the client process as a fork
183
+ @client_pids[username] = fork do
184
+ exec(client_cmd)
185
+ end
186
+
187
+ puts "Client #{username} started with PID #{@client_pids[username]}"
188
+ end
189
+
190
+ # Stop all client processes
191
+ def stop_clients
192
+ return if @client_pids.empty?
193
+
194
+ puts "Stopping #{@client_pids.size} clients..."
195
+
196
+ @client_pids.each do |username, pid|
197
+ # Try to gracefully terminate the process
198
+ begin
199
+ Process.kill("INT", pid)
200
+ # Give it a moment to shut down
201
+ sleep(0.5)
202
+
203
+ # Force kill if still running
204
+ Process.kill("KILL", pid) if process_running?(pid)
205
+ rescue Errno::ESRCH
206
+ # Process already gone
207
+ end
208
+
209
+ puts "Client #{username} stopped"
210
+ rescue StandardError => e
211
+ puts "Error stopping client #{username}: #{e.message}"
212
+ end
213
+
214
+ @client_pids.clear
215
+ end
216
+
217
+ # Check if a process is still running
218
+ def process_running?(pid)
219
+ Process.getpgid(pid)
220
+ true
221
+ rescue Errno::ESRCH
222
+ false
223
+ end
224
+
225
+ # Analyze the log files after the simulation
226
+ def analyze_logs
227
+ puts "\nSimulation Results"
228
+ puts "================="
229
+
230
+ # Analyze server log
231
+ server_log_file = File.join(@log_dir, "server_messages.log")
232
+ if File.exist?(server_log_file)
233
+ server_log = File.readlines(server_log_file)
234
+ puts "Server processed #{server_log.size} log entries"
235
+
236
+ # Count message types
237
+ message_count = server_log.count do |line|
238
+ line.include?("Received from")
239
+ end
240
+ broadcast_count = server_log.count do |line|
241
+ line.include?('Broadcasting: {:type=>"broadcast"')
242
+ end
243
+ direct_count = server_log.count do |line|
244
+ line =~ /Received from \w+:.*"recipient":"(?!all)/
245
+ end
246
+
247
+ puts " - #{message_count} messages received from clients"
248
+ puts " - #{broadcast_count} broadcast messages sent"
249
+ puts " - #{direct_count} direct messages sent"
250
+ else
251
+ puts "Server log file not found"
252
+ end
253
+
254
+ puts "\nClient Activity:"
255
+ # Analyze each client log
256
+ @client_pids.each_key do |username|
257
+ client_log_file = File.join(@log_dir, "client_#{username}_messages.log")
258
+ if File.exist?(client_log_file)
259
+ client_log = File.readlines(client_log_file)
260
+ sent_count = client_log.count { |line| line.include?("Sent message") }
261
+ received_count = client_log.count do |line|
262
+ line.include?("Received:")
263
+ end
264
+
265
+ puts " #{username}: Sent #{sent_count} messages, Received #{received_count} messages"
266
+ else
267
+ puts " #{username}: Log file not found"
268
+ end
269
+ end
270
+
271
+ puts "\nLog files are available in the #{@log_dir} directory for detailed analysis."
272
+ end
273
+ end
274
+ end
275
+
276
+ # When run directly, start the simulation
277
+ if __FILE__ == $PROGRAM_NAME
278
+ options = {
279
+ port: 3000,
280
+ duration: 10,
281
+ log_dir: "logs",
282
+ }
283
+
284
+ # Parse command line options
285
+ OptionParser.new do |opts|
286
+ opts.banner = "Usage: ruby simulate.rb [options]"
287
+
288
+ opts.on("-p", "--port PORT", Integer,
289
+ "Server port (default: 3000)") do |port|
290
+ options[:port] = port
291
+ end
292
+
293
+ opts.on("-d", "--duration SECONDS", Integer,
294
+ "Simulation duration in seconds (default: 10)") do |duration|
295
+ options[:duration] = duration
296
+ end
297
+
298
+ opts.on("-l", "--log-dir DIR",
299
+ "Directory for log files (default: logs)") do |dir|
300
+ options[:log_dir] = dir
301
+ end
302
+
303
+ opts.on("-h", "--help", "Show this help message") do
304
+ puts opts
305
+ exit
306
+ end
307
+ end.parse!
308
+
309
+ puts "Starting Chat Simulation"
310
+ puts "======================"
311
+ puts "This simulation runs a chat server and multiple clients as separate processes"
312
+ puts "to demonstrate a basic chat application with socket communication."
313
+ puts
314
+
315
+ # Create and run the simulation
316
+ simulation = ContinuousChat::Simulation.new(
317
+ options[:port],
318
+ options[:duration],
319
+ options[:log_dir],
320
+ )
321
+
322
+ # Set up signal handlers to properly clean up child processes
323
+ Signal.trap("INT") do
324
+ puts "\nSimulation interrupted"
325
+ simulation.stop
326
+ exit
327
+ end
328
+
329
+ Signal.trap("TERM") do
330
+ puts "\nSimulation terminated"
331
+ simulation.stop
332
+ exit
333
+ end
334
+
335
+ begin
336
+ simulation.start
337
+ rescue Interrupt
338
+ puts "\nSimulation interrupted"
339
+ simulation.stop
340
+ end
341
+
342
+ puts "Simulation completed"
343
+ end
@@ -0,0 +1,207 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../lib/fractor"
4
+ require_relative "../lib/fractor/error_reporter"
5
+
6
+ # Example demonstrating comprehensive error reporting and analytics
7
+ #
8
+ # This example shows how to:
9
+ # 1. Set up an ErrorReporter to track errors
10
+ # 2. Record successes and failures
11
+ # 3. Generate comprehensive error reports
12
+ # 4. Export metrics to Prometheus format
13
+ # 5. Set up error handlers for real-time notifications
14
+
15
+ # Simulate various types of workers
16
+ class NetworkWorker < Fractor::Worker
17
+ def process(work)
18
+ # Simulate network errors
19
+ if work.input[:fail]
20
+ Fractor::WorkResult.new(
21
+ error: SocketError.new("Connection refused"),
22
+ error_code: :connection_refused,
23
+ error_context: { endpoint: work.input[:endpoint], attempt: 1 },
24
+ work: work,
25
+ )
26
+ else
27
+ Fractor::WorkResult.new(result: "Data fetched", work: work)
28
+ end
29
+ end
30
+ end
31
+
32
+ class ValidationWorker < Fractor::Worker
33
+ def process(work)
34
+ # Simulate validation errors
35
+ if work.input[:invalid]
36
+ Fractor::WorkResult.new(
37
+ error: ArgumentError.new("Invalid input"),
38
+ error_code: :validation_failed,
39
+ error_context: { field: "email", value: work.input[:value] },
40
+ work: work,
41
+ )
42
+ else
43
+ Fractor::WorkResult.new(result: "Valid", work: work)
44
+ end
45
+ end
46
+ end
47
+
48
+ class CriticalWorker < Fractor::Worker
49
+ def process(work)
50
+ # Simulate critical errors
51
+ if work.input[:critical]
52
+ Fractor::WorkResult.new(
53
+ error: SystemStackError.new("Stack overflow"),
54
+ error_severity: :critical,
55
+ error_context: { stack_depth: 10000 },
56
+ work: work,
57
+ )
58
+ else
59
+ Fractor::WorkResult.new(result: "Success", work: work)
60
+ end
61
+ end
62
+ end
63
+
64
+ puts "=" * 80
65
+ puts "Error Reporting Example"
66
+ puts "=" * 80
67
+ puts ""
68
+
69
+ # Initialize error reporter
70
+ reporter = Fractor::ErrorReporter.new
71
+
72
+ # Set up error handler for real-time notifications
73
+ reporter.on_error do |work_result, job_name|
74
+ if work_result.critical?
75
+ puts "🚨 CRITICAL ERROR DETECTED!"
76
+ puts " Job: #{job_name || 'unknown'}"
77
+ puts " Error: #{work_result.error.message}"
78
+ puts ""
79
+ end
80
+ end
81
+
82
+ puts "1. Recording various work results..."
83
+ puts "-" * 80
84
+
85
+ # Simulate successful work
86
+ 5.times do |i|
87
+ work = Fractor::Work.new({ endpoint: "api.example.com", id: i })
88
+ result = NetworkWorker.new.process(work)
89
+ reporter.record(result, job_name: "network_job")
90
+ end
91
+ puts "✓ Recorded 5 successful network operations"
92
+
93
+ # Simulate network errors
94
+ 3.times do |i|
95
+ work = Fractor::Work.new({ fail: true, endpoint: "api.example.com", id: i })
96
+ result = NetworkWorker.new.process(work)
97
+ reporter.record(result, job_name: "network_job")
98
+ end
99
+ puts "✗ Recorded 3 network errors"
100
+
101
+ # Simulate validation errors
102
+ 4.times do |i|
103
+ work = Fractor::Work.new({ invalid: true, value: "bad-email-#{i}" })
104
+ result = ValidationWorker.new.process(work)
105
+ reporter.record(result, job_name: "validation_job")
106
+ end
107
+ puts "✗ Recorded 4 validation errors"
108
+
109
+ # Simulate critical error
110
+ work = Fractor::Work.new({ critical: true })
111
+ result = CriticalWorker.new.process(work)
112
+ reporter.record(result, job_name: "critical_job")
113
+ puts "🚨 Recorded 1 critical error"
114
+
115
+ # Add some successful validations
116
+ 3.times do |i|
117
+ work = Fractor::Work.new({ valid: true, value: "good-email-#{i}@example.com" })
118
+ result = ValidationWorker.new.process(work)
119
+ reporter.record(result, job_name: "validation_job")
120
+ end
121
+ puts "✓ Recorded 3 successful validations"
122
+
123
+ puts ""
124
+ puts "2. Overall Statistics"
125
+ puts "-" * 80
126
+ puts "Total Successes: #{reporter.total_successes}"
127
+ puts "Total Errors: #{reporter.total_errors}"
128
+ puts "Error Rate: #{reporter.overall_error_rate}%"
129
+ puts ""
130
+
131
+ puts "3. Top Error Categories"
132
+ puts "-" * 80
133
+ reporter.top_categories.each do |category, count|
134
+ puts "#{category.to_s.ljust(15)}: #{count} errors"
135
+ end
136
+ puts ""
137
+
138
+ puts "4. Top Error Jobs"
139
+ puts "-" * 80
140
+ reporter.top_jobs.each do |job, count|
141
+ puts "#{job.to_s.ljust(20)}: #{count} errors"
142
+ end
143
+ puts ""
144
+
145
+ puts "5. Critical Errors"
146
+ puts "-" * 80
147
+ critical = reporter.critical_errors
148
+ if critical.empty?
149
+ puts "No critical errors"
150
+ else
151
+ critical.each do |error_info|
152
+ puts "Category: #{error_info[:category]}"
153
+ puts "Count: #{error_info[:count]}"
154
+ puts "Recent:"
155
+ error_info[:recent].each do |err|
156
+ puts " - #{err[:error_class]}: #{err[:error_message]}"
157
+ end
158
+ end
159
+ end
160
+ puts ""
161
+
162
+ puts "6. Errors by Severity"
163
+ puts "-" * 80
164
+ reporter.errors_by_severity.each do |severity, count|
165
+ icon = case severity
166
+ when :critical then "🚨"
167
+ when :error then "❌"
168
+ when :warning then "⚠️"
169
+ else "ℹ️"
170
+ end
171
+ puts "#{icon} #{severity.to_s.ljust(10)}: #{count}"
172
+ end
173
+ puts ""
174
+
175
+ puts "7. Job-Specific Statistics"
176
+ puts "-" * 80
177
+ reporter.top_jobs.each_key do |job_name|
178
+ stats = reporter.job_stats(job_name)
179
+ puts "Job: #{job_name}"
180
+ puts " Total Errors: #{stats[:total_count]}"
181
+ puts " Error Rate: #{stats[:error_rate]}/s"
182
+ puts " Most Common Code: #{stats[:most_common_code]}"
183
+ puts " Highest Severity: #{stats[:highest_severity]}"
184
+ puts " Trend: #{stats[:trending]}"
185
+ puts ""
186
+ end
187
+
188
+ puts "8. Formatted Text Report"
189
+ puts "-" * 80
190
+ puts ""
191
+ puts reporter.formatted_report
192
+ puts ""
193
+
194
+ puts "9. Prometheus Metrics Export"
195
+ puts "-" * 80
196
+ puts reporter.to_prometheus
197
+ puts ""
198
+
199
+ puts "10. JSON Export"
200
+ puts "-" * 80
201
+ require "json"
202
+ puts JSON.pretty_generate(reporter.report)
203
+ puts ""
204
+
205
+ puts "=" * 80
206
+ puts "Example completed successfully!"
207
+ puts "=" * 80
@@ -0,0 +1,170 @@
1
+ = Batch File Processor Example
2
+ :toc:
3
+ :toclevels: 3
4
+
5
+ Production-ready batch file processor that handles CSV, JSON, and XML files with validation, transformation, retry logic, and dead letter queue (DLQ) support using Fractor.
6
+
7
+ == Purpose
8
+
9
+ This example demonstrates:
10
+
11
+ * Batch processing of multiple file formats (CSV, JSON, XML)
12
+ * Data validation and transformation
13
+ * Dead letter queue for failed files
14
+ * Retry logic for transient failures
15
+ * Progress tracking and reporting
16
+ * Checksum verification
17
+
18
+ == Features
19
+
20
+ === Multi-Format Support
21
+
22
+ * **CSV**: Headers with data rows
23
+ * **JSON**: Arrays or objects
24
+ * **XML**: Records with elements
25
+
26
+ === Dead Letter Queue (DLQ)
27
+
28
+ Files that fail validation or parsing are moved to a DLQ for later inspection and retry:
29
+
30
+ * Failed files preserved with error details
31
+ * Inspection of DLQ contents
32
+ * Individual file retry from DLQ
33
+ * Automatic removal on successful retry
34
+
35
+ === Data Transformation
36
+
37
+ * Type conversion (strings to numbers, booleans)
38
+ * Key normalization (symbol keys)
39
+ * Automatic data enrichment
40
+
41
+ == Usage
42
+
43
+ === Basic Processing
44
+
45
+ Process all CSV, JSON, and XML files:
46
+
47
+ [source,bash]
48
+ ----
49
+ ruby file_processor.rb sample_files/*.{csv,json,xml}
50
+ ----
51
+
52
+ === Custom Workers
53
+
54
+ Process with 8 workers:
55
+
56
+ [source,bash]
57
+ ----
58
+ ruby file_processor.rb -w 8 sample_files/*.csv
59
+ ----
60
+
61
+ === Disable Validation
62
+
63
+ Skip validation step:
64
+
65
+ [source,bash]
66
+ ----
67
+ ruby file_processor.rb --no-validate sample_files/*.json
68
+ ----
69
+
70
+ === Inspect DLQ
71
+
72
+ View failed files in dead letter queue:
73
+
74
+ [source,bash]
75
+ ----
76
+ ruby file_processor.rb --inspect-dlq
77
+ ----
78
+
79
+ === Retry DLQ File
80
+
81
+ Retry a specific file from DLQ:
82
+
83
+ [source,bash]
84
+ ----
85
+ ruby file_processor.rb --retry-dlq invalid_dlq.json
86
+ ----
87
+
88
+ === Generate Report
89
+
90
+ Save processing report to file:
91
+
92
+ [source,bash]
93
+ ----
94
+ ruby file_processor.rb -o report.txt sample_files/*.csv
95
+ ----
96
+
97
+ == Architecture
98
+
99
+ The processor consists of:
100
+
101
+ * **FileWork**: Work item encapsulating file path and options
102
+ * **FileProcessorWorker**: Worker that parses, validates, and transforms files
103
+ * **BatchFileProcessor**: Orchestrates processing and manages DLQ
104
+ * **ProcessingReport**: Generates summary reports
105
+
106
+ == Examples
107
+
108
+ === Example 1: Successful Processing
109
+
110
+ [source,bash]
111
+ ----
112
+ $ ruby file_processor.rb sample_files/users.csv
113
+
114
+ Processing 1 files with 4 workers...
115
+ Validation: enabled
116
+ Transformation: enabled
117
+
118
+ [✓] users.csv: 5 records processed
119
+
120
+ === Processing Complete ===
121
+ Successful: 1
122
+ Errors: 0
123
+ DLQ: 0
124
+ ----
125
+
126
+ === Example 2: Mixed Results with DLQ
127
+
128
+ [source,bash]
129
+ ----
130
+ $ ruby file_processor.rb sample_files/*.csv
131
+
132
+ Processing 2 files with 4 workers...
133
+
134
+ [✓] users.csv: 5 records processed
135
+ [✗] invalid.csv: No records found
136
+
137
+ === Processing Complete ===
138
+ Successful: 1
139
+ Errors: 1
140
+ DLQ: 1
141
+ ----
142
+
143
+ === Example 3: DLQ Inspection
144
+
145
+ [source,bash]
146
+ ----
147
+ $ ruby file_processor.rb --inspect-dlq
148
+
149
+ === Dead Letter Queue Inspection ===
150
+
151
+ File: invalid.csv
152
+ Error: No records found
153
+ Moved at: 2024-10-25T13:00:00+08:00
154
+ Checksum: abc123...
155
+ ----
156
+
157
+ == Testing
158
+
159
+ Run the test suite:
160
+
161
+ [source,bash]
162
+ ----
163
+ bundle exec rspec spec/examples/file_processor_spec.rb
164
+ ----
165
+
166
+ == See Also
167
+
168
+ * link:../../README.adoc[Fractor Main Documentation]
169
+ * link:../log_analyzer/README.adoc[Log Analyzer Example]
170
+ * link:../api_aggregator/README.adoc[API Aggregator Example]