fractor 0.1.6 → 0.1.7

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 (172) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +227 -102
  3. data/README.adoc +113 -1940
  4. data/docs/.lycheeignore +16 -0
  5. data/docs/Gemfile +24 -0
  6. data/docs/README.md +157 -0
  7. data/docs/_config.yml +151 -0
  8. data/docs/_features/error-handling.adoc +1192 -0
  9. data/docs/_features/index.adoc +80 -0
  10. data/docs/_features/monitoring.adoc +589 -0
  11. data/docs/_features/signal-handling.adoc +202 -0
  12. data/docs/_features/workflows.adoc +1235 -0
  13. data/docs/_guides/continuous-mode.adoc +736 -0
  14. data/docs/_guides/cookbook.adoc +1133 -0
  15. data/docs/_guides/index.adoc +55 -0
  16. data/docs/_guides/pipeline-mode.adoc +730 -0
  17. data/docs/_guides/troubleshooting.adoc +358 -0
  18. data/docs/_pages/architecture.adoc +1390 -0
  19. data/docs/_pages/core-concepts.adoc +1392 -0
  20. data/docs/_pages/design-principles.adoc +862 -0
  21. data/docs/_pages/getting-started.adoc +290 -0
  22. data/docs/_pages/installation.adoc +143 -0
  23. data/docs/_reference/api.adoc +1080 -0
  24. data/docs/_reference/error-reporting.adoc +670 -0
  25. data/docs/_reference/examples.adoc +181 -0
  26. data/docs/_reference/index.adoc +96 -0
  27. data/docs/_reference/troubleshooting.adoc +862 -0
  28. data/docs/_tutorials/complex-workflows.adoc +1022 -0
  29. data/docs/_tutorials/data-processing-pipeline.adoc +740 -0
  30. data/docs/_tutorials/first-application.adoc +384 -0
  31. data/docs/_tutorials/index.adoc +48 -0
  32. data/docs/_tutorials/long-running-services.adoc +931 -0
  33. data/docs/assets/images/favicon-16.png +0 -0
  34. data/docs/assets/images/favicon-32.png +0 -0
  35. data/docs/assets/images/favicon-48.png +0 -0
  36. data/docs/assets/images/favicon.ico +0 -0
  37. data/docs/assets/images/favicon.png +0 -0
  38. data/docs/assets/images/favicon.svg +45 -0
  39. data/docs/assets/images/fractor-icon.svg +49 -0
  40. data/docs/assets/images/fractor-logo.svg +61 -0
  41. data/docs/index.adoc +131 -0
  42. data/docs/lychee.toml +39 -0
  43. data/examples/api_aggregator/README.adoc +627 -0
  44. data/examples/api_aggregator/api_aggregator.rb +376 -0
  45. data/examples/auto_detection/README.adoc +407 -29
  46. data/examples/continuous_chat_common/message_protocol.rb +1 -1
  47. data/examples/error_reporting.rb +207 -0
  48. data/examples/file_processor/README.adoc +170 -0
  49. data/examples/file_processor/file_processor.rb +615 -0
  50. data/examples/file_processor/sample_files/invalid.csv +1 -0
  51. data/examples/file_processor/sample_files/orders.xml +24 -0
  52. data/examples/file_processor/sample_files/products.json +23 -0
  53. data/examples/file_processor/sample_files/users.csv +6 -0
  54. data/examples/hierarchical_hasher/README.adoc +629 -41
  55. data/examples/image_processor/README.adoc +610 -0
  56. data/examples/image_processor/image_processor.rb +349 -0
  57. data/examples/image_processor/processed_images/sample_10_processed.jpg.json +12 -0
  58. data/examples/image_processor/processed_images/sample_1_processed.jpg.json +12 -0
  59. data/examples/image_processor/processed_images/sample_2_processed.jpg.json +12 -0
  60. data/examples/image_processor/processed_images/sample_3_processed.jpg.json +12 -0
  61. data/examples/image_processor/processed_images/sample_4_processed.jpg.json +12 -0
  62. data/examples/image_processor/processed_images/sample_5_processed.jpg.json +12 -0
  63. data/examples/image_processor/processed_images/sample_6_processed.jpg.json +12 -0
  64. data/examples/image_processor/processed_images/sample_7_processed.jpg.json +12 -0
  65. data/examples/image_processor/processed_images/sample_8_processed.jpg.json +12 -0
  66. data/examples/image_processor/processed_images/sample_9_processed.jpg.json +12 -0
  67. data/examples/image_processor/test_images/sample_1.png +1 -0
  68. data/examples/image_processor/test_images/sample_10.png +1 -0
  69. data/examples/image_processor/test_images/sample_2.png +1 -0
  70. data/examples/image_processor/test_images/sample_3.png +1 -0
  71. data/examples/image_processor/test_images/sample_4.png +1 -0
  72. data/examples/image_processor/test_images/sample_5.png +1 -0
  73. data/examples/image_processor/test_images/sample_6.png +1 -0
  74. data/examples/image_processor/test_images/sample_7.png +1 -0
  75. data/examples/image_processor/test_images/sample_8.png +1 -0
  76. data/examples/image_processor/test_images/sample_9.png +1 -0
  77. data/examples/log_analyzer/README.adoc +662 -0
  78. data/examples/log_analyzer/log_analyzer.rb +579 -0
  79. data/examples/log_analyzer/sample_logs/apache.log +20 -0
  80. data/examples/log_analyzer/sample_logs/json.log +15 -0
  81. data/examples/log_analyzer/sample_logs/nginx.log +15 -0
  82. data/examples/log_analyzer/sample_logs/rails.log +29 -0
  83. data/examples/multi_work_type/README.adoc +576 -26
  84. data/examples/performance_monitoring.rb +120 -0
  85. data/examples/pipeline_processing/README.adoc +740 -26
  86. data/examples/pipeline_processing/pipeline_processing.rb +2 -2
  87. data/examples/priority_work_example.rb +155 -0
  88. data/examples/producer_subscriber/README.adoc +889 -46
  89. data/examples/scatter_gather/README.adoc +829 -27
  90. data/examples/simple/README.adoc +347 -0
  91. data/examples/specialized_workers/README.adoc +622 -26
  92. data/examples/specialized_workers/specialized_workers.rb +44 -8
  93. data/examples/stream_processor/README.adoc +206 -0
  94. data/examples/stream_processor/stream_processor.rb +284 -0
  95. data/examples/web_scraper/README.adoc +625 -0
  96. data/examples/web_scraper/web_scraper.rb +285 -0
  97. data/examples/workflow/README.adoc +406 -0
  98. data/examples/workflow/circuit_breaker/README.adoc +360 -0
  99. data/examples/workflow/circuit_breaker/circuit_breaker_workflow.rb +225 -0
  100. data/examples/workflow/conditional/README.adoc +483 -0
  101. data/examples/workflow/conditional/conditional_workflow.rb +215 -0
  102. data/examples/workflow/dead_letter_queue/README.adoc +374 -0
  103. data/examples/workflow/dead_letter_queue/dead_letter_queue_workflow.rb +217 -0
  104. data/examples/workflow/fan_out/README.adoc +381 -0
  105. data/examples/workflow/fan_out/fan_out_workflow.rb +202 -0
  106. data/examples/workflow/retry/README.adoc +248 -0
  107. data/examples/workflow/retry/retry_workflow.rb +195 -0
  108. data/examples/workflow/simple_linear/README.adoc +267 -0
  109. data/examples/workflow/simple_linear/simple_linear_workflow.rb +175 -0
  110. data/examples/workflow/simplified/README.adoc +329 -0
  111. data/examples/workflow/simplified/simplified_workflow.rb +222 -0
  112. data/exe/fractor +10 -0
  113. data/lib/fractor/cli.rb +288 -0
  114. data/lib/fractor/configuration.rb +307 -0
  115. data/lib/fractor/continuous_server.rb +60 -65
  116. data/lib/fractor/error_formatter.rb +72 -0
  117. data/lib/fractor/error_report_generator.rb +152 -0
  118. data/lib/fractor/error_reporter.rb +244 -0
  119. data/lib/fractor/error_statistics.rb +147 -0
  120. data/lib/fractor/execution_tracer.rb +162 -0
  121. data/lib/fractor/logger.rb +230 -0
  122. data/lib/fractor/main_loop_handler.rb +406 -0
  123. data/lib/fractor/main_loop_handler3.rb +135 -0
  124. data/lib/fractor/main_loop_handler4.rb +299 -0
  125. data/lib/fractor/performance_metrics_collector.rb +181 -0
  126. data/lib/fractor/performance_monitor.rb +215 -0
  127. data/lib/fractor/performance_report_generator.rb +202 -0
  128. data/lib/fractor/priority_work.rb +93 -0
  129. data/lib/fractor/priority_work_queue.rb +189 -0
  130. data/lib/fractor/result_aggregator.rb +32 -0
  131. data/lib/fractor/shutdown_handler.rb +168 -0
  132. data/lib/fractor/signal_handler.rb +80 -0
  133. data/lib/fractor/supervisor.rb +382 -269
  134. data/lib/fractor/supervisor_logger.rb +88 -0
  135. data/lib/fractor/version.rb +1 -1
  136. data/lib/fractor/work.rb +12 -0
  137. data/lib/fractor/work_distribution_manager.rb +151 -0
  138. data/lib/fractor/work_queue.rb +20 -0
  139. data/lib/fractor/work_result.rb +181 -9
  140. data/lib/fractor/worker.rb +73 -0
  141. data/lib/fractor/workflow/builder.rb +210 -0
  142. data/lib/fractor/workflow/chain_builder.rb +169 -0
  143. data/lib/fractor/workflow/circuit_breaker.rb +183 -0
  144. data/lib/fractor/workflow/circuit_breaker_orchestrator.rb +208 -0
  145. data/lib/fractor/workflow/circuit_breaker_registry.rb +112 -0
  146. data/lib/fractor/workflow/dead_letter_queue.rb +334 -0
  147. data/lib/fractor/workflow/execution_hooks.rb +39 -0
  148. data/lib/fractor/workflow/execution_strategy.rb +225 -0
  149. data/lib/fractor/workflow/execution_trace.rb +134 -0
  150. data/lib/fractor/workflow/helpers.rb +191 -0
  151. data/lib/fractor/workflow/job.rb +290 -0
  152. data/lib/fractor/workflow/job_dependency_validator.rb +120 -0
  153. data/lib/fractor/workflow/logger.rb +110 -0
  154. data/lib/fractor/workflow/pre_execution_context.rb +193 -0
  155. data/lib/fractor/workflow/retry_config.rb +156 -0
  156. data/lib/fractor/workflow/retry_orchestrator.rb +184 -0
  157. data/lib/fractor/workflow/retry_strategy.rb +93 -0
  158. data/lib/fractor/workflow/structured_logger.rb +30 -0
  159. data/lib/fractor/workflow/type_compatibility_validator.rb +222 -0
  160. data/lib/fractor/workflow/visualizer.rb +211 -0
  161. data/lib/fractor/workflow/workflow_context.rb +132 -0
  162. data/lib/fractor/workflow/workflow_executor.rb +669 -0
  163. data/lib/fractor/workflow/workflow_result.rb +55 -0
  164. data/lib/fractor/workflow/workflow_validator.rb +295 -0
  165. data/lib/fractor/workflow.rb +333 -0
  166. data/lib/fractor/wrapped_ractor.rb +66 -101
  167. data/lib/fractor/wrapped_ractor3.rb +161 -0
  168. data/lib/fractor/wrapped_ractor4.rb +242 -0
  169. data/lib/fractor.rb +92 -4
  170. metadata +179 -6
  171. data/tests/sample.rb.bak +0 -309
  172. data/tests/sample_working.rb.bak +0 -209
@@ -0,0 +1,406 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fractor
4
+ # Handles the main event loop for a Supervisor.
5
+ # Responsible for processing Ractor messages and coordinating work distribution.
6
+ #
7
+ # This class extracts the main loop logic from Supervisor to follow
8
+ # the Single Responsibility Principle.
9
+ class MainLoopHandler
10
+ def initialize(supervisor, debug: false)
11
+ @supervisor = supervisor
12
+ @debug = debug
13
+ @shutting_down = false
14
+ end
15
+
16
+ # Factory method to create the appropriate MainLoopHandler implementation
17
+ # based on the current Ruby version.
18
+ #
19
+ # @param supervisor [Fractor::Supervisor] The supervisor instance
20
+ # @param debug [Boolean] Whether debug mode is enabled
21
+ # @return [MainLoopHandler] The appropriate subclass instance
22
+ def self.create(supervisor, debug: false)
23
+ ruby_4_0 = Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("4.0.0")
24
+ if ruby_4_0
25
+ MainLoopHandler4.new(supervisor, debug: debug)
26
+ else
27
+ MainLoopHandler3.new(supervisor, debug: debug)
28
+ end
29
+ end
30
+
31
+ # Run the main event loop.
32
+ # This method blocks until all work is processed (batch mode) or until stopped (continuous mode).
33
+ #
34
+ # @return [void]
35
+ def run_loop
36
+ raise NotImplementedError, "Subclasses must implement #run_loop"
37
+ end
38
+
39
+ # Clean up the ractors map after batch processing.
40
+ # This is critical on Windows Ruby 3.4 where workers may not respond to shutdown
41
+ # if they're stuck in Ractor.receive.
42
+ #
43
+ # @return [void]
44
+ def cleanup_ractors_map
45
+ return if ractors_map.empty?
46
+
47
+ puts "Cleaning up ractors map (#{ractors_map.size} entries)..." if @debug
48
+
49
+ # Simply clear the map without trying to interact with ractors
50
+ # The main loop already attempted to shut down workers properly
51
+ # On Windows Ruby 3.4, some workers may be stuck in Ractor.receive
52
+ # and will never acknowledge shutdown - we must not block on them
53
+ ractors_map.clear
54
+
55
+ # Force garbage collection to help clean up orphaned ractors
56
+ # This is a workaround for Ruby 3.4 Windows where orphaned ractors
57
+ # can block creation of new ractors in subsequent tests
58
+ GC.start
59
+ puts "Ractors map cleared and GC forced." if @debug
60
+ end
61
+
62
+ # Initiate graceful shutdown.
63
+ # Sets the shutting_down flag to allow the main loop to process
64
+ # shutdown acknowledgments before exiting.
65
+ #
66
+ # @return [void]
67
+ def initiate_shutdown
68
+ @shutting_down = true
69
+ puts "Main loop shutdown initiated" if @debug
70
+ end
71
+
72
+ private
73
+
74
+ # Get the current processed count from results.
75
+ #
76
+ # @return [Integer]
77
+ def get_processed_count
78
+ @supervisor.results.results.size + @supervisor.results.errors.size
79
+ end
80
+
81
+ # Check if the main loop should continue running.
82
+ # Continues during shutdown until all workers have acknowledged.
83
+ #
84
+ # @param processed_count [Integer] Current number of processed items
85
+ # @return [Boolean]
86
+ def should_continue_running?(processed_count)
87
+ return true if @shutting_down && !all_workers_closed?
88
+
89
+ running? && (continuous_mode? || processed_count < total_work_count)
90
+ end
91
+
92
+ # Check if all workers have closed (acknowledged shutdown).
93
+ #
94
+ # @return [Boolean]
95
+ def all_workers_closed?
96
+ workers.all?(&:closed?)
97
+ end
98
+
99
+ # Log current processing status for debugging.
100
+ #
101
+ # @param processed_count [Integer] Current number of processed items
102
+ # @return [void]
103
+ def log_processing_status(processed_count)
104
+ return unless @debug
105
+
106
+ if continuous_mode?
107
+ puts "Continuous mode: Waiting for Ractor results. Processed: #{processed_count}, Queue size: #{work_queue.size}"
108
+ else
109
+ puts "Waiting for Ractor results. Processed: #{processed_count}/#{total_work_count}, Queue size: #{work_queue.size}"
110
+ end
111
+ end
112
+
113
+ # Process a message from a ractor.
114
+ #
115
+ # @param ready_ractor_obj [Ractor] The ractor that sent the message
116
+ # @param message [Hash] The message received
117
+ # @return [void]
118
+ def process_message(ready_ractor_obj, message)
119
+ # Find the corresponding WrappedRactor instance
120
+ wrapped_ractor = ractors_map[ready_ractor_obj]
121
+ unless wrapped_ractor
122
+ puts "Warning: Received message from unknown Ractor: #{ready_ractor_obj}. Ignoring." if @debug
123
+ ractors_map.delete(ready_ractor_obj)
124
+ return
125
+ end
126
+
127
+ # Guard against nil messages (indicates closed ractor)
128
+ if message.nil?
129
+ puts "Warning: Received nil message from #{wrapped_ractor.name}. Ractor likely closed." if @debug
130
+ ractors_map.delete(ready_ractor_obj)
131
+ workers.delete(wrapped_ractor)
132
+ return
133
+ end
134
+
135
+ puts "Selected Ractor: #{wrapped_ractor.name}, Message Type: #{message[:type]}" if @debug
136
+
137
+ # Route to appropriate message handler
138
+ case message[:type]
139
+ when :initialize
140
+ handle_initialize_message(wrapped_ractor)
141
+ when :shutdown
142
+ handle_shutdown_message(ready_ractor_obj, wrapped_ractor)
143
+ when :result
144
+ handle_result_message(wrapped_ractor, message)
145
+ when :error
146
+ handle_error_message(wrapped_ractor, message)
147
+ else
148
+ puts "Unknown message type received: #{message[:type]} from #{wrapped_ractor.name}" if @debug
149
+ end
150
+ end
151
+
152
+ # Handle :initialize message from a worker.
153
+ #
154
+ # @param wrapped_ractor [WrappedRactor] The worker ractor
155
+ # @return [void]
156
+ def handle_initialize_message(wrapped_ractor)
157
+ puts "Ractor initialized: #{wrapped_ractor.worker_class}" if @debug
158
+
159
+ if work_distribution_manager.assign_work_to_worker(wrapped_ractor)
160
+ # Work was sent
161
+ elsif continuous_mode?
162
+ work_distribution_manager.mark_worker_idle(wrapped_ractor)
163
+ puts "Worker #{wrapped_ractor.name} marked as idle (continuous mode)" if @debug
164
+ else
165
+ handle_batch_mode_no_work(wrapped_ractor)
166
+ end
167
+ end
168
+
169
+ # Handle :shutdown message from a worker.
170
+ #
171
+ # @param ready_ractor_obj [Ractor] The ractor object
172
+ # @param wrapped_ractor [WrappedRactor] The worker ractor
173
+ # @return [void]
174
+ def handle_shutdown_message(ready_ractor_obj, wrapped_ractor)
175
+ puts "Ractor #{wrapped_ractor.name} acknowledged shutdown" if @debug
176
+ ractors_map.delete(ready_ractor_obj)
177
+ end
178
+
179
+ # Handle :result message from a worker.
180
+ #
181
+ # @param wrapped_ractor [WrappedRactor] The worker ractor
182
+ # @param message [Hash] The message containing the result
183
+ # @return [void]
184
+ def handle_result_message(wrapped_ractor, message)
185
+ work_result = message[:result]
186
+ puts "Completed work: #{work_result.inspect} in Ractor: #{message[:processor]}" if @debug
187
+
188
+ # Record performance metrics
189
+ record_performance_metrics(work_result, success: true)
190
+
191
+ # Trace work item completed
192
+ @supervisor.send(:trace_work, :completed, work_result.work,
193
+ worker_name: wrapped_ractor.name,
194
+ worker_class: wrapped_ractor.worker_class)
195
+
196
+ # Record result to error reporter
197
+ error_reporter.record(work_result,
198
+ job_name: wrapped_ractor.worker_class.name)
199
+
200
+ results.add_result(work_result)
201
+
202
+ if @debug
203
+ puts "Result processed. Total processed: #{results.results.size + results.errors.size}"
204
+ puts "Aggregated Results: #{results.inspect}" unless continuous_mode?
205
+ end
206
+
207
+ # Send next piece of work
208
+ assign_next_work_or_shutdown(wrapped_ractor)
209
+ end
210
+
211
+ # Handle :error message from a worker.
212
+ #
213
+ # @param wrapped_ractor [WrappedRactor] The worker ractor
214
+ # @param message [Hash] The message containing the error
215
+ # @return [void]
216
+ def handle_error_message(wrapped_ractor, message)
217
+ error_result = message[:result]
218
+
219
+ # Record performance metrics
220
+ record_performance_metrics(error_result, success: false)
221
+
222
+ # Trace work item failed
223
+ @supervisor.send(:trace_work, :failed, error_result.work,
224
+ worker_name: wrapped_ractor.name,
225
+ worker_class: wrapped_ractor.worker_class)
226
+
227
+ # Record error to error reporter
228
+ error_reporter.record(error_result,
229
+ job_name: wrapped_ractor.worker_class.name)
230
+
231
+ # Invoke error callbacks
232
+ error_callbacks.each do |callback|
233
+ callback.call(error_result, wrapped_ractor.name,
234
+ wrapped_ractor.worker_class)
235
+ rescue StandardError => e
236
+ puts "Error in error callback: #{e.message}" if @debug
237
+ end
238
+
239
+ # Enhanced error message with context
240
+ error_context = @supervisor.send(:format_error_context, wrapped_ractor,
241
+ error_result)
242
+ puts error_context if @debug
243
+
244
+ results.add_result(error_result)
245
+
246
+ if @debug
247
+ puts "Error handled. Total processed: #{results.results.size + results.errors.size}"
248
+ puts "Aggregated Results (including errors): #{results.inspect}" unless continuous_mode?
249
+ end
250
+
251
+ # Send next piece of work even after an error
252
+ assign_next_work_or_shutdown(wrapped_ractor)
253
+ end
254
+
255
+ # Record performance metrics for a completed job.
256
+ #
257
+ # @param work_result [WorkResult] The result object
258
+ # @param success [Boolean] Whether the job succeeded
259
+ # @return [void]
260
+ def record_performance_metrics(work_result, success:)
261
+ return unless performance_monitor && work_result.work
262
+
263
+ start_time = work_distribution_manager.get_work_start_time(work_result.work.object_id)
264
+ return unless start_time
265
+
266
+ latency = Time.now - start_time
267
+ performance_monitor.record_job(latency, success: success)
268
+ end
269
+
270
+ # Handle batch mode when no work is available.
271
+ #
272
+ # @param wrapped_ractor [WrappedRactor] The worker ractor
273
+ # @return [void]
274
+ def handle_batch_mode_no_work(wrapped_ractor)
275
+ current_processed = results.results.size + results.errors.size
276
+ if current_processed >= total_work_count
277
+ puts "All work processed, shutting down worker #{wrapped_ractor.name} (batch mode)" if @debug
278
+ wrapped_ractor.send(:shutdown)
279
+ else
280
+ # Work still pending but queue empty - shouldn't happen in normal flow
281
+ # Keep worker alive and add to idle list
282
+ work_distribution_manager.mark_worker_idle(wrapped_ractor)
283
+ puts "Worker #{wrapped_ractor.name} marked as idle (queue empty but work pending: #{current_processed}/#{total_work_count})" if @debug
284
+ end
285
+ end
286
+
287
+ # Assign next work to worker or shut down if all work is done.
288
+ #
289
+ # @param wrapped_ractor [WrappedRactor] The worker ractor
290
+ # @return [void]
291
+ def assign_next_work_or_shutdown(wrapped_ractor)
292
+ if work_distribution_manager.assign_work_to_worker(wrapped_ractor)
293
+ # Work was sent
294
+ elsif continuous_mode?
295
+ work_distribution_manager.mark_worker_idle(wrapped_ractor)
296
+ puts "Worker #{wrapped_ractor.name} marked as idle after completing work (continuous mode)" if @debug
297
+ else
298
+ handle_batch_mode_no_work(wrapped_ractor)
299
+ end
300
+ end
301
+
302
+ # Helper methods to access supervisor state
303
+
304
+ def running?
305
+ @supervisor.instance_variable_get(:@running)
306
+ end
307
+
308
+ def continuous_mode?
309
+ @supervisor.instance_variable_get(:@continuous_mode)
310
+ end
311
+
312
+ def total_work_count
313
+ @supervisor.instance_variable_get(:@total_work_count)
314
+ end
315
+
316
+ def work_queue
317
+ @supervisor.work_queue
318
+ end
319
+
320
+ def workers
321
+ @supervisor.workers
322
+ end
323
+
324
+ def ractors_map
325
+ @supervisor.instance_variable_get(:@ractors_map)
326
+ end
327
+
328
+ def wakeup_ractor
329
+ @supervisor.instance_variable_get(:@wakeup_ractor)
330
+ end
331
+
332
+ def wakeup_port
333
+ @supervisor.instance_variable_get(:@wakeup_port)
334
+ end
335
+
336
+ def work_distribution_manager
337
+ @supervisor.instance_variable_get(:@work_distribution_manager)
338
+ end
339
+
340
+ def results
341
+ @supervisor.results
342
+ end
343
+
344
+ def error_reporter
345
+ @supervisor.error_reporter
346
+ end
347
+
348
+ def performance_monitor
349
+ @supervisor.instance_variable_get(:@performance_monitor)
350
+ end
351
+
352
+ def work_callbacks
353
+ @supervisor.instance_variable_get(:@work_callbacks)
354
+ end
355
+
356
+ def error_callbacks
357
+ @supervisor.instance_variable_get(:@error_callbacks)
358
+ end
359
+
360
+ # Check if running on Windows with Ruby 3.4
361
+ # Returns true for Windows Ruby 3.4.x where Ractor issues occur
362
+ #
363
+ # @return [Boolean]
364
+ def windows_ruby_34?
365
+ return false unless RUBY_PLATFORM.match?(/mswin|mingw|cygwin/)
366
+
367
+ ruby_version = Gem::Version.new(RUBY_VERSION)
368
+ ruby_version >= Gem::Version.new("3.4.0") && ruby_version < Gem::Version.new("3.5.0")
369
+ end
370
+
371
+ # Handle a stuck ractor by identifying and removing it from the active pool
372
+ # This is called when Ractor.select times out on Windows Ruby 3.4
373
+ #
374
+ # @param active [Array] List of active ractors/ports
375
+ # @return [void]
376
+ def handle_stuck_ractor(active)
377
+ puts "[WARNING] Ractor.select timeout - detecting stuck ractor..." if @debug
378
+
379
+ # Try to identify which ractor is stuck by checking their state
380
+ active.each do |ractor_or_port|
381
+ # Skip ports (Ruby 4.0) - they should be checked differently
382
+ next if ractor_or_port.is_a?(Ractor::Port)
383
+
384
+ wrapped_ractor = ractors_map[ractor_or_port]
385
+ next unless wrapped_ractor
386
+
387
+ # Check if ractor appears stuck (terminated or blocked)
388
+ begin
389
+ inspect_result = Timeout.timeout(0.1) { ractor_or_port.inspect }
390
+ rescue Timeout::Error
391
+ inspect_result = "#<Ractor:blocked>"
392
+ end
393
+
394
+ if inspect_result.include?("terminated") || inspect_result.include?("invalid")
395
+ puts "[WARNING] Removing stuck/terminated ractor: #{wrapped_ractor.name}" if @debug
396
+ ractors_map.delete(ractor_or_port)
397
+ workers.delete(wrapped_ractor)
398
+ end
399
+ end
400
+
401
+ # Force garbage collection to help clean up stuck ractors
402
+ GC.start
403
+ puts "[WARNING] Stuck ractor handled, GC forced" if @debug
404
+ end
405
+ end
406
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "main_loop_handler"
4
+
5
+ module Fractor
6
+ # Ruby 3.x specific implementation of MainLoopHandler.
7
+ # Uses Ractor.yield for worker communication.
8
+ class MainLoopHandler3 < MainLoopHandler
9
+ # Run the main event loop for Ruby 3.x.
10
+ def run_loop
11
+ loop do
12
+ processed_count = get_processed_count
13
+
14
+ # Check loop termination condition
15
+ break unless should_continue_running?(processed_count)
16
+
17
+ log_processing_status(processed_count)
18
+
19
+ active_ractors = get_active_ractors
20
+
21
+ # Check for new work from callbacks if in continuous mode
22
+ process_work_callbacks if continuous_mode? && !work_callbacks.empty?
23
+
24
+ # Handle edge cases - break if edge case handler indicates we should
25
+ next if handle_edge_cases(active_ractors, processed_count)
26
+
27
+ # Wait for next message from any active ractor
28
+ ready_ractor_obj, message = select_from_ractors(active_ractors)
29
+ next unless ready_ractor_obj && message
30
+
31
+ # Process the received message
32
+ process_message(ready_ractor_obj, message)
33
+ end
34
+
35
+ puts "Main loop finished." if @debug
36
+
37
+ # Clean up ractors map after batch mode completion
38
+ cleanup_ractors_map unless continuous_mode?
39
+ end
40
+
41
+ private
42
+
43
+ # Get list of active ractors for Ractor.select (Ruby 3.x).
44
+ # Excludes wakeup ractor unless in continuous mode with callbacks.
45
+ #
46
+ # @return [Array<Ractor>]
47
+ def get_active_ractors
48
+ ractors_map.keys.reject do |ractor|
49
+ ractor == wakeup_ractor && !(continuous_mode? && !work_callbacks.empty?)
50
+ end
51
+ end
52
+
53
+ # Check for new work from callbacks in continuous mode.
54
+ #
55
+ # @return [void]
56
+ def process_work_callbacks
57
+ work_callbacks.each do |callback|
58
+ new_work = callback.call
59
+ if new_work && !new_work.empty?
60
+ @supervisor.add_work_items(new_work)
61
+ puts "Work source provided #{new_work.size} new items" if @debug
62
+
63
+ # Distribute work to idle workers
64
+ distributed = work_distribution_manager.distribute_to_idle_workers
65
+ puts "Distributed work to #{distributed} idle workers" if @debug && distributed.positive?
66
+ end
67
+ end
68
+ end
69
+
70
+ # Handle edge cases like no active workers or empty queue.
71
+ #
72
+ # @param active_ractors [Array<Ractor>] List of active ractors
73
+ # @param processed_count [Integer] Current number of processed items
74
+ # @return [Boolean] true if should break from loop
75
+ def handle_edge_cases(active_ractors, processed_count)
76
+ # In continuous mode, if no active ractors and shutting down, exit loop
77
+ if active_ractors.empty? && @shutting_down
78
+ puts "No active ractors during shutdown, exiting main loop" if @debug
79
+ return true
80
+ end
81
+
82
+ # Break if no active workers and queue is empty, but work remains (indicates potential issue)
83
+ if active_ractors.empty? && work_queue.empty? && !continuous_mode? && processed_count < total_work_count
84
+ puts "Warning: No active workers and queue is empty, but not all work is processed. Exiting loop." if @debug
85
+ return true
86
+ end
87
+
88
+ # In continuous mode, just wait if no active ractors but keep running
89
+ if active_ractors.empty?
90
+ return true unless continuous_mode?
91
+
92
+ sleep(0.1) # Small delay to avoid CPU spinning
93
+ return false # Continue to next iteration
94
+ end
95
+
96
+ false
97
+ end
98
+
99
+ # Wait for a message from any active ractor (Ruby 3.x).
100
+ # Uses a timeout on Windows Ruby 3.4 to detect stuck ractors.
101
+ #
102
+ # @param active_ractors [Array<Ractor>] List of active ractors to select from
103
+ # @return [Array] ready_ractor_obj and message, or nil if should continue
104
+ def select_from_ractors(active_ractors)
105
+ # On Windows Ruby 3.4, use timeout to detect stuck ractors
106
+ ready_ractor_obj, message = if windows_ruby_34?
107
+ begin
108
+ Timeout.timeout(30) do
109
+ Ractor.select(*active_ractors)
110
+ end
111
+ rescue Timeout::Error
112
+ # Timeout indicates a ractor is stuck - identify and remove it
113
+ handle_stuck_ractor(active_ractors)
114
+ return nil, nil
115
+ end
116
+ else
117
+ Ractor.select(*active_ractors)
118
+ end
119
+
120
+ # Check if this is the wakeup ractor
121
+ if ready_ractor_obj == wakeup_ractor
122
+ puts "Wakeup signal received: #{message[:message]}" if @debug
123
+ # Remove wakeup ractor from map if shutting down
124
+ if message[:message] == :shutdown
125
+ ractors_map.delete(wakeup_ractor)
126
+ @supervisor.instance_variable_set(:@wakeup_ractor, nil)
127
+ end
128
+ # Return nil to indicate we should continue to next iteration
129
+ return nil, nil
130
+ end
131
+
132
+ [ready_ractor_obj, message]
133
+ end
134
+ end
135
+ end