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