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.
Files changed (33) 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 +162 -46
  5. data/README.adoc +1364 -376
  6. data/examples/auto_detection/auto_detection.rb +9 -9
  7. data/examples/continuous_chat_common/message_protocol.rb +53 -0
  8. data/examples/continuous_chat_fractor/README.adoc +217 -0
  9. data/examples/continuous_chat_fractor/chat_client.rb +303 -0
  10. data/examples/continuous_chat_fractor/chat_common.rb +83 -0
  11. data/examples/continuous_chat_fractor/chat_server.rb +167 -0
  12. data/examples/continuous_chat_fractor/simulate.rb +345 -0
  13. data/examples/continuous_chat_server/README.adoc +135 -0
  14. data/examples/continuous_chat_server/chat_client.rb +303 -0
  15. data/examples/continuous_chat_server/chat_server.rb +359 -0
  16. data/examples/continuous_chat_server/simulate.rb +343 -0
  17. data/examples/hierarchical_hasher/hierarchical_hasher.rb +12 -8
  18. data/examples/multi_work_type/multi_work_type.rb +30 -29
  19. data/examples/pipeline_processing/pipeline_processing.rb +15 -15
  20. data/examples/producer_subscriber/producer_subscriber.rb +20 -16
  21. data/examples/scatter_gather/scatter_gather.rb +29 -28
  22. data/examples/simple/sample.rb +5 -5
  23. data/examples/specialized_workers/specialized_workers.rb +44 -37
  24. data/lib/fractor/continuous_server.rb +188 -0
  25. data/lib/fractor/result_aggregator.rb +1 -1
  26. data/lib/fractor/supervisor.rb +277 -104
  27. data/lib/fractor/version.rb +1 -1
  28. data/lib/fractor/work_queue.rb +68 -0
  29. data/lib/fractor/work_result.rb +1 -1
  30. data/lib/fractor/worker.rb +2 -1
  31. data/lib/fractor/wrapped_ractor.rb +12 -2
  32. data/lib/fractor.rb +2 -0
  33. 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