fractor 0.1.4 → 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 (189) 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 +284 -43
  5. data/README.adoc +111 -950
  6. data/docs/.lycheeignore +16 -0
  7. data/docs/Gemfile +24 -0
  8. data/docs/README.md +157 -0
  9. data/docs/_config.yml +151 -0
  10. data/docs/_features/error-handling.adoc +1192 -0
  11. data/docs/_features/index.adoc +80 -0
  12. data/docs/_features/monitoring.adoc +589 -0
  13. data/docs/_features/signal-handling.adoc +202 -0
  14. data/docs/_features/workflows.adoc +1235 -0
  15. data/docs/_guides/continuous-mode.adoc +736 -0
  16. data/docs/_guides/cookbook.adoc +1133 -0
  17. data/docs/_guides/index.adoc +55 -0
  18. data/docs/_guides/pipeline-mode.adoc +730 -0
  19. data/docs/_guides/troubleshooting.adoc +358 -0
  20. data/docs/_pages/architecture.adoc +1390 -0
  21. data/docs/_pages/core-concepts.adoc +1392 -0
  22. data/docs/_pages/design-principles.adoc +862 -0
  23. data/docs/_pages/getting-started.adoc +290 -0
  24. data/docs/_pages/installation.adoc +143 -0
  25. data/docs/_reference/api.adoc +1080 -0
  26. data/docs/_reference/error-reporting.adoc +670 -0
  27. data/docs/_reference/examples.adoc +181 -0
  28. data/docs/_reference/index.adoc +96 -0
  29. data/docs/_reference/troubleshooting.adoc +862 -0
  30. data/docs/_tutorials/complex-workflows.adoc +1022 -0
  31. data/docs/_tutorials/data-processing-pipeline.adoc +740 -0
  32. data/docs/_tutorials/first-application.adoc +384 -0
  33. data/docs/_tutorials/index.adoc +48 -0
  34. data/docs/_tutorials/long-running-services.adoc +931 -0
  35. data/docs/assets/images/favicon-16.png +0 -0
  36. data/docs/assets/images/favicon-32.png +0 -0
  37. data/docs/assets/images/favicon-48.png +0 -0
  38. data/docs/assets/images/favicon.ico +0 -0
  39. data/docs/assets/images/favicon.png +0 -0
  40. data/docs/assets/images/favicon.svg +45 -0
  41. data/docs/assets/images/fractor-icon.svg +49 -0
  42. data/docs/assets/images/fractor-logo.svg +61 -0
  43. data/docs/index.adoc +131 -0
  44. data/docs/lychee.toml +39 -0
  45. data/examples/api_aggregator/README.adoc +627 -0
  46. data/examples/api_aggregator/api_aggregator.rb +376 -0
  47. data/examples/auto_detection/README.adoc +407 -29
  48. data/examples/auto_detection/auto_detection.rb +9 -9
  49. data/examples/continuous_chat_common/message_protocol.rb +53 -0
  50. data/examples/continuous_chat_fractor/README.adoc +217 -0
  51. data/examples/continuous_chat_fractor/chat_client.rb +303 -0
  52. data/examples/continuous_chat_fractor/chat_common.rb +83 -0
  53. data/examples/continuous_chat_fractor/chat_server.rb +167 -0
  54. data/examples/continuous_chat_fractor/simulate.rb +345 -0
  55. data/examples/continuous_chat_server/README.adoc +135 -0
  56. data/examples/continuous_chat_server/chat_client.rb +303 -0
  57. data/examples/continuous_chat_server/chat_server.rb +359 -0
  58. data/examples/continuous_chat_server/simulate.rb +343 -0
  59. data/examples/error_reporting.rb +207 -0
  60. data/examples/file_processor/README.adoc +170 -0
  61. data/examples/file_processor/file_processor.rb +615 -0
  62. data/examples/file_processor/sample_files/invalid.csv +1 -0
  63. data/examples/file_processor/sample_files/orders.xml +24 -0
  64. data/examples/file_processor/sample_files/products.json +23 -0
  65. data/examples/file_processor/sample_files/users.csv +6 -0
  66. data/examples/hierarchical_hasher/README.adoc +629 -41
  67. data/examples/hierarchical_hasher/hierarchical_hasher.rb +12 -8
  68. data/examples/image_processor/README.adoc +610 -0
  69. data/examples/image_processor/image_processor.rb +349 -0
  70. data/examples/image_processor/processed_images/sample_10_processed.jpg.json +12 -0
  71. data/examples/image_processor/processed_images/sample_1_processed.jpg.json +12 -0
  72. data/examples/image_processor/processed_images/sample_2_processed.jpg.json +12 -0
  73. data/examples/image_processor/processed_images/sample_3_processed.jpg.json +12 -0
  74. data/examples/image_processor/processed_images/sample_4_processed.jpg.json +12 -0
  75. data/examples/image_processor/processed_images/sample_5_processed.jpg.json +12 -0
  76. data/examples/image_processor/processed_images/sample_6_processed.jpg.json +12 -0
  77. data/examples/image_processor/processed_images/sample_7_processed.jpg.json +12 -0
  78. data/examples/image_processor/processed_images/sample_8_processed.jpg.json +12 -0
  79. data/examples/image_processor/processed_images/sample_9_processed.jpg.json +12 -0
  80. data/examples/image_processor/test_images/sample_1.png +1 -0
  81. data/examples/image_processor/test_images/sample_10.png +1 -0
  82. data/examples/image_processor/test_images/sample_2.png +1 -0
  83. data/examples/image_processor/test_images/sample_3.png +1 -0
  84. data/examples/image_processor/test_images/sample_4.png +1 -0
  85. data/examples/image_processor/test_images/sample_5.png +1 -0
  86. data/examples/image_processor/test_images/sample_6.png +1 -0
  87. data/examples/image_processor/test_images/sample_7.png +1 -0
  88. data/examples/image_processor/test_images/sample_8.png +1 -0
  89. data/examples/image_processor/test_images/sample_9.png +1 -0
  90. data/examples/log_analyzer/README.adoc +662 -0
  91. data/examples/log_analyzer/log_analyzer.rb +579 -0
  92. data/examples/log_analyzer/sample_logs/apache.log +20 -0
  93. data/examples/log_analyzer/sample_logs/json.log +15 -0
  94. data/examples/log_analyzer/sample_logs/nginx.log +15 -0
  95. data/examples/log_analyzer/sample_logs/rails.log +29 -0
  96. data/examples/multi_work_type/README.adoc +576 -26
  97. data/examples/multi_work_type/multi_work_type.rb +30 -29
  98. data/examples/performance_monitoring.rb +120 -0
  99. data/examples/pipeline_processing/README.adoc +740 -26
  100. data/examples/pipeline_processing/pipeline_processing.rb +16 -16
  101. data/examples/priority_work_example.rb +155 -0
  102. data/examples/producer_subscriber/README.adoc +889 -46
  103. data/examples/producer_subscriber/producer_subscriber.rb +20 -16
  104. data/examples/scatter_gather/README.adoc +829 -27
  105. data/examples/scatter_gather/scatter_gather.rb +29 -28
  106. data/examples/simple/README.adoc +347 -0
  107. data/examples/simple/sample.rb +5 -5
  108. data/examples/specialized_workers/README.adoc +622 -26
  109. data/examples/specialized_workers/specialized_workers.rb +88 -45
  110. data/examples/stream_processor/README.adoc +206 -0
  111. data/examples/stream_processor/stream_processor.rb +284 -0
  112. data/examples/web_scraper/README.adoc +625 -0
  113. data/examples/web_scraper/web_scraper.rb +285 -0
  114. data/examples/workflow/README.adoc +406 -0
  115. data/examples/workflow/circuit_breaker/README.adoc +360 -0
  116. data/examples/workflow/circuit_breaker/circuit_breaker_workflow.rb +225 -0
  117. data/examples/workflow/conditional/README.adoc +483 -0
  118. data/examples/workflow/conditional/conditional_workflow.rb +215 -0
  119. data/examples/workflow/dead_letter_queue/README.adoc +374 -0
  120. data/examples/workflow/dead_letter_queue/dead_letter_queue_workflow.rb +217 -0
  121. data/examples/workflow/fan_out/README.adoc +381 -0
  122. data/examples/workflow/fan_out/fan_out_workflow.rb +202 -0
  123. data/examples/workflow/retry/README.adoc +248 -0
  124. data/examples/workflow/retry/retry_workflow.rb +195 -0
  125. data/examples/workflow/simple_linear/README.adoc +267 -0
  126. data/examples/workflow/simple_linear/simple_linear_workflow.rb +175 -0
  127. data/examples/workflow/simplified/README.adoc +329 -0
  128. data/examples/workflow/simplified/simplified_workflow.rb +222 -0
  129. data/exe/fractor +10 -0
  130. data/lib/fractor/cli.rb +288 -0
  131. data/lib/fractor/configuration.rb +307 -0
  132. data/lib/fractor/continuous_server.rb +183 -0
  133. data/lib/fractor/error_formatter.rb +72 -0
  134. data/lib/fractor/error_report_generator.rb +152 -0
  135. data/lib/fractor/error_reporter.rb +244 -0
  136. data/lib/fractor/error_statistics.rb +147 -0
  137. data/lib/fractor/execution_tracer.rb +162 -0
  138. data/lib/fractor/logger.rb +230 -0
  139. data/lib/fractor/main_loop_handler.rb +406 -0
  140. data/lib/fractor/main_loop_handler3.rb +135 -0
  141. data/lib/fractor/main_loop_handler4.rb +299 -0
  142. data/lib/fractor/performance_metrics_collector.rb +181 -0
  143. data/lib/fractor/performance_monitor.rb +215 -0
  144. data/lib/fractor/performance_report_generator.rb +202 -0
  145. data/lib/fractor/priority_work.rb +93 -0
  146. data/lib/fractor/priority_work_queue.rb +189 -0
  147. data/lib/fractor/result_aggregator.rb +33 -1
  148. data/lib/fractor/shutdown_handler.rb +168 -0
  149. data/lib/fractor/signal_handler.rb +80 -0
  150. data/lib/fractor/supervisor.rb +430 -144
  151. data/lib/fractor/supervisor_logger.rb +88 -0
  152. data/lib/fractor/version.rb +1 -1
  153. data/lib/fractor/work.rb +12 -0
  154. data/lib/fractor/work_distribution_manager.rb +151 -0
  155. data/lib/fractor/work_queue.rb +88 -0
  156. data/lib/fractor/work_result.rb +181 -9
  157. data/lib/fractor/worker.rb +75 -1
  158. data/lib/fractor/workflow/builder.rb +210 -0
  159. data/lib/fractor/workflow/chain_builder.rb +169 -0
  160. data/lib/fractor/workflow/circuit_breaker.rb +183 -0
  161. data/lib/fractor/workflow/circuit_breaker_orchestrator.rb +208 -0
  162. data/lib/fractor/workflow/circuit_breaker_registry.rb +112 -0
  163. data/lib/fractor/workflow/dead_letter_queue.rb +334 -0
  164. data/lib/fractor/workflow/execution_hooks.rb +39 -0
  165. data/lib/fractor/workflow/execution_strategy.rb +225 -0
  166. data/lib/fractor/workflow/execution_trace.rb +134 -0
  167. data/lib/fractor/workflow/helpers.rb +191 -0
  168. data/lib/fractor/workflow/job.rb +290 -0
  169. data/lib/fractor/workflow/job_dependency_validator.rb +120 -0
  170. data/lib/fractor/workflow/logger.rb +110 -0
  171. data/lib/fractor/workflow/pre_execution_context.rb +193 -0
  172. data/lib/fractor/workflow/retry_config.rb +156 -0
  173. data/lib/fractor/workflow/retry_orchestrator.rb +184 -0
  174. data/lib/fractor/workflow/retry_strategy.rb +93 -0
  175. data/lib/fractor/workflow/structured_logger.rb +30 -0
  176. data/lib/fractor/workflow/type_compatibility_validator.rb +222 -0
  177. data/lib/fractor/workflow/visualizer.rb +211 -0
  178. data/lib/fractor/workflow/workflow_context.rb +132 -0
  179. data/lib/fractor/workflow/workflow_executor.rb +669 -0
  180. data/lib/fractor/workflow/workflow_result.rb +55 -0
  181. data/lib/fractor/workflow/workflow_validator.rb +295 -0
  182. data/lib/fractor/workflow.rb +333 -0
  183. data/lib/fractor/wrapped_ractor.rb +66 -91
  184. data/lib/fractor/wrapped_ractor3.rb +161 -0
  185. data/lib/fractor/wrapped_ractor4.rb +242 -0
  186. data/lib/fractor.rb +93 -3
  187. metadata +192 -6
  188. data/tests/sample.rb.bak +0 -309
  189. data/tests/sample_working.rb.bak +0 -209
@@ -1,103 +1,70 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Fractor
4
- # Wraps a Ruby Ractor to manage a worker instance.
5
- # Handles communication and error propagation.
4
+ # Base class for wrapped Ractors with shared functionality.
5
+ # Subclasses implement Ruby 3.x and Ruby 4.0+ specific communication patterns.
6
6
  class WrappedRactor
7
- attr_reader :ractor, :name
7
+ attr_reader :ractor, :name, :worker_class
8
+
9
+ # Factory method to create the appropriate WrappedRactor implementation
10
+ # based on the current Ruby version.
11
+ #
12
+ # @param name [String] Name for the ractor
13
+ # @param worker_class [Class] Worker class to instantiate
14
+ # @param kwargs [Hash] Additional keyword arguments for subclass initialization
15
+ # @return [WrappedRactor] The appropriate subclass instance
16
+ def self.create(name, worker_class, **kwargs)
17
+ ruby_4_0 = Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("4.0.0")
18
+ if ruby_4_0
19
+ WrappedRactor4.new(name, worker_class, **kwargs)
20
+ else
21
+ WrappedRactor3.new(name, worker_class, **kwargs)
22
+ end
23
+ end
8
24
 
9
- # Initializes the WrappedRactor with a name and the Worker class to instantiate.
10
- # The worker_class parameter allows flexibility in specifying the worker type.
25
+ # Initializes the WrappedRactor with a name and the Worker class.
26
+ #
27
+ # @param name [String] Name for the ractor
28
+ # @param worker_class [Class] Worker class to instantiate
11
29
  def initialize(name, worker_class)
12
30
  puts "Creating Ractor #{name} with worker #{worker_class}" if ENV["FRACTOR_DEBUG"]
13
31
  @name = name
14
- @worker_class = worker_class # Store the worker class
15
- @ractor = nil # Initialize ractor as nil
32
+ @worker_class = worker_class
33
+ @ractor = nil
16
34
  end
17
35
 
18
- # Starts the underlying Ractor.
36
+ # Starts the underlying Ractor. Must be implemented by subclasses.
19
37
  def start
20
- puts "Starting Ractor #{@name}" if ENV["FRACTOR_DEBUG"]
21
- # Pass worker_class to the Ractor block
22
- @ractor = Ractor.new(@name, @worker_class) do |name, worker_cls|
23
- puts "Ractor #{name} started with worker class #{worker_cls}" if ENV["FRACTOR_DEBUG"]
24
- # Yield an initialization message
25
- Ractor.yield({ type: :initialize, processor: name })
26
-
27
- # Instantiate the specific worker inside the Ractor
28
- worker = worker_cls.new(name: name)
29
-
30
- loop do
31
- # Ractor.receive will block until a message is received
32
- puts "Waiting for work in #{name}" if ENV["FRACTOR_DEBUG"]
33
- work = Ractor.receive
34
-
35
- # Handle shutdown message
36
- if work == :shutdown
37
- puts "Received shutdown message in Ractor #{name}, terminating..." if ENV["FRACTOR_DEBUG"]
38
- break
39
- end
40
-
41
- puts "Received work #{work.inspect} in #{name}" if ENV["FRACTOR_DEBUG"]
42
-
43
- begin
44
- # Process the work using the instantiated worker
45
- result = worker.process(work)
46
- puts "Sending result #{result.inspect} from Ractor #{name}" if ENV["FRACTOR_DEBUG"]
47
- # Yield the result back
48
- Ractor.yield({ type: :result, result: result, processor: name })
49
- rescue StandardError => e
50
- # Handle errors during processing
51
- puts "Error processing work #{work.inspect} in Ractor #{name}: #{e.message}\n#{e.backtrace.join("\n")}" if ENV["FRACTOR_DEBUG"]
52
- # Yield an error message back
53
- # Ensure the original work object is included in the error result
54
- error_result = Fractor::WorkResult.new(error: e.message, work: work)
55
- Ractor.yield({ type: :error, result: error_result, processor: name })
56
- end
57
- end
58
- rescue Ractor::ClosedError
59
- puts "Ractor #{name} closed." if ENV["FRACTOR_DEBUG"]
60
- rescue StandardError => e
61
- puts "Unexpected error in Ractor #{name}: #{e.message}\n#{e.backtrace.join("\n")}" if ENV["FRACTOR_DEBUG"]
62
- # Optionally yield a critical error message if needed
63
- ensure
64
- puts "Ractor #{name} shutting down." if ENV["FRACTOR_DEBUG"]
65
- end
66
- puts "Ractor #{@name} instance created: #{@ractor}" if ENV["FRACTOR_DEBUG"]
38
+ raise NotImplementedError, "Subclasses must implement #start"
67
39
  end
68
40
 
69
- # Sends work to the Ractor if it's active.
41
+ # Sends work to the Ractor. Must be implemented by subclasses.
42
+ #
43
+ # @param work [Fractor::Work] The work item to process
44
+ # @return [Boolean] true if sent successfully, false otherwise
70
45
  def send(work)
71
- if @ractor
72
- begin
73
- @ractor.send(work)
74
- true
75
- rescue Exception => e
76
- puts "Warning: Error sending work to Ractor #{@name}: #{e.message}" if ENV["FRACTOR_DEBUG"]
77
- false
78
- end
79
- else
80
- puts "Warning: Attempted to send work to nil Ractor #{@name}" if ENV["FRACTOR_DEBUG"]
81
- false
82
- end
46
+ raise NotImplementedError, "Subclasses must implement #send"
47
+ end
48
+
49
+ # Receives a message from the Ractor. Must be implemented by subclasses.
50
+ #
51
+ # @return [Hash, nil] The message received or nil
52
+ def receive_message
53
+ raise NotImplementedError, "Subclasses must implement #receive_message"
83
54
  end
84
55
 
85
56
  # Closes the Ractor.
86
- # Ruby 3.0+ has different ways to terminate Ractors, we try the available methods
57
+ #
58
+ # @return [Boolean] true if closed successfully
87
59
  def close
88
60
  return true if @ractor.nil?
89
61
 
90
62
  begin
91
- # Send a nil message to signal we're done - this might be processed
92
- # if the Ractor is waiting for input
63
+ # Send a nil message to signal we're done
93
64
  begin
94
- begin
95
- @ractor.send(nil)
96
- rescue StandardError
97
- nil
98
- end
65
+ @ractor.send(nil)
99
66
  rescue StandardError
100
- # Ignore errors when sending nil
67
+ nil
101
68
  end
102
69
 
103
70
  # Mark as closed in our object
@@ -123,25 +90,33 @@ module Fractor
123
90
  end
124
91
 
125
92
  # Checks if the Ractor is closed or unavailable.
93
+ # Uses a timeout to avoid blocking on Windows Ruby 3.4 where
94
+ # Ractor#inspect can block if the ractor is waiting on receive.
95
+ #
96
+ # @return [Boolean] true if closed, false otherwise
126
97
  def closed?
127
98
  return true if @ractor.nil?
128
99
 
129
- begin
130
- # Check if the Ractor is terminated using Ractor#inspect
131
- # This is safer than calling methods on the Ractor
132
- r_status = @ractor.inspect
133
- if r_status.include?("terminated")
134
- # If terminated, clean up our reference
135
- @ractor = nil
136
- return true
137
- end
138
- false
139
- rescue Exception => e
140
- # If we get an exception, the Ractor is likely terminated
141
- puts "Ractor #{@name} appears to be terminated: #{e.message}" if ENV["FRACTOR_DEBUG"]
100
+ # Use a timeout to avoid blocking indefinitely on Windows Ruby 3.4
101
+ result = Timeout.timeout(0.1) do
102
+ @ractor.inspect
103
+ rescue Timeout::Error
104
+ # Timeout means ractor is still running (not terminated)
105
+ "#<Ractor:blocked>"
106
+ end
107
+
108
+ if result.include?("terminated")
109
+ # If terminated, clean up our reference
142
110
  @ractor = nil
143
- true
111
+ return true
144
112
  end
113
+
114
+ false
115
+ rescue Exception => e
116
+ # If we get an exception, the Ractor is likely terminated
117
+ puts "Ractor #{@name} appears to be terminated: #{e.message}" if ENV["FRACTOR_DEBUG"]
118
+ @ractor = nil
119
+ true
145
120
  end
146
121
  end
147
122
  end
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "timeout"
4
+ require_relative "wrapped_ractor"
5
+ require_relative "logger"
6
+
7
+ module Fractor
8
+ # Ruby 3.x specific implementation of WrappedRactor.
9
+ # Uses Ractor.yield for sending messages back from workers.
10
+ class WrappedRactor3 < WrappedRactor
11
+ # Initializes the WrappedRactor3.
12
+ # The response_port parameter is accepted for API compatibility but not used in Ruby 3.x.
13
+ #
14
+ # @param name [String] Name for the ractor
15
+ # @param worker_class [Class] Worker class to instantiate
16
+ # @param response_port [Object, nil] Unused in Ruby 3.x (for API compatibility with Ruby 4.0)
17
+ def initialize(name, worker_class, response_port: nil)
18
+ super(name, worker_class)
19
+ # response_port is not used in Ruby 3.x
20
+ end
21
+
22
+ # Starts the underlying Ractor using Ractor.yield pattern.
23
+ def start
24
+ RactorLogger.info("Starting Ractor #{@name} (Ruby 3.x mode)",
25
+ ractor_name: @name)
26
+
27
+ # Capture timeout value before entering ractor (Ractors can't access Fractor.config)
28
+ # Get class-level timeout, or fall back to default of nil (no timeout)
29
+ # Note: We avoid accessing Fractor.config from ractor creation context
30
+ class_level_timeout = @worker_class.worker_timeout
31
+
32
+ # Pass worker_class and timeout to the Ractor block
33
+ @ractor = Ractor.new(@name, @worker_class,
34
+ class_level_timeout) do |name, worker_cls, timeout_val|
35
+ RactorLogger.debug(
36
+ "Ractor started with worker class #{worker_cls} and timeout #{timeout_val.inspect}", ractor_name: name
37
+ )
38
+ # Yield an initialization message
39
+ Ractor.yield({ type: :initialize, processor: name })
40
+
41
+ # Instantiate the specific worker inside the Ractor
42
+ # Pass timeout as an option only if it's not nil, to avoid accessing self.class from ractor
43
+ worker = if timeout_val.nil?
44
+ worker_cls.new(name: name)
45
+ else
46
+ worker_cls.new(name: name, timeout: timeout_val)
47
+ end
48
+
49
+ loop do
50
+ # Ractor.receive will block until a message is received
51
+ RactorLogger.debug("Waiting for work", ractor_name: name)
52
+ work = Ractor.receive
53
+
54
+ # Handle shutdown message
55
+ if work == :shutdown
56
+ RactorLogger.debug("Received shutdown message, terminating",
57
+ ractor_name: name)
58
+ # Yield a shutdown acknowledgment before terminating
59
+ Ractor.yield({ type: :shutdown, processor: name })
60
+ break
61
+ end
62
+
63
+ RactorLogger.debug("Received work #{work.inspect}", ractor_name: name)
64
+
65
+ begin
66
+ # Get the timeout for this worker (nil means no timeout)
67
+ worker_timeout = worker.timeout
68
+
69
+ # Process the work with timeout if configured
70
+ # Note: Ruby's Timeout.timeout uses threads which don't work with Ractors.
71
+ # We measure execution time and raise timeout error afterward if exceeded.
72
+ result = if worker_timeout
73
+ start_time = Time.now
74
+ process_result = worker.process(work)
75
+ elapsed = Time.now - start_time
76
+ if elapsed > worker_timeout
77
+ # Raise a timeout error after the fact
78
+ # Note: This is a post-facto timeout check - the work has already completed
79
+ raise Timeout::Error,
80
+ "execution timed out after #{elapsed}s (limit: #{worker_timeout}s)"
81
+ end
82
+
83
+ process_result
84
+ else
85
+ worker.process(work)
86
+ end
87
+
88
+ RactorLogger.debug("Sending result #{result.inspect}",
89
+ ractor_name: name)
90
+ # Wrap the result in a WorkResult object if not already wrapped
91
+ work_result = if result.is_a?(Fractor::WorkResult)
92
+ result
93
+ else
94
+ Fractor::WorkResult.new(result: result, work: work)
95
+ end
96
+ # Yield the result back
97
+ Ractor.yield({ type: :result, result: work_result,
98
+ processor: name })
99
+ rescue Timeout::Error => e
100
+ # Handle timeout errors as retriable errors
101
+ RactorLogger.warn(
102
+ "Timed out after #{worker.timeout}s processing work #{work.inspect}", ractor_name: name
103
+ )
104
+ error_result = Fractor::WorkResult.new(
105
+ error: "Worker timeout: #{e.message}",
106
+ work: work,
107
+ error_category: :timeout,
108
+ )
109
+ Ractor.yield({ type: :error, result: error_result,
110
+ processor: name })
111
+ rescue StandardError => e
112
+ # Handle errors during processing
113
+ RactorLogger.error("Error processing work #{work.inspect}",
114
+ ractor_name: name, exception: e)
115
+ # Yield an error message back
116
+ # Ensure the original work object is included in the error result
117
+ error_result = Fractor::WorkResult.new(error: e.message, work: work)
118
+ Ractor.yield({ type: :error, result: error_result,
119
+ processor: name })
120
+ end
121
+ end
122
+ rescue Ractor::ClosedError
123
+ RactorLogger.debug("Ractor closed", ractor_name: @name)
124
+ rescue StandardError => e
125
+ RactorLogger.error("Unexpected error", ractor_name: @name, exception: e)
126
+ ensure
127
+ RactorLogger.debug("Ractor shutting down", ractor_name: @name)
128
+ end
129
+ RactorLogger.debug("Ractor instance created: #{@ractor}",
130
+ ractor_name: @name)
131
+ end
132
+
133
+ # Sends work to the Ractor.
134
+ #
135
+ # @param work [Fractor::Work] The work item to process
136
+ # @return [Boolean] true if sent successfully, false otherwise
137
+ def send(work)
138
+ if @ractor
139
+ begin
140
+ @ractor.send(work)
141
+ true
142
+ rescue Exception => e
143
+ RactorLogger.warn("Error sending work to Ractor: #{e.message}",
144
+ ractor_name: @name)
145
+ false
146
+ end
147
+ else
148
+ RactorLogger.warn("Attempted to send work to nil Ractor",
149
+ ractor_name: @name)
150
+ false
151
+ end
152
+ end
153
+
154
+ # Receives a message from the Ractor using Ractor.take.
155
+ #
156
+ # @return [Hash, nil] The message received or nil
157
+ def receive_message
158
+ @ractor&.take
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,242 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "timeout"
4
+ require_relative "wrapped_ractor"
5
+ require_relative "logger"
6
+
7
+ module Fractor
8
+ # Ruby 4.0+ specific implementation of WrappedRactor.
9
+ # Uses Ractor::Port for communication - main ractor creates response ports
10
+ # and passes them to workers when sending work.
11
+ class WrappedRactor4 < WrappedRactor
12
+ attr_reader :response_port
13
+
14
+ # Initializes the WrappedRactor with a name, worker class, and response port.
15
+ #
16
+ # @param name [String] Name for the ractor
17
+ # @param worker_class [Class] Worker class to instantiate
18
+ # @param response_port [Ractor::Port, nil] The port to receive responses on (created by main ractor)
19
+ def initialize(name, worker_class, response_port: nil)
20
+ super(name, worker_class)
21
+ @response_port = response_port
22
+ end
23
+
24
+ # Sets the response port for this worker.
25
+ #
26
+ # @param port [Ractor::Port] The port to receive responses on
27
+ def response_port=(port)
28
+ @response_port = port
29
+ end
30
+
31
+ # Starts the underlying Ractor using the port-based pattern.
32
+ # In Ruby 4.0:
33
+ # - Main ractor creates response ports (one per worker)
34
+ # - Main ractor sends [work, response_port] to workers
35
+ # - Workers receive work and response_port, send results back via response_port
36
+ def start
37
+ RactorLogger.info("Starting Ractor #{@name} (Ruby 4.0 mode)",
38
+ ractor_name: @name)
39
+
40
+ # Capture timeout value before entering ractor (Ractors can't access Fractor.config)
41
+ # Get class-level timeout, or fall back to default of nil (no timeout)
42
+ # Note: We avoid accessing Fractor.config from ractor creation context
43
+ class_level_timeout = @worker_class.worker_timeout
44
+
45
+ # In Ruby 4.0, workers don't create their own ports
46
+ # They receive response_port from main ractor when work is sent
47
+ @ractor = Ractor.new(@name, @worker_class,
48
+ class_level_timeout) do |name, worker_cls, timeout_val|
49
+ RactorLogger.debug(
50
+ "Ractor started with worker class #{worker_cls} and timeout #{timeout_val.inspect}", ractor_name: name
51
+ )
52
+
53
+ # Instantiate the specific worker inside the Ractor
54
+ # Pass timeout as an option only if it's not nil, to avoid accessing self.class from ractor
55
+ worker = if timeout_val.nil?
56
+ worker_cls.new(name: name)
57
+ else
58
+ worker_cls.new(name: name, timeout: timeout_val)
59
+ end
60
+
61
+ # Main message processing loop
62
+ loop do
63
+ # Receive work from the main ractor (blocks until message available)
64
+ # In Ruby 4.0, main sends [work, response_port]
65
+ received = Ractor.receive
66
+ RactorLogger.debug("Received #{received.inspect}", ractor_name: name)
67
+
68
+ # Handle shutdown message
69
+ if received == :shutdown
70
+ RactorLogger.debug("Received shutdown message, terminating",
71
+ ractor_name: name)
72
+ break
73
+ end
74
+
75
+ # Extract work and response_port
76
+ # Main should send [work, response_port]
77
+ if received.is_a?(Array) && received.size == 2
78
+ work, response_port = received
79
+ else
80
+ # Legacy format for initialization or other messages
81
+ work = received
82
+ response_port = nil
83
+ end
84
+
85
+ # Handle initialize message (for backwards compatibility during startup)
86
+ if work.is_a?(Hash) && work[:type] == :initialize
87
+ RactorLogger.debug("Worker initialized", ractor_name: name)
88
+ next
89
+ end
90
+
91
+ begin
92
+ # Get the timeout for this worker (nil means no timeout)
93
+ worker_timeout = worker.timeout
94
+
95
+ # Process the work with timeout if configured
96
+ # Note: Ruby's Timeout.timeout uses threads which don't work with Ractors.
97
+ # We measure execution time and raise timeout error afterward if exceeded.
98
+ result = if worker_timeout
99
+ start_time = Time.now
100
+ process_result = worker.process(work)
101
+ elapsed = Time.now - start_time
102
+ if elapsed > worker_timeout
103
+ # Raise a timeout error after the fact
104
+ # Note: This is a post-facto timeout check - the work has already completed
105
+ raise Timeout::Error,
106
+ "execution timed out after #{elapsed}s (limit: #{worker_timeout}s)"
107
+ end
108
+
109
+ process_result
110
+ else
111
+ worker.process(work)
112
+ end
113
+
114
+ RactorLogger.debug("Sending result #{result.inspect}",
115
+ ractor_name: name)
116
+
117
+ # Wrap the result in a WorkResult object if not already wrapped
118
+ work_result = if result.is_a?(Fractor::WorkResult)
119
+ result
120
+ else
121
+ Fractor::WorkResult.new(result: result, work: work)
122
+ end
123
+
124
+ # Send the result back through the response port
125
+ if response_port
126
+ response_port << { type: :result, result: work_result,
127
+ processor: name }
128
+ else
129
+ RactorLogger.warn("No response port available, result lost",
130
+ ractor_name: name)
131
+ end
132
+ rescue Timeout::Error => e
133
+ # Handle timeout errors as retriable errors
134
+ RactorLogger.warn(
135
+ "Timed out after #{worker.timeout}s processing work #{work.inspect}", ractor_name: name
136
+ )
137
+ error_result = Fractor::WorkResult.new(
138
+ error: "Worker timeout: #{e.message}",
139
+ work: work,
140
+ error_category: :timeout,
141
+ )
142
+ if response_port
143
+ response_port << { type: :error, result: error_result,
144
+ processor: name }
145
+ end
146
+ rescue StandardError => e
147
+ # Handle errors during processing
148
+ RactorLogger.error("Error processing work #{work.inspect}",
149
+ ractor_name: name, exception: e)
150
+
151
+ # Send an error message back through the response port
152
+ # Ensure the original work object is included in the error result
153
+ error_result = Fractor::WorkResult.new(error: e.message, work: work)
154
+ if response_port
155
+ response_port << { type: :error, result: error_result,
156
+ processor: name }
157
+ end
158
+ end
159
+ end
160
+ rescue Ractor::ClosedError
161
+ RactorLogger.debug("Ractor closed", ractor_name: name)
162
+ rescue StandardError => e
163
+ RactorLogger.error("Unexpected error", ractor_name: name, exception: e)
164
+ ensure
165
+ RactorLogger.debug("Ractor shutting down", ractor_name: name)
166
+ end
167
+ RactorLogger.debug("Ractor instance created", ractor_name: name)
168
+ end
169
+ RactorLogger.debug("Ractor #{@ractor} started", ractor_name: @name)
170
+
171
+ # Sends work to the Ractor.
172
+ # In Ruby 4.0, sends [work, response_port] so worker can reply back.
173
+ # Special case: sends just :shutdown for shutdown messages.
174
+ #
175
+ # @param work [Fractor::Work, Symbol] The work item to process, or :shutdown
176
+ # @return [Boolean] true if sent successfully, false otherwise
177
+ def send(work)
178
+ if @ractor
179
+ begin
180
+ # In Ruby 4.0, send [work, response_port] so worker can reply
181
+ # Special case: shutdown is sent as a symbol, not an array
182
+ if work == :shutdown
183
+ @ractor.send(:shutdown)
184
+ else
185
+ @ractor.send([work, @response_port])
186
+ end
187
+ true
188
+ rescue Exception => e
189
+ RactorLogger.warn("Error sending work to Ractor: #{e.message}",
190
+ ractor_name: @name)
191
+ false
192
+ end
193
+ else
194
+ RactorLogger.warn("Attempted to send work to nil Ractor",
195
+ ractor_name: @name)
196
+ false
197
+ end
198
+ end
199
+
200
+ # Receives a message from the Ractor.
201
+ # In Ruby 4.0, messages come through response ports in the main loop.
202
+ # This method is kept for backwards compatibility with tests.
203
+ #
204
+ # Note: In Ruby 4.0, this will block if no message is available.
205
+ # The test should either skip this or ensure a message was sent first.
206
+ #
207
+ # @return [Hash, nil] The message received or nil
208
+ def receive_message
209
+ # In Ruby 4.0, we receive through the response_port
210
+ # Try to receive with a small timeout to avoid blocking indefinitely
211
+ return nil unless @response_port
212
+
213
+ # Use a non-blocking receive attempt
214
+ # In Ruby 4.0, if the port is empty, this would block
215
+ # For test compatibility, we return nil if no message is available
216
+ @response_port.receive
217
+ rescue Ractor::ClosedError, Ractor::Error
218
+ nil
219
+ end
220
+
221
+ # Closes the Ractor and its response port.
222
+ # In Ruby 4.0, we need to explicitly close the response port
223
+ # to prevent Ractor.select from hanging.
224
+ #
225
+ # @return [Boolean] true if closed successfully
226
+ def close
227
+ # Close the response port first
228
+ if @response_port
229
+ begin
230
+ # Send nil through the port to signal it's closing
231
+ @response_port.send(nil) if @response_port.respond_to?(:send)
232
+ rescue StandardError
233
+ # Port may already be closed
234
+ end
235
+ @response_port = nil
236
+ end
237
+
238
+ # Then close the underlying ractor
239
+ super
240
+ end
241
+ end
242
+ end