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.
- checksums.yaml +4 -4
- data/.rubocop-https---raw-githubusercontent-com-riboseinc-oss-guides-main-ci-rubocop-yml +552 -0
- data/.rubocop.yml +14 -8
- data/.rubocop_todo.yml +284 -43
- data/README.adoc +111 -950
- 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/auto_detection/auto_detection.rb +9 -9
- data/examples/continuous_chat_common/message_protocol.rb +53 -0
- data/examples/continuous_chat_fractor/README.adoc +217 -0
- data/examples/continuous_chat_fractor/chat_client.rb +303 -0
- data/examples/continuous_chat_fractor/chat_common.rb +83 -0
- data/examples/continuous_chat_fractor/chat_server.rb +167 -0
- data/examples/continuous_chat_fractor/simulate.rb +345 -0
- data/examples/continuous_chat_server/README.adoc +135 -0
- data/examples/continuous_chat_server/chat_client.rb +303 -0
- data/examples/continuous_chat_server/chat_server.rb +359 -0
- data/examples/continuous_chat_server/simulate.rb +343 -0
- 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/hierarchical_hasher/hierarchical_hasher.rb +12 -8
- 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/multi_work_type/multi_work_type.rb +30 -29
- data/examples/performance_monitoring.rb +120 -0
- data/examples/pipeline_processing/README.adoc +740 -26
- data/examples/pipeline_processing/pipeline_processing.rb +16 -16
- data/examples/priority_work_example.rb +155 -0
- data/examples/producer_subscriber/README.adoc +889 -46
- data/examples/producer_subscriber/producer_subscriber.rb +20 -16
- data/examples/scatter_gather/README.adoc +829 -27
- data/examples/scatter_gather/scatter_gather.rb +29 -28
- data/examples/simple/README.adoc +347 -0
- data/examples/simple/sample.rb +5 -5
- data/examples/specialized_workers/README.adoc +622 -26
- data/examples/specialized_workers/specialized_workers.rb +88 -45
- 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 +183 -0
- 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 +33 -1
- data/lib/fractor/shutdown_handler.rb +168 -0
- data/lib/fractor/signal_handler.rb +80 -0
- data/lib/fractor/supervisor.rb +430 -144
- 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 +88 -0
- data/lib/fractor/work_result.rb +181 -9
- data/lib/fractor/worker.rb +75 -1
- 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 -91
- data/lib/fractor/wrapped_ractor3.rb +161 -0
- data/lib/fractor/wrapped_ractor4.rb +242 -0
- data/lib/fractor.rb +93 -3
- metadata +192 -6
- data/tests/sample.rb.bak +0 -309
- 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]
|