fractor 0.1.4 → 0.1.6
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 +162 -46
- data/README.adoc +1364 -376
- 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/hierarchical_hasher/hierarchical_hasher.rb +12 -8
- data/examples/multi_work_type/multi_work_type.rb +30 -29
- data/examples/pipeline_processing/pipeline_processing.rb +15 -15
- data/examples/producer_subscriber/producer_subscriber.rb +20 -16
- data/examples/scatter_gather/scatter_gather.rb +29 -28
- data/examples/simple/sample.rb +5 -5
- data/examples/specialized_workers/specialized_workers.rb +44 -37
- data/lib/fractor/continuous_server.rb +188 -0
- data/lib/fractor/result_aggregator.rb +1 -1
- data/lib/fractor/supervisor.rb +277 -104
- data/lib/fractor/version.rb +1 -1
- data/lib/fractor/work_queue.rb +68 -0
- data/lib/fractor/work_result.rb +1 -1
- data/lib/fractor/worker.rb +2 -1
- data/lib/fractor/wrapped_ractor.rb +12 -2
- data/lib/fractor.rb +2 -0
- metadata +15 -2
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative '../continuous_chat_common/message_protocol'
|
|
5
|
+
require_relative '../../lib/fractor'
|
|
6
|
+
|
|
7
|
+
module ContinuousChatFractor
|
|
8
|
+
# ChatMessage represents a chat message as a unit of work for Fractor
|
|
9
|
+
class ChatMessage < Fractor::Work
|
|
10
|
+
def initialize(packet, client_socket = nil)
|
|
11
|
+
super({ packet: packet, client_socket: client_socket })
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def packet
|
|
15
|
+
input[:packet]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def client_socket
|
|
19
|
+
input[:client_socket]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def to_s
|
|
23
|
+
"ChatMessage: #{packet.type} from #{packet.data}"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# ChatWorker processes chat messages using Fractor
|
|
28
|
+
class ChatWorker < Fractor::Worker
|
|
29
|
+
def process(work)
|
|
30
|
+
packet = work.packet
|
|
31
|
+
work.client_socket
|
|
32
|
+
|
|
33
|
+
# Process based on message type
|
|
34
|
+
result = case packet.type
|
|
35
|
+
when :broadcast
|
|
36
|
+
# Broadcast message processing
|
|
37
|
+
{
|
|
38
|
+
action: :broadcast,
|
|
39
|
+
from: packet.data[:from],
|
|
40
|
+
content: packet.data[:content],
|
|
41
|
+
timestamp: packet.timestamp
|
|
42
|
+
}
|
|
43
|
+
when :direct_message
|
|
44
|
+
# Direct message processing
|
|
45
|
+
{
|
|
46
|
+
action: :direct_message,
|
|
47
|
+
from: packet.data[:from],
|
|
48
|
+
to: packet.data[:to],
|
|
49
|
+
content: packet.data[:content],
|
|
50
|
+
timestamp: packet.timestamp
|
|
51
|
+
}
|
|
52
|
+
when :server_message
|
|
53
|
+
# Server message processing
|
|
54
|
+
{
|
|
55
|
+
action: :server_message,
|
|
56
|
+
message: packet.data[:message],
|
|
57
|
+
timestamp: packet.timestamp
|
|
58
|
+
}
|
|
59
|
+
when :user_list
|
|
60
|
+
# User list update
|
|
61
|
+
{
|
|
62
|
+
action: :user_list,
|
|
63
|
+
users: packet.data[:users],
|
|
64
|
+
timestamp: packet.timestamp
|
|
65
|
+
}
|
|
66
|
+
else
|
|
67
|
+
# Unknown message type
|
|
68
|
+
{
|
|
69
|
+
action: :error,
|
|
70
|
+
message: "Unknown message type: #{packet.type}",
|
|
71
|
+
timestamp: packet.timestamp
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
Fractor::WorkResult.new(result: result, work: work)
|
|
76
|
+
rescue StandardError => e
|
|
77
|
+
Fractor::WorkResult.new(
|
|
78
|
+
error: "Error processing message: #{e.message}",
|
|
79
|
+
work: work
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'socket'
|
|
5
|
+
require 'json'
|
|
6
|
+
require 'time'
|
|
7
|
+
require_relative 'chat_common'
|
|
8
|
+
|
|
9
|
+
# Refactored Chat Server using new Fractor primitives
|
|
10
|
+
puts 'Starting Fractor-based chat server (refactored)...'
|
|
11
|
+
|
|
12
|
+
# Parse command line args
|
|
13
|
+
port = ARGV[0]&.to_i || 3000
|
|
14
|
+
log_file_path = ARGV[1] || 'logs/server_messages.log'
|
|
15
|
+
|
|
16
|
+
# Thread-safe hash for client connections
|
|
17
|
+
clients = {}
|
|
18
|
+
clients_mutex = Mutex.new
|
|
19
|
+
|
|
20
|
+
# Create server socket
|
|
21
|
+
server = TCPServer.new('0.0.0.0', port)
|
|
22
|
+
|
|
23
|
+
# Set up work queue and Fractor server
|
|
24
|
+
work_queue = Fractor::WorkQueue.new
|
|
25
|
+
|
|
26
|
+
fractor_server = Fractor::ContinuousServer.new(
|
|
27
|
+
worker_pools: [
|
|
28
|
+
{ worker_class: ContinuousChatFractor::ChatWorker, num_workers: 2 }
|
|
29
|
+
],
|
|
30
|
+
work_queue: work_queue,
|
|
31
|
+
log_file: log_file_path
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Handle results from Fractor workers
|
|
35
|
+
fractor_server.on_result do |result|
|
|
36
|
+
action_data = result.result
|
|
37
|
+
case action_data[:action]
|
|
38
|
+
when :broadcast
|
|
39
|
+
puts "Broadcasting: #{action_data[:content]}"
|
|
40
|
+
when :direct_message
|
|
41
|
+
puts "DM from #{action_data[:from]} to #{action_data[:to]}"
|
|
42
|
+
when :server_message
|
|
43
|
+
puts "Server: #{action_data[:message]}"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
fractor_server.on_error do |error|
|
|
48
|
+
puts "Error: #{error.error}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Start Fractor server in background
|
|
52
|
+
Thread.new { fractor_server.run }
|
|
53
|
+
sleep(0.2) # Give it time to start
|
|
54
|
+
|
|
55
|
+
puts "Server started on port #{port}"
|
|
56
|
+
puts "Server ready to accept connections"
|
|
57
|
+
|
|
58
|
+
# Handle new client connections
|
|
59
|
+
begin
|
|
60
|
+
sockets = [server]
|
|
61
|
+
|
|
62
|
+
loop do
|
|
63
|
+
readable, = IO.select(sockets, [], [], 0.1)
|
|
64
|
+
next unless readable
|
|
65
|
+
|
|
66
|
+
readable.each do |socket|
|
|
67
|
+
if socket == server
|
|
68
|
+
# New client connection
|
|
69
|
+
client = server.accept
|
|
70
|
+
sockets << client
|
|
71
|
+
|
|
72
|
+
# Read join message
|
|
73
|
+
line = client.gets&.chomp
|
|
74
|
+
if line
|
|
75
|
+
message = JSON.parse(line)
|
|
76
|
+
if message['type'] == 'join' && message['data']['username']
|
|
77
|
+
username = message['data']['username']
|
|
78
|
+
clients_mutex.synchronize { clients[username] = client }
|
|
79
|
+
|
|
80
|
+
packet = ContinuousChat::MessagePacket.new(
|
|
81
|
+
:server_message,
|
|
82
|
+
{ message: "#{username} joined!" }
|
|
83
|
+
)
|
|
84
|
+
work_queue << ContinuousChatFractor::ChatMessage.new(packet)
|
|
85
|
+
|
|
86
|
+
client.puts(JSON.generate({
|
|
87
|
+
type: 'server_message',
|
|
88
|
+
data: { message: "Welcome #{username}!" },
|
|
89
|
+
timestamp: Time.now.to_i
|
|
90
|
+
}))
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
else
|
|
94
|
+
# Existing client sent data
|
|
95
|
+
line = socket.gets&.chomp
|
|
96
|
+
if line.nil?
|
|
97
|
+
# Client disconnected
|
|
98
|
+
username = clients_mutex.synchronize { clients.key(socket) }
|
|
99
|
+
if username
|
|
100
|
+
clients_mutex.synchronize { clients.delete(username) }
|
|
101
|
+
packet = ContinuousChat::MessagePacket.new(
|
|
102
|
+
:server_message,
|
|
103
|
+
{ message: "#{username} left" }
|
|
104
|
+
)
|
|
105
|
+
work_queue << ContinuousChatFractor::ChatMessage.new(packet)
|
|
106
|
+
end
|
|
107
|
+
sockets.delete(socket)
|
|
108
|
+
socket.close rescue nil
|
|
109
|
+
else
|
|
110
|
+
# Process message
|
|
111
|
+
message = JSON.parse(line)
|
|
112
|
+
username = clients_mutex.synchronize { clients.key(socket) }
|
|
113
|
+
|
|
114
|
+
if message['type'] == 'message'
|
|
115
|
+
content = message['data']['content']
|
|
116
|
+
recipient = message['data']['recipient'] || 'all'
|
|
117
|
+
|
|
118
|
+
if recipient == 'all'
|
|
119
|
+
packet = ContinuousChat::MessagePacket.new(
|
|
120
|
+
:broadcast,
|
|
121
|
+
{ from: username, content: content }
|
|
122
|
+
)
|
|
123
|
+
work_queue << ContinuousChatFractor::ChatMessage.new(packet)
|
|
124
|
+
|
|
125
|
+
# Broadcast to clients
|
|
126
|
+
broadcast_msg = {
|
|
127
|
+
type: 'broadcast',
|
|
128
|
+
data: { from: username, content: content },
|
|
129
|
+
timestamp: Time.now.to_i
|
|
130
|
+
}
|
|
131
|
+
clients_mutex.synchronize do
|
|
132
|
+
clients.each_value { |c| c.puts(JSON.generate(broadcast_msg)) rescue nil }
|
|
133
|
+
end
|
|
134
|
+
else
|
|
135
|
+
packet = ContinuousChat::MessagePacket.new(
|
|
136
|
+
:direct_message,
|
|
137
|
+
{ from: username, to: recipient, content: content }
|
|
138
|
+
)
|
|
139
|
+
work_queue << ContinuousChatFractor::ChatMessage.new(packet)
|
|
140
|
+
|
|
141
|
+
# Send direct message
|
|
142
|
+
dm_msg = {
|
|
143
|
+
type: 'direct_message',
|
|
144
|
+
data: { from: username, content: content },
|
|
145
|
+
timestamp: Time.now.to_i
|
|
146
|
+
}
|
|
147
|
+
clients_mutex.synchronize do
|
|
148
|
+
recipient_socket = clients[recipient]
|
|
149
|
+
if recipient_socket
|
|
150
|
+
recipient_socket.puts(JSON.generate(dm_msg)) rescue nil
|
|
151
|
+
socket.puts(JSON.generate(dm_msg)) if username != recipient
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
rescue Interrupt
|
|
161
|
+
puts "\nServer interrupted, shutting down..."
|
|
162
|
+
ensure
|
|
163
|
+
fractor_server.stop
|
|
164
|
+
clients_mutex.synchronize { clients.each_value { |c| c.close rescue nil } }
|
|
165
|
+
server&.close
|
|
166
|
+
puts 'Server stopped'
|
|
167
|
+
end
|
|
@@ -0,0 +1,345 @@
|
|
|
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?('Fractor: Broadcasting message from') ||
|
|
242
|
+
line.include?('Fractor processed: broadcast')
|
|
243
|
+
end
|
|
244
|
+
direct_count = server_log.count do |line|
|
|
245
|
+
line.include?('Fractor: Direct message from') ||
|
|
246
|
+
line.include?('Fractor processed: direct_message')
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
puts " - #{message_count} messages received from clients"
|
|
250
|
+
puts " - #{broadcast_count} broadcast messages processed by Fractor"
|
|
251
|
+
puts " - #{direct_count} direct messages processed by Fractor"
|
|
252
|
+
else
|
|
253
|
+
puts 'Server log file not found'
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
puts "\nClient Activity:"
|
|
257
|
+
# Analyze each client log
|
|
258
|
+
@client_pids.each_key do |username|
|
|
259
|
+
client_log_file = File.join(@log_dir, "client_#{username}_messages.log")
|
|
260
|
+
if File.exist?(client_log_file)
|
|
261
|
+
client_log = File.readlines(client_log_file)
|
|
262
|
+
sent_count = client_log.count { |line| line.include?('Sent message') }
|
|
263
|
+
received_count = client_log.count do |line|
|
|
264
|
+
line.include?('Received:')
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
puts " #{username}: Sent #{sent_count} messages, Received #{received_count} messages"
|
|
268
|
+
else
|
|
269
|
+
puts " #{username}: Log file not found"
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
puts "\nLog files are available in the #{@log_dir} directory for detailed analysis."
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# When run directly, start the simulation
|
|
279
|
+
if __FILE__ == $PROGRAM_NAME
|
|
280
|
+
options = {
|
|
281
|
+
port: 3000,
|
|
282
|
+
duration: 10,
|
|
283
|
+
log_dir: 'logs'
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
# Parse command line options
|
|
287
|
+
OptionParser.new do |opts|
|
|
288
|
+
opts.banner = 'Usage: ruby simulate.rb [options]'
|
|
289
|
+
|
|
290
|
+
opts.on('-p', '--port PORT', Integer,
|
|
291
|
+
'Server port (default: 3000)') do |port|
|
|
292
|
+
options[:port] = port
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
opts.on('-d', '--duration SECONDS', Integer,
|
|
296
|
+
'Simulation duration in seconds (default: 10)') do |duration|
|
|
297
|
+
options[:duration] = duration
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
opts.on('-l', '--log-dir DIR',
|
|
301
|
+
'Directory for log files (default: logs)') do |dir|
|
|
302
|
+
options[:log_dir] = dir
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
opts.on('-h', '--help', 'Show this help message') do
|
|
306
|
+
puts opts
|
|
307
|
+
exit
|
|
308
|
+
end
|
|
309
|
+
end.parse!
|
|
310
|
+
|
|
311
|
+
puts 'Starting Chat Simulation'
|
|
312
|
+
puts '======================'
|
|
313
|
+
puts 'This simulation runs a chat server and multiple clients as separate processes'
|
|
314
|
+
puts 'to demonstrate a basic chat application with socket communication.'
|
|
315
|
+
puts
|
|
316
|
+
|
|
317
|
+
# Create and run the simulation
|
|
318
|
+
simulation = ContinuousChat::Simulation.new(
|
|
319
|
+
options[:port],
|
|
320
|
+
options[:duration],
|
|
321
|
+
options[:log_dir]
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
# Set up signal handlers to properly clean up child processes
|
|
325
|
+
Signal.trap('INT') do
|
|
326
|
+
puts "\nSimulation interrupted"
|
|
327
|
+
simulation.stop
|
|
328
|
+
exit
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
Signal.trap('TERM') do
|
|
332
|
+
puts "\nSimulation terminated"
|
|
333
|
+
simulation.stop
|
|
334
|
+
exit
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
begin
|
|
338
|
+
simulation.start
|
|
339
|
+
rescue Interrupt
|
|
340
|
+
puts "\nSimulation interrupted"
|
|
341
|
+
simulation.stop
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
puts 'Simulation completed'
|
|
345
|
+
end
|