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,359 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "socket"
5
+ require "json"
6
+ require "time"
7
+ require "fileutils"
8
+ require_relative "../continuous_chat_common/message_protocol"
9
+
10
+ # Simple Chat Server using Ruby's standard socket library
11
+ # Based on the approach from https://dev.to/aurelieverrot/create-a-chat-in-the-command-line-with-ruby-2po9
12
+ # but modified to use JSON for message passing.
13
+ puts "Starting chat server..."
14
+
15
+ # Parse command line args
16
+ port = ARGV[0]&.to_i || 3000
17
+ log_file_path = ARGV[1] || "logs/server_messages.log"
18
+
19
+ # Create logs directory if it doesn't exist
20
+ FileUtils.mkdir_p(File.dirname(log_file_path))
21
+ log_file = File.open(log_file_path, "w")
22
+
23
+ def log_message(message, log_file)
24
+ timestamp = Time.now.strftime("%Y-%m-%d %H:%M:%S.%L")
25
+ log_entry = "[#{timestamp}] #{message}"
26
+
27
+ # Check if log file is still open before writing
28
+ if log_file && !log_file.closed?
29
+ log_file.puts(log_entry)
30
+ log_file.flush # Ensure it's written immediately
31
+ end
32
+
33
+ # Also print to console for debugging
34
+ puts log_entry
35
+ end
36
+
37
+ # Create the server socket
38
+ server = TCPServer.new("0.0.0.0", port)
39
+ log_message("Server started on port #{port}", log_file)
40
+ puts "Server bound to port #{port}"
41
+
42
+ # Array to store connected clients
43
+ clients = {}
44
+
45
+ # Broadcast a message to all clients
46
+ def announce_to_everyone(clients, message, log_file)
47
+ log_message("Broadcasting: #{message}", log_file)
48
+
49
+ # Convert to JSON if it's not already a string
50
+ message_json = message.is_a?(String) ? message : JSON.generate(message)
51
+
52
+ # Create a copy of the clients hash to avoid modification during iteration
53
+ clients_copy = clients.dup
54
+
55
+ clients_copy.each do |username, client|
56
+ client.puts(message_json)
57
+ rescue StandardError => e
58
+ log_message("Error broadcasting to #{username}: #{e.message}", log_file)
59
+ # We don't disconnect here, that's handled in the client thread
60
+ end
61
+ end
62
+
63
+ # Handle a new client joining the chat
64
+ def handle_new_client(clients, client, log_file)
65
+ line = client.gets&.chomp
66
+ return client.close unless line
67
+
68
+ log_message("Received join message: #{line}", log_file)
69
+
70
+ # Parse the join message
71
+ message = JSON.parse(line)
72
+
73
+ if message["type"] == "join" && message["data"] && message["data"]["username"]
74
+ username = message["data"]["username"]
75
+
76
+ # Store the client with username as key
77
+ clients[username] = client
78
+
79
+ # Send welcome message to the client
80
+ welcome_msg = {
81
+ type: "server_message",
82
+ data: {
83
+ message: "Hello #{username}! Connected clients: #{clients.count}",
84
+ },
85
+ timestamp: Time.now.to_i,
86
+ }
87
+ client.puts(JSON.generate(welcome_msg))
88
+
89
+ # Broadcast to all clients that a new user joined
90
+ join_msg = {
91
+ type: "server_message",
92
+ data: {
93
+ message: "#{username} joined the chat!",
94
+ },
95
+ timestamp: Time.now.to_i,
96
+ }
97
+ announce_to_everyone(clients, join_msg, log_file)
98
+
99
+ # Broadcast the updated user list
100
+ user_list_msg = {
101
+ type: "user_list",
102
+ data: {
103
+ users: clients.keys,
104
+ },
105
+ timestamp: Time.now.to_i,
106
+ }
107
+ announce_to_everyone(clients, user_list_msg, log_file)
108
+
109
+ true
110
+ else
111
+ # Invalid join message
112
+ error_msg = {
113
+ type: "error",
114
+ data: {
115
+ message: "First message must be a valid join command",
116
+ },
117
+ timestamp: Time.now.to_i,
118
+ }
119
+ client.puts(JSON.generate(error_msg))
120
+ client.close
121
+ false
122
+ end
123
+ rescue JSON::ParserError => e
124
+ log_message("Error parsing initial join message: #{e.message}", log_file)
125
+ client.puts(JSON.generate({
126
+ type: "error",
127
+ data: {
128
+ message: "Invalid JSON format in join message",
129
+ },
130
+ timestamp: Time.now.to_i,
131
+ }))
132
+ client.close
133
+ false
134
+ rescue StandardError => e
135
+ log_message("Error handling new client: #{e.message}", log_file)
136
+ client.close
137
+ false
138
+ end
139
+
140
+ # Process a message from an existing client
141
+ def process_client_message(clients, client, username, log_file)
142
+ # Read a message from the client (non-blocking with timeout)
143
+ readable, = IO.select([client], nil, nil, 0)
144
+ return true unless readable # No data to read yet
145
+
146
+ line = client.gets&.chomp
147
+ return false unless line # Client disconnected
148
+
149
+ begin
150
+ message = JSON.parse(line)
151
+ log_message("Received from #{username}: #{line}", log_file)
152
+
153
+ case message["type"]
154
+ when "message"
155
+ content = message["data"]["content"]
156
+ recipient = message["data"]["recipient"] || "all"
157
+
158
+ if content.start_with?("/")
159
+ # Handle commands
160
+ case content
161
+ when "/list"
162
+ list_msg = {
163
+ type: "user_list",
164
+ data: {
165
+ users: clients.keys,
166
+ },
167
+ timestamp: Time.now.to_i,
168
+ }
169
+ client.puts(JSON.generate(list_msg))
170
+ else
171
+ # Unknown command
172
+ client.puts(JSON.generate({
173
+ type: "error",
174
+ data: {
175
+ message: "Unknown command: #{content}",
176
+ },
177
+ timestamp: Time.now.to_i,
178
+ }))
179
+ end
180
+ elsif recipient == "all"
181
+ # Broadcast message
182
+ broadcast_msg = {
183
+ type: "broadcast",
184
+ data: {
185
+ from: username,
186
+ content: content,
187
+ },
188
+ timestamp: Time.now.to_i,
189
+ }
190
+ announce_to_everyone(clients, broadcast_msg, log_file)
191
+ elsif clients[recipient]
192
+ # Direct message
193
+ dm_msg = {
194
+ type: "direct_message",
195
+ data: {
196
+ from: username,
197
+ content: content,
198
+ },
199
+ timestamp: Time.now.to_i,
200
+ }
201
+ clients[recipient].puts(JSON.generate(dm_msg))
202
+ # Also send to sender if not the same person
203
+ client.puts(JSON.generate(dm_msg)) if username != recipient
204
+ else
205
+ # Recipient not found
206
+ client.puts(JSON.generate({
207
+ type: "error",
208
+ data: {
209
+ message: "User #{recipient} not found",
210
+ },
211
+ timestamp: Time.now.to_i,
212
+ }))
213
+ end
214
+ when "leave"
215
+ # Client wants to leave
216
+ return false
217
+ end
218
+ rescue JSON::ParserError => e
219
+ log_message("Error parsing JSON from #{username}: #{e.message}", log_file)
220
+ client.puts(JSON.generate({
221
+ type: "error",
222
+ data: {
223
+ message: "Invalid JSON format",
224
+ },
225
+ timestamp: Time.now.to_i,
226
+ }))
227
+ rescue StandardError => e
228
+ log_message("Error processing message from #{username}: #{e.message}",
229
+ log_file)
230
+ return false
231
+ end
232
+
233
+ true # Client still connected
234
+ end
235
+
236
+ # Handle client disconnection
237
+ def handle_client_disconnect(clients, client, log_file)
238
+ username = clients.key(client)
239
+ return unless username
240
+
241
+ # Remove from clients list
242
+ clients.delete(username)
243
+ log_message("Client disconnected: #{username}", log_file)
244
+
245
+ # Notify everyone
246
+ leave_msg = {
247
+ type: "server_message",
248
+ data: {
249
+ message: "#{username} left the chat.",
250
+ },
251
+ timestamp: Time.now.to_i,
252
+ }
253
+ announce_to_everyone(clients, leave_msg, log_file)
254
+
255
+ # Update user list
256
+ user_list_msg = {
257
+ type: "user_list",
258
+ data: {
259
+ users: clients.keys,
260
+ },
261
+ timestamp: Time.now.to_i,
262
+ }
263
+ announce_to_everyone(clients, user_list_msg, log_file)
264
+
265
+ # Close the client socket
266
+ begin
267
+ client.close
268
+ rescue StandardError
269
+ nil
270
+ end
271
+ end
272
+
273
+ # Main server loop
274
+ begin
275
+ log_message("Server ready to accept connections", log_file)
276
+
277
+ # Add the server socket to the list of sockets to monitor
278
+ sockets = [server]
279
+
280
+ # Main event loop
281
+ loop do
282
+ # Use IO.select to check which sockets have data to read
283
+ # This is non-blocking and allows us to handle multiple clients sequentially
284
+ readable, _, errored = IO.select(sockets, [], sockets, 0.1)
285
+
286
+ # Handle errors first
287
+ if errored && !errored.empty?
288
+ errored.each do |socket|
289
+ raise "Server socket error" if socket == server
290
+
291
+ # Server socket error - critical
292
+
293
+ # Client socket error
294
+ username = clients.key(socket)
295
+ log_message("Error on client socket: #{username || 'unknown'}",
296
+ log_file)
297
+ handle_client_disconnect(clients, socket, log_file)
298
+ sockets.delete(socket)
299
+ end
300
+ end
301
+
302
+ # Nothing to process this iteration
303
+ next unless readable
304
+
305
+ # Process readable sockets
306
+ readable.each do |socket|
307
+ if socket == server
308
+ # New client connection
309
+ begin
310
+ client = server.accept
311
+ log_message(
312
+ "New client connection from #{client.peeraddr[2]}:#{client.peeraddr[1]}", log_file
313
+ )
314
+
315
+ # Add the client socket to our monitoring list
316
+ sockets << client
317
+
318
+ # We'll process the initial join message in the next iteration
319
+ rescue StandardError => e
320
+ log_message("Error accepting client: #{e.message}", log_file)
321
+ end
322
+ elsif clients.key(socket)
323
+ # Existing client sent data
324
+ username = clients.key(socket)
325
+
326
+ # Process message, remove client if it disconnected
327
+ unless process_client_message(clients, socket, username, log_file)
328
+ handle_client_disconnect(clients, socket, log_file)
329
+ sockets.delete(socket)
330
+ end
331
+ else
332
+ # This is a new client that needs to send their join message
333
+ unless handle_new_client(clients, socket, log_file)
334
+ # Join failed, remove from sockets
335
+ sockets.delete(socket)
336
+ end
337
+ end
338
+ end
339
+ end
340
+ rescue Interrupt
341
+ log_message("Server interrupted, shutting down...", log_file)
342
+ rescue StandardError => e
343
+ log_message("Server error: #{e.message}", log_file)
344
+ ensure
345
+ # Close all client connections
346
+ clients.each_value do |client|
347
+ client.close
348
+ rescue StandardError
349
+ # Ignore errors when closing
350
+ end
351
+
352
+ # Close the server socket
353
+ server&.close
354
+
355
+ # Close the log file
356
+ log_file&.close
357
+
358
+ log_message("Server stopped", log_file)
359
+ end