fractor 0.1.3 → 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 (34) 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 +154 -48
  5. data/README.adoc +1371 -317
  6. data/examples/auto_detection/README.adoc +52 -0
  7. data/examples/auto_detection/auto_detection.rb +170 -0
  8. data/examples/continuous_chat_common/message_protocol.rb +53 -0
  9. data/examples/continuous_chat_fractor/README.adoc +217 -0
  10. data/examples/continuous_chat_fractor/chat_client.rb +303 -0
  11. data/examples/continuous_chat_fractor/chat_common.rb +83 -0
  12. data/examples/continuous_chat_fractor/chat_server.rb +167 -0
  13. data/examples/continuous_chat_fractor/simulate.rb +345 -0
  14. data/examples/continuous_chat_server/README.adoc +135 -0
  15. data/examples/continuous_chat_server/chat_client.rb +303 -0
  16. data/examples/continuous_chat_server/chat_server.rb +359 -0
  17. data/examples/continuous_chat_server/simulate.rb +343 -0
  18. data/examples/hierarchical_hasher/hierarchical_hasher.rb +12 -8
  19. data/examples/multi_work_type/multi_work_type.rb +30 -29
  20. data/examples/pipeline_processing/pipeline_processing.rb +15 -15
  21. data/examples/producer_subscriber/producer_subscriber.rb +20 -16
  22. data/examples/scatter_gather/scatter_gather.rb +29 -28
  23. data/examples/simple/sample.rb +38 -6
  24. data/examples/specialized_workers/specialized_workers.rb +44 -37
  25. data/lib/fractor/continuous_server.rb +188 -0
  26. data/lib/fractor/result_aggregator.rb +1 -1
  27. data/lib/fractor/supervisor.rb +291 -108
  28. data/lib/fractor/version.rb +1 -1
  29. data/lib/fractor/work_queue.rb +68 -0
  30. data/lib/fractor/work_result.rb +1 -1
  31. data/lib/fractor/worker.rb +2 -1
  32. data/lib/fractor/wrapped_ractor.rb +12 -2
  33. data/lib/fractor.rb +2 -0
  34. metadata +17 -2
@@ -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
@@ -0,0 +1,135 @@
1
+ = Continuous Chat Server Example
2
+
3
+ == Overview
4
+
5
+ This example demonstrates Fractor's continuous mode feature with a chat server implementation. In continuous mode, a Fractor supervisor can run indefinitely, processing work items as they arrive without stopping after the initial work queue is empty.
6
+
7
+ The example is structured as a real client-server application with socket communication, allowing you to test it with multiple terminals.
8
+
9
+ == Key Concepts
10
+
11
+ * *Continuous Mode*: Supervisors run indefinitely, waiting for new work rather than terminating
12
+ * *Work Sources*: Callback functions that provide new work to the supervisor as needed
13
+ * *Asynchronous Processing*: Workers process messages concurrently as they arrive
14
+ * *Graceful Shutdown*: Proper handling of resources when the system terminates
15
+ * *Thread Coordination*: Multiple threads working together with Ractors
16
+ * *Parallel Components*: Server and client both use multiple Fractors for different responsibilities
17
+
18
+ == Example Components
19
+
20
+ This example is split into multiple files for better organization:
21
+
22
+ 1. *chat_common.rb*: Contains shared code used by both server and client
23
+ * `ChatMessage` class extending `Fractor::Work`
24
+ * `ChatWorker` class extending `Fractor::Worker`
25
+ * Message protocol utilities for serialization
26
+
27
+ 2. *chat_server.rb*: Implements the chat server with two parallel components
28
+ * Socket handler Fractor that manages client connections using IO.select
29
+ * Logger Fractor that writes messages to log sequentially
30
+ * Thread-safe queue for message passing between components
31
+ * `ChatServer` class that manages the supervisor and coordinates components
32
+
33
+ 3. *chat_client.rb*: Implements the chat client with two parallel components
34
+ * User I/O handler Fractor for STDIN (user input) and STDOUT (display)
35
+ * Server I/O handler Fractor for socket communication with the server
36
+ * Separate queues for messages received and to be sent
37
+ * Interactive command-line interface
38
+
39
+ 4. *simulate.rb*: Automated simulation with multiple clients
40
+ * Creates a server and multiple clients as separate processes
41
+ * Runs a predefined message schedule
42
+ * Demonstrates the system at scale
43
+ * Provides analysis of communication logs after completion
44
+
45
+ == Running the Example
46
+
47
+ You can run the example in several ways:
48
+
49
+ === 1. Running the Simulation
50
+
51
+ To run the complete automated simulation:
52
+
53
+ [source,sh]
54
+ ----
55
+ ruby examples/continuous_chat_server/simulate.rb
56
+ ----
57
+
58
+ Optional parameters:
59
+ * `-p, --port PORT` - Specify server port (default: 3000)
60
+ * `-w, --workers NUM` - Number of worker Ractors (default: 2)
61
+ * `-d, --duration SECONDS` - Duration of simulation in seconds (default: 10)
62
+ * `-l, --log-dir DIR` - Directory for log files (default: logs)
63
+ * `-h, --help` - Show help message
64
+
65
+ === 2. Running Server and Clients Separately
66
+
67
+ For a more interactive experience, you can run the server in one terminal:
68
+
69
+ [source,sh]
70
+ ----
71
+ ruby examples/continuous_chat_server/chat_server.rb [PORT] [NUM_WORKERS] [LOG_FILE]
72
+ ----
73
+
74
+ And then run multiple clients in different terminals:
75
+
76
+ [source,sh]
77
+ ----
78
+ ruby examples/continuous_chat_server/chat_client.rb [USERNAME] [PORT] [LOG_FILE]
79
+ ----
80
+
81
+ == Client Commands
82
+
83
+ When running the interactive client, you can use the following commands:
84
+
85
+ * `/help` - Show help message
86
+ * `/quit` - Disconnect and exit
87
+ * `/list` - List connected users
88
+ * `/msg USERNAME MESSAGE` - Send a private message
89
+
90
+ == Architecture
91
+
92
+ === Server Architecture
93
+ The chat server has two parallel components:
94
+ 1. A Fractor that handles IO.select on a TCPSocket (waits for incoming connections and messages)
95
+ * This component only processes work when IO.select has something to provide
96
+ * It puts received messages into a thread-safe Queue
97
+ 2. A Fractor that writes messages to log sequentially
98
+ * Ensures that log entries are written in order without race conditions
99
+
100
+ The server runs as a single process with these concurrent components communicating via message passing.
101
+
102
+ === Client Architecture
103
+ The chat client also has two parallel components:
104
+ 1. A Fractor that handles IO.select for STDIN and STDOUT
105
+ * Manages user input without blocking
106
+ * Ensures sequential printing to the user's screen
107
+ * Uses separate queues for:
108
+ * Messages received from server (to be displayed to user)
109
+ * Messages from user (to be sent to server)
110
+ 2. A Fractor that handles IO.select on the TCPSocket
111
+ * Manages the connection to the server
112
+ * Sends and receives messages concurrently
113
+
114
+ == Features Demonstrated
115
+
116
+ * Setting up a supervisor in continuous mode
117
+ * Registering a work source callback that provides new work on demand
118
+ * Running the supervisor in a non-blocking manner
119
+ * Socket-based client-server communication
120
+ * Coordinating between Ruby threads and Ractor workers
121
+ * Managing the lifecycle of a long-running application
122
+ * Proper resource cleanup on shutdown
123
+ * Using Ractors for handling separate concerns (IO, logging, etc.)
124
+ * Thread-safe message passing between components
125
+
126
+ == Expected Output
127
+
128
+ The example will show:
129
+ * The chat server starting up with multiple workers
130
+ * Clients connecting to the server
131
+ * Messages being sent between clients
132
+ * Workers processing the messages concurrently
133
+ * Results being delivered to recipients
134
+ * The system gracefully shutting down after all messages are processed
135
+ * A summary of message activity from the logs