Skip to content

Commit 150fac3

Browse files
committed
fix: ractors
1 parent e3df972 commit 150fac3

File tree

2 files changed

+80
-24
lines changed

2 files changed

+80
-24
lines changed

lib/hrma/build/document_generator.rb

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
require "fileutils"
55
require "ruby-progressbar"
66
require "etc"
7+
require "timeout"
8+
require "open3"
79
require_relative "../config"
810
require_relative "tools"
911
require_relative "ractor_document_processor"
@@ -146,12 +148,16 @@ def generate_parallel(xsd_files)
146148
mutex = Mutex.new
147149

148150
# Pass necessary tool paths to Ractors
151+
# Make sure paths are simple strings that can be safely shared across Ractors
149152
tools_constants = {
150-
xsdvi_path: Tools::XSDVI_PATH,
151-
xsdmerge_path: Tools::XSDMERGE_PATH,
152-
xs3p_path: Tools::XS3P_PATH
153+
xsdvi_path: Tools::XSDVI_PATH.to_s,
154+
xsdmerge_path: Tools::XSDMERGE_PATH.to_s,
155+
xs3p_path: Tools::XS3P_PATH.to_s
153156
}
154157

158+
# Make the hash shareable across Ractors
159+
tools_constants = Ractor.make_shareable(tools_constants)
160+
155161
# Create a pool Ractor that will distribute work
156162
pool = Ractor.new do
157163
# This Ractor acts as a work distributor
@@ -167,7 +173,13 @@ def generate_parallel(xsd_files)
167173

168174
# Create worker Ractors with improved error handling
169175
workers = ractor_count.times.map do |i|
170-
Ractor.new(pool, @log_dir, Dir.pwd, tools_constants, i) do |pool, log_dir, pwd, tool_paths, id|
176+
# Ensure all values passed to Ractor are shareable
177+
log_dir_copy = @log_dir.nil? ? nil : @log_dir.to_s
178+
pwd_copy = Dir.pwd.to_s
179+
worker_id = i
180+
181+
# Create worker Ractor with explicitly shareable values
182+
Ractor.new(pool, log_dir_copy, pwd_copy, tools_constants, worker_id) do |pool, log_dir, pwd, tool_paths, id|
171183
# Worker Ractor
172184
loop do
173185
begin
@@ -216,10 +228,17 @@ def generate_parallel(xsd_files)
216228
end
217229
end
218230

231+
# Ensure xsd_files is shareable - convert all entries to simple strings
232+
shareable_files = xsd_files.map(&:to_s)
233+
234+
# Make the array shareable across Ractors
235+
shareable_files = Ractor.make_shareable(shareable_files)
236+
219237
# Send all files to the pool with better error handling
220-
xsd_files.each do |file|
238+
shareable_files.each do |file|
221239
begin
222240
puts "Main: Sending file #{file} to queue"
241+
# File names are passed as plain strings, which are always shareable
223242
pool.send(file)
224243
rescue => e
225244
puts "Main: Error sending file #{file} to queue: #{e.message}"
@@ -240,8 +259,19 @@ def generate_parallel(xsd_files)
240259
# Wait for any worker to produce a result with a timeout
241260
worker, result = Ractor.select(*workers)
242261

243-
# Process the result
244-
file, success, error_message = result
262+
# Process the result - make sure we're handling the result safely
263+
# Each result should be an array [file, success, error_message]
264+
if result.is_a?(Array) && result.size >= 3
265+
file = result[0].to_s
266+
success = !!result[1] # Convert to boolean explicitly
267+
error_message = result[2] ? result[2].to_s : nil
268+
else
269+
# Handle unexpected result format gracefully
270+
file = "unknown_file"
271+
success = false
272+
error_message = "Invalid result format from worker"
273+
puts "Main: Received unexpected result format: #{result.inspect}"
274+
end
245275

246276
mutex.synchronize do
247277
processed_count += 1

lib/hrma/build/ractor_document_processor.rb

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
# frozen_string_literal: true
22

3+
require 'open3'
4+
require 'fileutils'
5+
36
module Hrma
47
module Build
58
# A self-contained class for processing XSD documents that can run within a Ractor
69
class RactorDocumentProcessor
10+
# Keep these methods pure and Ractor-friendly
11+
# They should not use unshareable objects like Procs, Thread-locals, etc.
712
# Process a batch of XSD files
813
#
914
# @param files [Array<String>] List of XSD files to process
@@ -45,7 +50,8 @@ def self.process_single_file(file, log_dir, pwd, tool_paths)
4550
# @return [Array] Result of processing [file_path, success_flag, error_message]
4651
def self.process_with_logging(file, log_dir, pwd, tool_paths)
4752
log_file = File.join(log_dir, "#{File.basename(file, '.xsd')}.log")
48-
FileUtils.mkdir_p(File.dirname(log_file))
53+
# Use Dir.mkdir instead of FileUtils to avoid unshareable Procs
54+
safe_mkdir_p(File.dirname(log_file))
4955

5056
File.open(log_file, 'w') do |log|
5157
log.puts "Processing #{file}..."
@@ -84,8 +90,9 @@ def self.process_file(file, pwd, tool_paths, log)
8490

8591
log.puts "Generating documentation for #{file}..."
8692

87-
# Create output directory
88-
create_output_directory(paths[:output_dir])
93+
# Create output directory using ractor-safe approach
94+
safe_mkdir_p(paths[:output_dir])
95+
safe_mkdir_p(File.join(paths[:output_dir], "diagrams"))
8996

9097
# Generate diagrams
9198
return "Error generating diagrams" unless generate_diagrams(file, pwd, paths[:output_dir], tool_paths[:xsdvi_path], log)
@@ -98,7 +105,7 @@ def self.process_file(file, pwd, tool_paths, log)
98105
return "Error generating documentation" unless generate_final_doc(paths[:temp_file], paths[:output_file], file_basename, tool_paths[:xs3p_path], log)
99106

100107
# Clean up and return success
101-
FileUtils.rm_f(paths[:temp_file])
108+
safe_rm(paths[:temp_file])
102109
true
103110
end
104111

@@ -115,8 +122,9 @@ def self.process_file_without_logging(file, pwd, tool_paths)
115122
# Skip if up to date
116123
return true if skip_if_up_to_date?(paths[:output_file], file)
117124

118-
# Create output directory
119-
create_output_directory(paths[:output_dir])
125+
# Create output directory using ractor-safe approach
126+
safe_mkdir_p(paths[:output_dir])
127+
safe_mkdir_p(File.join(paths[:output_dir], "diagrams"))
120128

121129
# Generate diagrams
122130
diagrams_cmd = "java -jar #{tool_paths[:xsdvi_path]} #{pwd}/#{file} -rootNodeName all -oneNodeOnly -outputPath #{paths[:output_dir]}/diagrams"
@@ -132,7 +140,7 @@ def self.process_file_without_logging(file, pwd, tool_paths)
132140
return "Error generating documentation" unless system(xs3p_cmd)
133141

134142
# Clean up and return success
135-
FileUtils.rm_f(paths[:temp_file])
143+
safe_rm(paths[:temp_file])
136144
true
137145
end
138146

@@ -165,12 +173,36 @@ def self.skip_if_up_to_date?(output_file, source_file)
165173
File.exist?(output_file) && File.mtime(output_file) > File.mtime(source_file)
166174
end
167175

168-
# Create output directory
176+
# Safe mkdir_p implementation that doesn't rely on FileUtils
177+
# This avoids the un-shareable Proc issue in Ractors
169178
#
170-
# @param output_dir [String] Path to the output directory
179+
# @param dir [String] Directory path to create
171180
# @return [void]
172-
def self.create_output_directory(output_dir)
173-
FileUtils.mkdir_p("#{output_dir}/diagrams")
181+
def self.safe_mkdir_p(dir)
182+
return if Dir.exist?(dir)
183+
184+
# Create parent directories first
185+
parent = File.dirname(dir)
186+
unless Dir.exist?(parent)
187+
safe_mkdir_p(parent)
188+
end
189+
190+
# Create the directory
191+
begin
192+
Dir.mkdir(dir)
193+
rescue Errno::EEXIST
194+
# Directory already exists (possible race condition)
195+
end
196+
end
197+
198+
# Safe file removal that doesn't rely on FileUtils
199+
#
200+
# @param file [String] File to remove
201+
# @return [void]
202+
def self.safe_rm(file)
203+
File.unlink(file) if File.exist?(file)
204+
rescue Errno::ENOENT
205+
# File doesn't exist, which is what we wanted anyway
174206
end
175207

176208
# Generate diagrams
@@ -184,8 +216,6 @@ def self.create_output_directory(output_dir)
184216
def self.generate_diagrams(file, pwd, output_dir, xsdvi_path, log = nil)
185217
diagrams_cmd = "java -jar #{xsdvi_path} #{pwd}/#{file} -rootNodeName all -oneNodeOnly -outputPath #{output_dir}/diagrams"
186218

187-
require 'open3'
188-
189219
if log
190220
log.puts "Running: #{diagrams_cmd}"
191221
stdout_and_stderr_str, status = Open3.capture2e(diagrams_cmd)
@@ -209,8 +239,6 @@ def self.generate_diagrams(file, pwd, output_dir, xsdvi_path, log = nil)
209239
def self.generate_merged_xsd(file, temp_file, xsdmerge_path, log = nil)
210240
xsdmerge_cmd = "xsltproc --nonet --stringparam rootxsd #{file} --output #{temp_file} #{xsdmerge_path} #{file}"
211241

212-
require 'open3'
213-
214242
if log
215243
log.puts "Running: #{xsdmerge_cmd}"
216244
stdout_and_stderr_str, status = Open3.capture2e(xsdmerge_cmd)
@@ -235,8 +263,6 @@ def self.generate_merged_xsd(file, temp_file, xsdmerge_path, log = nil)
235263
def self.generate_final_doc(temp_file, output_file, file_basename, xs3p_path, log = nil)
236264
xs3p_cmd = "xsltproc --nonet --param title \"'Schema Documentation for #{file_basename}'\" --output #{output_file} #{xs3p_path} #{temp_file}"
237265

238-
require 'open3'
239-
240266
if log
241267
log.puts "Running: #{xs3p_cmd}"
242268
stdout_and_stderr_str, status = Open3.capture2e(xs3p_cmd)

0 commit comments

Comments
 (0)