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,359 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "socket"
|
|
5
|
+
require "json"
|
|
6
|
+
require "time"
|
|
7
|
+
require "fileutils"
|
|
8
|
+
require_relative "../continuous_chat_common/message_protocol"
|
|
9
|
+
|
|
10
|
+
# Simple Chat Server using Ruby's standard socket library
|
|
11
|
+
# Based on the approach from https://dev.to/aurelieverrot/create-a-chat-in-the-command-line-with-ruby-2po9
|
|
12
|
+
# but modified to use JSON for message passing.
|
|
13
|
+
puts "Starting chat server..."
|
|
14
|
+
|
|
15
|
+
# Parse command line args
|
|
16
|
+
port = ARGV[0]&.to_i || 3000
|
|
17
|
+
log_file_path = ARGV[1] || "logs/server_messages.log"
|
|
18
|
+
|
|
19
|
+
# Create logs directory if it doesn't exist
|
|
20
|
+
FileUtils.mkdir_p(File.dirname(log_file_path))
|
|
21
|
+
log_file = File.open(log_file_path, "w")
|
|
22
|
+
|
|
23
|
+
def log_message(message, log_file)
|
|
24
|
+
timestamp = Time.now.strftime("%Y-%m-%d %H:%M:%S.%L")
|
|
25
|
+
log_entry = "[#{timestamp}] #{message}"
|
|
26
|
+
|
|
27
|
+
# Check if log file is still open before writing
|
|
28
|
+
if log_file && !log_file.closed?
|
|
29
|
+
log_file.puts(log_entry)
|
|
30
|
+
log_file.flush # Ensure it's written immediately
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Also print to console for debugging
|
|
34
|
+
puts log_entry
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Create the server socket
|
|
38
|
+
server = TCPServer.new("0.0.0.0", port)
|
|
39
|
+
log_message("Server started on port #{port}", log_file)
|
|
40
|
+
puts "Server bound to port #{port}"
|
|
41
|
+
|
|
42
|
+
# Array to store connected clients
|
|
43
|
+
clients = {}
|
|
44
|
+
|
|
45
|
+
# Broadcast a message to all clients
|
|
46
|
+
def announce_to_everyone(clients, message, log_file)
|
|
47
|
+
log_message("Broadcasting: #{message}", log_file)
|
|
48
|
+
|
|
49
|
+
# Convert to JSON if it's not already a string
|
|
50
|
+
message_json = message.is_a?(String) ? message : JSON.generate(message)
|
|
51
|
+
|
|
52
|
+
# Create a copy of the clients hash to avoid modification during iteration
|
|
53
|
+
clients_copy = clients.dup
|
|
54
|
+
|
|
55
|
+
clients_copy.each do |username, client|
|
|
56
|
+
client.puts(message_json)
|
|
57
|
+
rescue StandardError => e
|
|
58
|
+
log_message("Error broadcasting to #{username}: #{e.message}", log_file)
|
|
59
|
+
# We don't disconnect here, that's handled in the client thread
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Handle a new client joining the chat
|
|
64
|
+
def handle_new_client(clients, client, log_file)
|
|
65
|
+
line = client.gets&.chomp
|
|
66
|
+
return client.close unless line
|
|
67
|
+
|
|
68
|
+
log_message("Received join message: #{line}", log_file)
|
|
69
|
+
|
|
70
|
+
# Parse the join message
|
|
71
|
+
message = JSON.parse(line)
|
|
72
|
+
|
|
73
|
+
if message["type"] == "join" && message["data"] && message["data"]["username"]
|
|
74
|
+
username = message["data"]["username"]
|
|
75
|
+
|
|
76
|
+
# Store the client with username as key
|
|
77
|
+
clients[username] = client
|
|
78
|
+
|
|
79
|
+
# Send welcome message to the client
|
|
80
|
+
welcome_msg = {
|
|
81
|
+
type: "server_message",
|
|
82
|
+
data: {
|
|
83
|
+
message: "Hello #{username}! Connected clients: #{clients.count}",
|
|
84
|
+
},
|
|
85
|
+
timestamp: Time.now.to_i,
|
|
86
|
+
}
|
|
87
|
+
client.puts(JSON.generate(welcome_msg))
|
|
88
|
+
|
|
89
|
+
# Broadcast to all clients that a new user joined
|
|
90
|
+
join_msg = {
|
|
91
|
+
type: "server_message",
|
|
92
|
+
data: {
|
|
93
|
+
message: "#{username} joined the chat!",
|
|
94
|
+
},
|
|
95
|
+
timestamp: Time.now.to_i,
|
|
96
|
+
}
|
|
97
|
+
announce_to_everyone(clients, join_msg, log_file)
|
|
98
|
+
|
|
99
|
+
# Broadcast the updated user list
|
|
100
|
+
user_list_msg = {
|
|
101
|
+
type: "user_list",
|
|
102
|
+
data: {
|
|
103
|
+
users: clients.keys,
|
|
104
|
+
},
|
|
105
|
+
timestamp: Time.now.to_i,
|
|
106
|
+
}
|
|
107
|
+
announce_to_everyone(clients, user_list_msg, log_file)
|
|
108
|
+
|
|
109
|
+
true
|
|
110
|
+
else
|
|
111
|
+
# Invalid join message
|
|
112
|
+
error_msg = {
|
|
113
|
+
type: "error",
|
|
114
|
+
data: {
|
|
115
|
+
message: "First message must be a valid join command",
|
|
116
|
+
},
|
|
117
|
+
timestamp: Time.now.to_i,
|
|
118
|
+
}
|
|
119
|
+
client.puts(JSON.generate(error_msg))
|
|
120
|
+
client.close
|
|
121
|
+
false
|
|
122
|
+
end
|
|
123
|
+
rescue JSON::ParserError => e
|
|
124
|
+
log_message("Error parsing initial join message: #{e.message}", log_file)
|
|
125
|
+
client.puts(JSON.generate({
|
|
126
|
+
type: "error",
|
|
127
|
+
data: {
|
|
128
|
+
message: "Invalid JSON format in join message",
|
|
129
|
+
},
|
|
130
|
+
timestamp: Time.now.to_i,
|
|
131
|
+
}))
|
|
132
|
+
client.close
|
|
133
|
+
false
|
|
134
|
+
rescue StandardError => e
|
|
135
|
+
log_message("Error handling new client: #{e.message}", log_file)
|
|
136
|
+
client.close
|
|
137
|
+
false
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Process a message from an existing client
|
|
141
|
+
def process_client_message(clients, client, username, log_file)
|
|
142
|
+
# Read a message from the client (non-blocking with timeout)
|
|
143
|
+
readable, = IO.select([client], nil, nil, 0)
|
|
144
|
+
return true unless readable # No data to read yet
|
|
145
|
+
|
|
146
|
+
line = client.gets&.chomp
|
|
147
|
+
return false unless line # Client disconnected
|
|
148
|
+
|
|
149
|
+
begin
|
|
150
|
+
message = JSON.parse(line)
|
|
151
|
+
log_message("Received from #{username}: #{line}", log_file)
|
|
152
|
+
|
|
153
|
+
case message["type"]
|
|
154
|
+
when "message"
|
|
155
|
+
content = message["data"]["content"]
|
|
156
|
+
recipient = message["data"]["recipient"] || "all"
|
|
157
|
+
|
|
158
|
+
if content.start_with?("/")
|
|
159
|
+
# Handle commands
|
|
160
|
+
case content
|
|
161
|
+
when "/list"
|
|
162
|
+
list_msg = {
|
|
163
|
+
type: "user_list",
|
|
164
|
+
data: {
|
|
165
|
+
users: clients.keys,
|
|
166
|
+
},
|
|
167
|
+
timestamp: Time.now.to_i,
|
|
168
|
+
}
|
|
169
|
+
client.puts(JSON.generate(list_msg))
|
|
170
|
+
else
|
|
171
|
+
# Unknown command
|
|
172
|
+
client.puts(JSON.generate({
|
|
173
|
+
type: "error",
|
|
174
|
+
data: {
|
|
175
|
+
message: "Unknown command: #{content}",
|
|
176
|
+
},
|
|
177
|
+
timestamp: Time.now.to_i,
|
|
178
|
+
}))
|
|
179
|
+
end
|
|
180
|
+
elsif recipient == "all"
|
|
181
|
+
# Broadcast message
|
|
182
|
+
broadcast_msg = {
|
|
183
|
+
type: "broadcast",
|
|
184
|
+
data: {
|
|
185
|
+
from: username,
|
|
186
|
+
content: content,
|
|
187
|
+
},
|
|
188
|
+
timestamp: Time.now.to_i,
|
|
189
|
+
}
|
|
190
|
+
announce_to_everyone(clients, broadcast_msg, log_file)
|
|
191
|
+
elsif clients[recipient]
|
|
192
|
+
# Direct message
|
|
193
|
+
dm_msg = {
|
|
194
|
+
type: "direct_message",
|
|
195
|
+
data: {
|
|
196
|
+
from: username,
|
|
197
|
+
content: content,
|
|
198
|
+
},
|
|
199
|
+
timestamp: Time.now.to_i,
|
|
200
|
+
}
|
|
201
|
+
clients[recipient].puts(JSON.generate(dm_msg))
|
|
202
|
+
# Also send to sender if not the same person
|
|
203
|
+
client.puts(JSON.generate(dm_msg)) if username != recipient
|
|
204
|
+
else
|
|
205
|
+
# Recipient not found
|
|
206
|
+
client.puts(JSON.generate({
|
|
207
|
+
type: "error",
|
|
208
|
+
data: {
|
|
209
|
+
message: "User #{recipient} not found",
|
|
210
|
+
},
|
|
211
|
+
timestamp: Time.now.to_i,
|
|
212
|
+
}))
|
|
213
|
+
end
|
|
214
|
+
when "leave"
|
|
215
|
+
# Client wants to leave
|
|
216
|
+
return false
|
|
217
|
+
end
|
|
218
|
+
rescue JSON::ParserError => e
|
|
219
|
+
log_message("Error parsing JSON from #{username}: #{e.message}", log_file)
|
|
220
|
+
client.puts(JSON.generate({
|
|
221
|
+
type: "error",
|
|
222
|
+
data: {
|
|
223
|
+
message: "Invalid JSON format",
|
|
224
|
+
},
|
|
225
|
+
timestamp: Time.now.to_i,
|
|
226
|
+
}))
|
|
227
|
+
rescue StandardError => e
|
|
228
|
+
log_message("Error processing message from #{username}: #{e.message}",
|
|
229
|
+
log_file)
|
|
230
|
+
return false
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
true # Client still connected
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Handle client disconnection
|
|
237
|
+
def handle_client_disconnect(clients, client, log_file)
|
|
238
|
+
username = clients.key(client)
|
|
239
|
+
return unless username
|
|
240
|
+
|
|
241
|
+
# Remove from clients list
|
|
242
|
+
clients.delete(username)
|
|
243
|
+
log_message("Client disconnected: #{username}", log_file)
|
|
244
|
+
|
|
245
|
+
# Notify everyone
|
|
246
|
+
leave_msg = {
|
|
247
|
+
type: "server_message",
|
|
248
|
+
data: {
|
|
249
|
+
message: "#{username} left the chat.",
|
|
250
|
+
},
|
|
251
|
+
timestamp: Time.now.to_i,
|
|
252
|
+
}
|
|
253
|
+
announce_to_everyone(clients, leave_msg, log_file)
|
|
254
|
+
|
|
255
|
+
# Update user list
|
|
256
|
+
user_list_msg = {
|
|
257
|
+
type: "user_list",
|
|
258
|
+
data: {
|
|
259
|
+
users: clients.keys,
|
|
260
|
+
},
|
|
261
|
+
timestamp: Time.now.to_i,
|
|
262
|
+
}
|
|
263
|
+
announce_to_everyone(clients, user_list_msg, log_file)
|
|
264
|
+
|
|
265
|
+
# Close the client socket
|
|
266
|
+
begin
|
|
267
|
+
client.close
|
|
268
|
+
rescue StandardError
|
|
269
|
+
nil
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Main server loop
|
|
274
|
+
begin
|
|
275
|
+
log_message("Server ready to accept connections", log_file)
|
|
276
|
+
|
|
277
|
+
# Add the server socket to the list of sockets to monitor
|
|
278
|
+
sockets = [server]
|
|
279
|
+
|
|
280
|
+
# Main event loop
|
|
281
|
+
loop do
|
|
282
|
+
# Use IO.select to check which sockets have data to read
|
|
283
|
+
# This is non-blocking and allows us to handle multiple clients sequentially
|
|
284
|
+
readable, _, errored = IO.select(sockets, [], sockets, 0.1)
|
|
285
|
+
|
|
286
|
+
# Handle errors first
|
|
287
|
+
if errored && !errored.empty?
|
|
288
|
+
errored.each do |socket|
|
|
289
|
+
raise "Server socket error" if socket == server
|
|
290
|
+
|
|
291
|
+
# Server socket error - critical
|
|
292
|
+
|
|
293
|
+
# Client socket error
|
|
294
|
+
username = clients.key(socket)
|
|
295
|
+
log_message("Error on client socket: #{username || 'unknown'}",
|
|
296
|
+
log_file)
|
|
297
|
+
handle_client_disconnect(clients, socket, log_file)
|
|
298
|
+
sockets.delete(socket)
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# Nothing to process this iteration
|
|
303
|
+
next unless readable
|
|
304
|
+
|
|
305
|
+
# Process readable sockets
|
|
306
|
+
readable.each do |socket|
|
|
307
|
+
if socket == server
|
|
308
|
+
# New client connection
|
|
309
|
+
begin
|
|
310
|
+
client = server.accept
|
|
311
|
+
log_message(
|
|
312
|
+
"New client connection from #{client.peeraddr[2]}:#{client.peeraddr[1]}", log_file
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
# Add the client socket to our monitoring list
|
|
316
|
+
sockets << client
|
|
317
|
+
|
|
318
|
+
# We'll process the initial join message in the next iteration
|
|
319
|
+
rescue StandardError => e
|
|
320
|
+
log_message("Error accepting client: #{e.message}", log_file)
|
|
321
|
+
end
|
|
322
|
+
elsif clients.key(socket)
|
|
323
|
+
# Existing client sent data
|
|
324
|
+
username = clients.key(socket)
|
|
325
|
+
|
|
326
|
+
# Process message, remove client if it disconnected
|
|
327
|
+
unless process_client_message(clients, socket, username, log_file)
|
|
328
|
+
handle_client_disconnect(clients, socket, log_file)
|
|
329
|
+
sockets.delete(socket)
|
|
330
|
+
end
|
|
331
|
+
else
|
|
332
|
+
# This is a new client that needs to send their join message
|
|
333
|
+
unless handle_new_client(clients, socket, log_file)
|
|
334
|
+
# Join failed, remove from sockets
|
|
335
|
+
sockets.delete(socket)
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
rescue Interrupt
|
|
341
|
+
log_message("Server interrupted, shutting down...", log_file)
|
|
342
|
+
rescue StandardError => e
|
|
343
|
+
log_message("Server error: #{e.message}", log_file)
|
|
344
|
+
ensure
|
|
345
|
+
# Close all client connections
|
|
346
|
+
clients.each_value do |client|
|
|
347
|
+
client.close
|
|
348
|
+
rescue StandardError
|
|
349
|
+
# Ignore errors when closing
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# Close the server socket
|
|
353
|
+
server&.close
|
|
354
|
+
|
|
355
|
+
# Close the log file
|
|
356
|
+
log_file&.close
|
|
357
|
+
|
|
358
|
+
log_message("Server stopped", log_file)
|
|
359
|
+
end
|
|
@@ -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
|