Rails ActiveStorage在Sidekiq任务中出现文件找不到错误
ActiveStorage 集成 Sidekiq 后头像附件文件找不到问题解决
问题背景
原本用以下代码处理用户头像上传,运行完全正常:
if params[:avatar] authorize! :update, current_user type, base64_image = Utils::Attach.type_and_base64(params[:avatar], 'avatar') current_user.avatar.attach(io: StringIO.new(base64_image), filename: "#{current_user.id}-#{Time.current.to_i}", content_type: type) current_user.save! end
但给邮件服务集成Sidekiq后,执行头像附件操作时频繁出现文件找不到的错误。
核心原因分析
- 异步任务触发过早:ActiveStorage还没把文件完全写入存储,Sidekiq的邮件任务就已经启动,尝试读取还不存在的文件
- 进程间存储配置不一致:如果用本地存储,Sidekiq进程和主应用进程的文件目录权限或路径不匹配;如果用云存储,可能存在文件同步延迟,或者Sidekiq进程的云存储配置有误
- StringIO临时对象的局限性:直接用StringIO上传,可能在异步场景下出现文件未持久化完成的情况
解决方案
方案一:确保附件持久化后再触发异步任务
不要在请求周期内直接调用Sidekiq邮件任务,改用after_commit回调,等数据库和存储操作都彻底完成后再触发:
修改上传代码:
if params[:avatar] authorize! :update, current_user type, base64_image = Utils::Attach.type_and_base64(params[:avatar], 'avatar') current_user.avatar.attach(io: StringIO.new(base64_image), filename: "#{current_user.id}-#{Time.current.to_i}", content_type: type) current_user.save! # 用run_after_commit确保所有数据库操作提交后再触发邮件 current_user.run_after_commit do UserMailer.update_avatar_notification(current_user).deliver_later end end
或者在User模型里加回调(更规范):
class User < ApplicationRecord has_one_attached :avatar # 仅在头像新增/更新且提交完成后发邮件 after_commit :send_avatar_update_email, on: :update, if: -> { avatar.attached? && avatar.saved_change_to_attached? } private def send_avatar_update_email UserMailer.update_avatar_notification(self).deliver_later end end
方案二:统一Sidekiq与主进程的存储配置
- 本地存储:检查
config/storage.yml里的:local配置,确保Sidekiq进程有权限读写存储目录(比如public/storage),且路径和主应用完全一致 - 云存储:确认Sidekiq进程加载了正确的云存储密钥、区域等配置,必要时在邮件任务里加短暂延迟(比如
sleep 1),或者利用云存储的上传完成回调触发邮件
方案三:改用临时文件上传替代StringIO
把base64内容先写入本地临时文件,再上传到ActiveStorage,避免StringIO在异步场景下的不稳定:
if params[:avatar] authorize! :update, current_user type, base64_image = Utils::Attach.type_and_base64(params[:avatar], 'avatar') # 创建临时文件,后缀匹配文件类型 ext = type.split('/').last temp_file = Tempfile.new(["#{current_user.id}-#{Time.current.to_i}", ".#{ext}"], Rails.root.join('tmp')) begin temp_file.write(base64_image) temp_file.rewind # 把文件指针移到开头,确保能被读取 current_user.avatar.attach(io: temp_file, filename: temp_file.path.split('/').last, content_type: type) current_user.save! ensure temp_file.close temp_file.unlink # 用完删除临时文件 end end
内容的提问来源于stack exchange,提问作者vladislav
相关产品推荐
相关产品推荐

