You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Ruby多线程场景下如何避免Tempfile内容为空?

问题根源分析
  1. Tempfile自动回收机制:Ruby的Tempfile对象被GC回收时会自动删除文件。主线程超时结束请求后,控制器实例销毁,file变量被GC回收,子线程还未完成写入或读取操作时,文件就已被清空或删除。
  2. 线程资源竞争:主线程与子线程共享同一个Tempfile实例,主线程结束后可能提前关闭文件句柄,导致子线程写入失败。
  3. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.16 14:55:24