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
@@ -32,7 +32,7 @@
32
32
  #
33
33
  # =============================================================================
34
34
 
35
- require_relative "../../lib/fractor"
35
+ require_relative '../../lib/fractor'
36
36
 
37
37
  # Client-specific work item implementation inheriting from Fractor::Work
38
38
  class MyWork < Fractor::Work
@@ -72,14 +72,14 @@ class MyWorker < Fractor::Worker
72
72
  # It should return a Fractor::WorkResult object
73
73
  def process(work)
74
74
  # Only print debug information if FRACTOR_DEBUG is enabled
75
- puts "Working on '#{work.inspect}'" if ENV["FRACTOR_DEBUG"]
75
+ puts "Working on '#{work.inspect}'" if ENV['FRACTOR_DEBUG']
76
76
 
77
77
  # Check work type and handle accordingly
78
78
  if work.is_a?(MyWork)
79
79
  if work.value == 5
80
80
  # Return a Fractor::WorkResult for errors
81
81
  # Store the error object, not just the string
82
- error = StandardError.new("Cannot process value 5")
82
+ error = StandardError.new('Cannot process value 5')
83
83
  return Fractor::WorkResult.new(error: error, work: work)
84
84
  end
85
85
 
@@ -116,8 +116,8 @@ if __FILE__ == $PROGRAM_NAME
116
116
  # Run the supervisor to start processing work
117
117
  supervisor.run
118
118
 
119
- puts "Processing complete."
120
- puts "Final Aggregated Results:"
119
+ puts 'Processing complete.'
120
+ puts 'Final Aggregated Results:'
121
121
  # Access the results aggregator from the supervisor
122
122
  puts supervisor.results.inspect
123
123
 
@@ -9,7 +9,7 @@ module SpecializedWorkers
9
9
  super({
10
10
  data: data,
11
11
  operation: operation,
12
- parameters: parameters
12
+ parameters: parameters,
13
13
  })
14
14
  end
15
15
 
@@ -32,12 +32,13 @@ module SpecializedWorkers
32
32
 
33
33
  # Second work type: Database operations
34
34
  class DatabaseWork < Fractor::Work
35
- def initialize(data = "", query_type = :select, table = "unknown", conditions = {})
35
+ def initialize(data = "", query_type = :select, table = "unknown",
36
+ conditions = {})
36
37
  super({
37
38
  data: data,
38
39
  query_type: query_type,
39
40
  table: table,
40
- conditions: conditions
41
+ conditions: conditions,
41
42
  })
42
43
  end
43
44
 
@@ -74,14 +75,16 @@ module SpecializedWorkers
74
75
  unless work.is_a?(ComputeWork)
75
76
  return Fractor::WorkResult.new(
76
77
  error: "ComputeWorker can only process ComputeWork, got: #{work.class}",
77
- work: work
78
+ work: work,
78
79
  )
79
80
  end
80
81
 
81
82
  # Process based on the requested operation
82
83
  result = case work.operation
83
- when :matrix_multiply then matrix_multiply(work.data, work.parameters)
84
- when :image_transform then image_transform(work.data, work.parameters)
84
+ when :matrix_multiply then matrix_multiply(work.data,
85
+ work.parameters)
86
+ when :image_transform then image_transform(work.data,
87
+ work.parameters)
85
88
  when :path_finding then path_finding(work.data, work.parameters)
86
89
  else default_computation(work.data, work.parameters)
87
90
  end
@@ -90,9 +93,9 @@ module SpecializedWorkers
90
93
  result: {
91
94
  operation: work.operation,
92
95
  computation_result: result,
93
- resources_used: @compute_resources
96
+ resources_used: @compute_resources,
94
97
  },
95
- work: work
98
+ work: work,
96
99
  )
97
100
  end
98
101
 
@@ -110,7 +113,7 @@ module SpecializedWorkers
110
113
  # Simulate image transformation
111
114
  sleep(rand(0.1..0.3))
112
115
  transforms = params[:transforms] || %i[rotate scale]
113
- "Image transformation applied: #{transforms.join(", ")} with parameters #{params}"
116
+ "Image transformation applied: #{transforms.join(', ')} with parameters #{params}"
114
117
  end
115
118
 
116
119
  def path_finding(_data, params)
@@ -140,7 +143,7 @@ module SpecializedWorkers
140
143
  unless work.is_a?(DatabaseWork)
141
144
  return Fractor::WorkResult.new(
142
145
  error: "DatabaseWorker can only process DatabaseWork, got: #{work.class}",
143
- work: work
146
+ work: work,
144
147
  )
145
148
  end
146
149
 
@@ -148,7 +151,8 @@ module SpecializedWorkers
148
151
  result = case work.query_type
149
152
  when :select then perform_select(work.table, work.conditions)
150
153
  when :insert then perform_insert(work.table, work.data)
151
- when :update then perform_update(work.table, work.data, work.conditions)
154
+ when :update then perform_update(work.table, work.data,
155
+ work.conditions)
152
156
  when :delete then perform_delete(work.table, work.conditions)
153
157
  else default_query(work.query_type, work.table, work.conditions)
154
158
  end
@@ -159,9 +163,9 @@ module SpecializedWorkers
159
163
  table: work.table,
160
164
  rows_affected: result[:rows_affected],
161
165
  data: result[:data],
162
- execution_time: result[:time]
166
+ execution_time: result[:time],
163
167
  },
164
- work: work
168
+ work: work,
165
169
  )
166
170
  end
167
171
 
@@ -174,8 +178,10 @@ module SpecializedWorkers
174
178
  record_count = rand(0..20)
175
179
  {
176
180
  rows_affected: record_count,
177
- data: record_count.times.map { |i| { id: i + 1, name: "Record #{i + 1}" } },
178
- time: rand(0.01..0.05)
181
+ data: Array.new(record_count) do |i|
182
+ { id: i + 1, name: "Record #{i + 1}" }
183
+ end,
184
+ time: rand(0.01..0.05),
179
185
  }
180
186
  end
181
187
 
@@ -185,7 +191,7 @@ module SpecializedWorkers
185
191
  {
186
192
  rows_affected: 1,
187
193
  data: { id: rand(1000..9999) },
188
- time: rand(0.01..0.03)
194
+ time: rand(0.01..0.03),
189
195
  }
190
196
  end
191
197
 
@@ -196,7 +202,7 @@ module SpecializedWorkers
196
202
  {
197
203
  rows_affected: affected,
198
204
  data: nil,
199
- time: rand(0.01..0.05)
205
+ time: rand(0.01..0.05),
200
206
  }
201
207
  end
202
208
 
@@ -207,7 +213,7 @@ module SpecializedWorkers
207
213
  {
208
214
  rows_affected: affected,
209
215
  data: nil,
210
- time: rand(0.01..0.03)
216
+ time: rand(0.01..0.03),
211
217
  }
212
218
  end
213
219
 
@@ -217,7 +223,7 @@ module SpecializedWorkers
217
223
  {
218
224
  rows_affected: 0,
219
225
  data: nil,
220
- time: rand(0.005..0.01)
226
+ time: rand(0.005..0.01),
221
227
  }
222
228
  end
223
229
  end
@@ -230,14 +236,14 @@ module SpecializedWorkers
230
236
  # Create separate supervisors for each worker type
231
237
  @compute_supervisor = Fractor::Supervisor.new(
232
238
  worker_pools: [
233
- { worker_class: ComputeWorker, num_workers: compute_workers }
234
- ]
239
+ { worker_class: ComputeWorker, num_workers: compute_workers },
240
+ ],
235
241
  )
236
242
 
237
243
  @db_supervisor = Fractor::Supervisor.new(
238
244
  worker_pools: [
239
- { worker_class: DatabaseWorker, num_workers: db_workers }
240
- ]
245
+ { worker_class: DatabaseWorker, num_workers: db_workers },
246
+ ],
241
247
  )
242
248
 
243
249
  @compute_results = []
@@ -253,7 +259,8 @@ module SpecializedWorkers
253
259
 
254
260
  # Create and add database work items
255
261
  db_work_items = db_tasks.map do |task|
256
- DatabaseWork.new(task[:data], task[:query_type], task[:table], task[:conditions])
262
+ DatabaseWork.new(task[:data], task[:query_type], task[:table],
263
+ task[:conditions])
257
264
  end
258
265
  @db_supervisor.add_work_items(db_work_items)
259
266
 
@@ -277,13 +284,13 @@ module SpecializedWorkers
277
284
  computation: {
278
285
  tasks: compute_tasks.size,
279
286
  completed: @compute_results.size,
280
- results: @compute_results
287
+ results: @compute_results,
281
288
  },
282
289
  database: {
283
290
  tasks: db_tasks.size,
284
291
  completed: @db_results.size,
285
- results: @db_results
286
- }
292
+ results: @db_results,
293
+ },
287
294
  }
288
295
  end
289
296
 
@@ -316,18 +323,18 @@ if __FILE__ == $PROGRAM_NAME
316
323
  {
317
324
  operation: :matrix_multiply,
318
325
  data: "Matrix data...",
319
- parameters: { size: [10, 10] }
326
+ parameters: { size: [10, 10] },
320
327
  },
321
328
  {
322
329
  operation: :image_transform,
323
330
  data: "Image data...",
324
- parameters: { transforms: %i[rotate scale blur], angle: 45, scale: 1.5 }
331
+ parameters: { transforms: %i[rotate scale blur], angle: 45, scale: 1.5 },
325
332
  },
326
333
  {
327
334
  operation: :path_finding,
328
335
  data: "Graph data...",
329
- parameters: { algorithm: :dijkstra, nodes: 20, start: 1, end: 15 }
330
- }
336
+ parameters: { algorithm: :dijkstra, nodes: 20, start: 1, end: 15 },
337
+ },
331
338
  ]
332
339
 
333
340
  # Prepare database tasks
@@ -335,24 +342,24 @@ if __FILE__ == $PROGRAM_NAME
335
342
  {
336
343
  query_type: :select,
337
344
  table: "users",
338
- conditions: { active: true, role: "admin" }
345
+ conditions: { active: true, role: "admin" },
339
346
  },
340
347
  {
341
348
  query_type: :insert,
342
349
  table: "orders",
343
- data: "Order data..."
350
+ data: "Order data...",
344
351
  },
345
352
  {
346
353
  query_type: :update,
347
354
  table: "products",
348
355
  data: "Product data...",
349
- conditions: { category: "electronics" }
356
+ conditions: { category: "electronics" },
350
357
  },
351
358
  {
352
359
  query_type: :delete,
353
360
  table: "sessions",
354
- conditions: { expired: true }
355
- }
361
+ conditions: { expired: true },
362
+ },
356
363
  ]
357
364
 
358
365
  compute_workers = 2
@@ -363,7 +370,7 @@ if __FILE__ == $PROGRAM_NAME
363
370
  start_time = Time.now
364
371
  system = SpecializedWorkers::HybridSystem.new(
365
372
  compute_workers: compute_workers,
366
- db_workers: db_workers
373
+ db_workers: db_workers,
367
374
  )
368
375
  result = system.process_mixed_workload(compute_tasks, db_tasks)
369
376
  end_time = Time.now
@@ -0,0 +1,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module Fractor
6
+ # High-level wrapper for running Fractor in continuous mode.
7
+ # Handles threading, signal handling, and results processing automatically.
8
+ class ContinuousServer
9
+ attr_reader :supervisor, :work_queue
10
+
11
+ # Initialize a continuous server
12
+ # @param worker_pools [Array<Hash>] Worker pool configurations
13
+ # @param work_queue [WorkQueue, nil] Optional work queue to auto-register
14
+ # @param log_file [String, nil] Optional log file path
15
+ def initialize(worker_pools:, work_queue: nil, log_file: nil)
16
+ @worker_pools = worker_pools
17
+ @work_queue = work_queue
18
+ @log_file_path = log_file
19
+ @log_file = nil
20
+ @result_callbacks = []
21
+ @error_callbacks = []
22
+ @supervisor = nil
23
+ @supervisor_thread = nil
24
+ @results_thread = nil
25
+ @running = false
26
+ end
27
+
28
+ # Register a callback for successful results
29
+ # @yield [WorkResult] The successful result
30
+ def on_result(&block)
31
+ @result_callbacks << block
32
+ end
33
+
34
+ # Register a callback for errors
35
+ # @yield [WorkResult] The error result
36
+ def on_error(&block)
37
+ @error_callbacks << block
38
+ end
39
+
40
+ # Start the server and block until shutdown
41
+ # This method handles:
42
+ # - Opening log file if specified
43
+ # - Creating and starting supervisor
44
+ # - Starting results processing thread
45
+ # - Setting up signal handlers
46
+ # - Blocking until shutdown signal received
47
+ def run
48
+ setup_log_file
49
+ setup_supervisor
50
+ start_supervisor_thread
51
+ start_results_thread
52
+
53
+ log_message("Continuous server started")
54
+ log_message("Press Ctrl+C to stop")
55
+
56
+ begin
57
+ # Block until shutdown
58
+ @supervisor_thread&.join
59
+ rescue Interrupt
60
+ log_message("Interrupt received, shutting down...")
61
+ ensure
62
+ cleanup
63
+ end
64
+ end
65
+
66
+ # Stop the server programmatically
67
+ def stop
68
+ return unless @running
69
+
70
+ log_message("Stopping continuous server...")
71
+ @running = false
72
+
73
+ @supervisor&.stop
74
+
75
+ # Wait for threads to finish
76
+ [@supervisor_thread, @results_thread].compact.each do |thread|
77
+ thread.join(2) if thread.alive?
78
+ end
79
+
80
+ log_message("Continuous server stopped")
81
+ end
82
+
83
+ private
84
+
85
+ def setup_log_file
86
+ return unless @log_file_path
87
+
88
+ FileUtils.mkdir_p(File.dirname(@log_file_path))
89
+ @log_file = File.open(@log_file_path, "w")
90
+ end
91
+
92
+ def setup_supervisor
93
+ @supervisor = Supervisor.new(
94
+ worker_pools: @worker_pools,
95
+ continuous_mode: true,
96
+ )
97
+
98
+ # Auto-register work queue if provided
99
+ if @work_queue
100
+ @work_queue.register_with_supervisor(@supervisor)
101
+ log_message(
102
+ "Work queue registered with supervisor (batch size: 10)",
103
+ )
104
+ end
105
+ end
106
+
107
+ def start_supervisor_thread
108
+ @running = true
109
+ @supervisor_thread = Thread.new do
110
+ @supervisor.run
111
+ rescue StandardError => e
112
+ log_message("Supervisor error: #{e.message}")
113
+ log_message(e.backtrace.join("\n")) if ENV["FRACTOR_DEBUG"]
114
+ end
115
+
116
+ # Give supervisor time to start up
117
+ sleep(0.1)
118
+ end
119
+
120
+ def start_results_thread
121
+ @results_thread = Thread.new do
122
+ log_message("Results processing thread started")
123
+ process_results_loop
124
+ rescue StandardError => e
125
+ log_message("Results thread error: #{e.message}")
126
+ log_message(e.backtrace.join("\n")) if ENV["FRACTOR_DEBUG"]
127
+ end
128
+ end
129
+
130
+ def process_results_loop
131
+ while @running
132
+ sleep(0.05)
133
+
134
+ process_successful_results
135
+ process_error_results
136
+ end
137
+ log_message("Results processing thread stopped")
138
+ end
139
+
140
+ def process_successful_results
141
+ loop do
142
+ result = @supervisor.results.results.shift
143
+ break unless result
144
+
145
+ @result_callbacks.each do |callback|
146
+ callback.call(result)
147
+ rescue StandardError => e
148
+ log_message("Error in result callback: #{e.message}")
149
+ end
150
+ end
151
+ end
152
+
153
+ def process_error_results
154
+ loop do
155
+ error_result = @supervisor.results.errors.shift
156
+ break unless error_result
157
+
158
+ @error_callbacks.each do |callback|
159
+ callback.call(error_result)
160
+ rescue StandardError => e
161
+ log_message("Error in error callback: #{e.message}")
162
+ end
163
+ end
164
+ end
165
+
166
+ def cleanup
167
+ @running = false
168
+
169
+ # Close log file if open
170
+ if @log_file && !@log_file.closed?
171
+ @log_file.close
172
+ @log_file = nil
173
+ end
174
+ end
175
+
176
+ def log_message(message)
177
+ timestamp = Time.now.strftime("%Y-%m-%d %H:%M:%S.%L")
178
+ log_entry = "[#{timestamp}] #{message}"
179
+
180
+ if @log_file && !@log_file.closed?
181
+ @log_file.puts(log_entry)
182
+ @log_file.flush
183
+ end
184
+
185
+ puts log_entry
186
+ end
187
+ end
188
+ end
@@ -36,7 +36,7 @@ module Fractor
36
36
  def inspect
37
37
  {
38
38
  results: @results.map(&:inspect),
39
- errors: @errors.map(&:inspect)
39
+ errors: @errors.map(&:inspect),
40
40
  }
41
41
  end
42
42
  end