Ruby多线程场景下如何避免Tempfile内容为空?
问题根源分析
- Tempfile自动回收机制:Ruby的
Tempfile对象被GC回收时会自动删除文件。主线程超时结束请求后,控制器实例销毁,file变量被GC回收,子线程还未完成写入或读取操作时,文件就已被清空或删除。 - 线程资源竞争:主线程与子线程共享同一个
Tempfile实例,主线程结束后可能提前关闭文件句柄,导致子线程写入失败。 deliver_later异步特性:deliver_later会将任务放入Active Job队列,若直接传递file.read,文件可能已被清理,或序列化时出现异常。
解决方案
1. 改用持久化临时文件替代Tempfile
放弃Tempfile的自动清理机制,手动在tmp目录创建唯一命名的临时文件,确保子线程与Active Job都能稳定访问文件内容。
2. 分离线程资源操作
子线程独立完成文件写入逻辑,主线程仅负责判断超时并通知用户,避免共享文件句柄引发的竞态问题。
3. 手动管理数据库连接
子线程中使用Active Record时,需通过连接池手动获取和释放连接,防止连接泄漏。
修改后的代码
def report # 生成唯一临时文件路径,避免命名冲突 file_path = Rails.root.join('tmp', "report_#{SecureRandom.uuid}.csv") queue = Queue.new thr = Thread.new do # 从连接池获取数据库连接 ActiveRecord::Base.connection_pool.with_connection do query = %Q(blah blah blah) @calibrations = ActiveRecord::Base.connection.exec_query(query) query = %Q(blah blah blah) @tunings = ActiveRecord::Base.connection.exec_query(query) unless @tunings.empty? # 写入CSV内容到临时文件 CSV.open(file_path, 'wb') do |csv| csv << ["headers...", @parameters].flatten @calibrations.each do |c| line = [c["h1"], c["h2"], c["h3"], c["h4"], c["h5"], c["h6"], c["h7"], c["h8"]] t = @tunings.select { |t| t["code"] == c["code"] }.first @parameters.each do |parameter| line << t[parameter.downcase] end csv << line end end end # 根据队列状态执行不同逻辑 if queue.empty? # 未超时:直接返回文件给用户,随后清理临时文件 if File.exist?(file_path) send_data File.read(file_path), type: 'text/csv; charset=iso-8859-1; header=present', disposition: "attachment; filename=\"report.csv\"" File.delete(file_path) if File.exist?(file_path) end else # 超时:发送邮件,延时清理文件给Active Job留足读取时间 if File.exist?(file_path) NotificationMailer.report_ready(current_user, File.read(file_path)).deliver_later Thread.new { sleep 30; File.delete(file_path) if File.exist?(file_path) } end end end end give_up_at = Time.now + 120.seconds while Time.now < give_up_at do break unless thr.alive? sleep 1 end if thr.alive? queue << "Timeout" render html: "Your report is taking longer than 2 minutes to generate. To avoid a browser timeout, it will finish in the background, and the report will be sent to you in email." end end
额外注意事项
- 文件清理:除了延时线程清理,还可通过定时任务(如Sidekiq Cron)定期清理
tmp目录下的过期临时文件。 - 错误捕获:在子线程中添加异常捕获逻辑,避免因查询或写入失败导致文件未清理、邮件未发送。
- 邮件优化:若报表体积较大,建议直接传递文件路径给邮件方法,在邮件内部读取文件,避免序列化大内容引发性能问题。
内容的提问来源于stack exchange,提问作者David Krider
相关产品推荐
相关产品推荐

