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,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
@@ -0,0 +1,303 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "socket"
5
+ require "json"
6
+ require "fileutils"
7
+
8
+ module ContinuousChat
9
+ # Simple Chat Client using Ruby's standard socket library
10
+ class ChatClient
11
+ def initialize(username, server_host = "localhost", server_port = 3000,
12
+ log_file_path = nil)
13
+ @username = username
14
+ @server_host = server_host
15
+ @server_port = server_port
16
+ @running = true
17
+
18
+ # Set up logging
19
+ @log_file_path = log_file_path || "logs/client_#{username}_messages.log"
20
+ FileUtils.mkdir_p(File.dirname(@log_file_path))
21
+ @log_file = File.open(@log_file_path, "w")
22
+
23
+ log_message("Client initialized for #{username}, connecting to #{server_host}:#{server_port}")
24
+ end
25
+
26
+ def connect
27
+ puts "Connecting to server at #{@server_host}:#{@server_port}..."
28
+ log_message("Connecting to server at #{@server_host}:#{@server_port}")
29
+
30
+ begin
31
+ @socket = TCPSocket.new(@server_host, @server_port)
32
+
33
+ # Send join message
34
+ join_data = {
35
+ type: "join",
36
+ data: {
37
+ username: @username,
38
+ },
39
+ timestamp: Time.now.to_i,
40
+ }
41
+ @socket.puts(JSON.generate(join_data))
42
+ log_message("Sent join message: #{join_data}")
43
+
44
+ puts "Connected to chat server!"
45
+ log_message("Connected to chat server")
46
+
47
+ true
48
+ rescue StandardError => e
49
+ puts "Failed to connect: #{e.message}"
50
+ log_message("Failed to connect: #{e.message}")
51
+ false
52
+ end
53
+ end
54
+
55
+ def start
56
+ return false unless @socket
57
+
58
+ # Main event loop that handles both sending and receiving messages
59
+ # without creating separate threads
60
+ main_event_loop
61
+
62
+ true
63
+ end
64
+
65
+ # Main event loop that handles both user input and server messages
66
+ def main_event_loop
67
+ log_message("Starting main event loop")
68
+
69
+ # Print initial prompt
70
+ print "(#{@username})> "
71
+ $stdout.flush
72
+
73
+ while @running
74
+ # Use IO.select to wait for input from either STDIN or the socket
75
+ # This is non-blocking and allows us to handle both in a single loop
76
+ readable, = IO.select([@socket, $stdin], nil, nil, 0.1)
77
+
78
+ next unless readable # Nothing to process this iteration
79
+
80
+ readable.each do |io|
81
+ if io == $stdin
82
+ # Handle user input
83
+ handle_user_input
84
+ elsif io == @socket
85
+ # Handle server message
86
+ handle_server_message
87
+ end
88
+ end
89
+ end
90
+ rescue Interrupt
91
+ log_message("Client interrupted")
92
+ @running = false
93
+ rescue StandardError => e
94
+ log_message("Error in main event loop: #{e.message}")
95
+ @running = false
96
+ ensure
97
+ disconnect
98
+ end
99
+
100
+ # Handle user input (non-blocking)
101
+ def handle_user_input
102
+ text = $stdin.gets&.chomp
103
+ return unless text
104
+
105
+ # Check if client wants to quit
106
+ if text == "/quit" || text.nil?
107
+ @running = false
108
+ return
109
+ end
110
+
111
+ # Create message packet
112
+ message_data = {
113
+ type: "message",
114
+ data: {
115
+ content: text,
116
+ recipient: "all", # Default to broadcast
117
+ },
118
+ timestamp: Time.now.to_i,
119
+ }
120
+
121
+ # Send to server
122
+ @socket.puts(JSON.generate(message_data))
123
+ log_message("Sent message: #{text}")
124
+
125
+ # Print prompt for next input
126
+ print "(#{@username})> "
127
+ $stdout.flush
128
+ rescue StandardError => e
129
+ log_message("Error handling user input: #{e.message}")
130
+ @running = false
131
+ end
132
+
133
+ # Handle server message (non-blocking)
134
+ def handle_server_message
135
+ line = @socket.gets&.chomp
136
+
137
+ if line.nil?
138
+ # Server closed the connection
139
+ log_message("Connection to server lost")
140
+ @running = false
141
+ return
142
+ end
143
+
144
+ # Parse and handle the message
145
+ message = JSON.parse(line)
146
+ log_message("Received: #{line}")
147
+
148
+ # Display formatted message based on type
149
+ case message["type"]
150
+ when "broadcast"
151
+ puts "\r#{message['data']['from']}: #{message['data']['content']}"
152
+ when "direct_message"
153
+ puts "\r[DM] #{message['data']['from']}: #{message['data']['content']}"
154
+ when "server_message"
155
+ puts "\r[Server] #{message['data']['message']}"
156
+ when "user_list"
157
+ puts "\r[Server] Users online: #{message['data']['users'].join(', ')}"
158
+ when "error"
159
+ puts "\r[Error] #{message['data']['message']}"
160
+ end
161
+
162
+ # Reprint the prompt
163
+ print "(#{@username})> "
164
+ $stdout.flush
165
+ rescue StandardError => e
166
+ log_message("Error handling server message: #{e.message}")
167
+ # Don't immediately break for connection errors, may be temporary
168
+ # Just log and continue, IO.select will catch closed connections
169
+ end
170
+
171
+ def run_with_messages(messages, _delay_between_messages = 1)
172
+ return false unless @socket && @running
173
+
174
+ log_message("Running with #{messages.size} predefined messages")
175
+ puts "Sending #{messages.size} predefined messages"
176
+
177
+ # Send all messages in a non-blocking way
178
+ batch_send_messages(messages)
179
+
180
+ log_message("Finished sending all predefined messages")
181
+
182
+ # Start the event loop to receive responses
183
+ main_event_loop
184
+
185
+ true
186
+ end
187
+
188
+ # Helper to send a batch of messages without blocking
189
+ def batch_send_messages(messages)
190
+ messages.each_with_index do |msg, index|
191
+ content = msg[:content]
192
+ recipient = msg[:recipient] || "all"
193
+
194
+ log_message("Sending message #{index + 1}: '#{content}' to #{recipient}")
195
+
196
+ message_data = {
197
+ type: "message",
198
+ data: {
199
+ content: content,
200
+ recipient: recipient,
201
+ },
202
+ timestamp: Time.now.to_i,
203
+ }
204
+
205
+ @socket.puts(JSON.generate(message_data))
206
+ log_message("Sent message to #{recipient}: #{content}")
207
+
208
+ # Small delay between messages for stability
209
+ sleep(0.1)
210
+ end
211
+ end
212
+
213
+ def disconnect
214
+ return unless @running
215
+
216
+ @running = false
217
+ log_message("Disconnecting from server")
218
+
219
+ if @socket && !@socket.closed?
220
+ # Send leave message
221
+ leave_data = {
222
+ type: "leave",
223
+ data: {
224
+ username: @username,
225
+ },
226
+ timestamp: Time.now.to_i,
227
+ }
228
+ @socket.puts(JSON.generate(leave_data))
229
+ log_message("Sent leave message")
230
+
231
+ @socket.close
232
+ end
233
+
234
+ @log_file&.close
235
+
236
+ puts "Disconnected from server."
237
+ true
238
+ end
239
+
240
+ private
241
+
242
+ def log_message(message)
243
+ timestamp = Time.now.strftime("%Y-%m-%d %H:%M:%S.%L")
244
+ log_entry = "[#{timestamp}] #{message}"
245
+
246
+ @log_file.puts(log_entry)
247
+ @log_file.flush # Ensure it's written immediately
248
+ end
249
+ end
250
+ end
251
+
252
+ # When run directly, start the client
253
+ if __FILE__ == $PROGRAM_NAME
254
+ require "fileutils"
255
+ require "json"
256
+
257
+ puts "Chat Client"
258
+ puts "==========="
259
+ puts "This is the chat client that connects to the chat server."
260
+ puts "All messages are logged to a file for later analysis."
261
+ puts
262
+
263
+ # Get username from command line or prompt
264
+ username = ARGV[0]
265
+
266
+ unless username
267
+ print "Enter your username: "
268
+ username = gets.chomp
269
+ end
270
+
271
+ # Get port from command line or use default
272
+ port = ARGV[1]&.to_i || 3000
273
+ log_file = ARGV[2] || "logs/client_#{username}_messages.log"
274
+
275
+ # Check for messages file
276
+ messages_file = "logs/client_#{username}_send_messages.json"
277
+
278
+ # Create and run the client
279
+ client = ContinuousChat::ChatClient.new(username, "localhost", port, log_file)
280
+
281
+ if client.connect
282
+ begin
283
+ # Load and send messages if the file exists
284
+ if File.exist?(messages_file)
285
+ puts "Loading messages from #{messages_file}"
286
+ messages = JSON.parse(File.read(messages_file), symbolize_names: true)
287
+ puts "Loaded #{messages.size} messages"
288
+
289
+ # Send the messages
290
+ client.run_with_messages(messages)
291
+ end
292
+
293
+ # Start the client
294
+ client.start
295
+ rescue Interrupt
296
+ puts "\nClient interrupted."
297
+ ensure
298
+ client.disconnect
299
+ end
300
+ end
301
+
302
+ puts "Chat client exited."
303
+ end