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,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
|