😮

has_many_attachedを使用した際、transaction内で複数枚同時にattachすると一枚しかアップロードされない

2021/09/29に公開

環境

Ruby: 3.0.1
Rails: 6.1.4.1
storage: Google Cloud Storage

(おそらくS3等でも同じ事象が起きると思いますが未検証です)

発生する問題

app/model/article.rb
  has_many_attached :images

設定時に

任意の場所
ActiveRecord::Base.transaction do
  article = Article.find(params[:id])
  
  params[:images].count #=> 3

  params[:images].each do |image|
    article.image.attach(image)
  end
  
  article.save
end

みたいなことをすると、Transactionは成功するが、タイトル通りStorageには一枚しかファイルがアップロードされず、成功した一枚以外のファイルのURLを参照すると404となる。

ActiveStorage::Blob は問題なく作成されており、Rails上はURLを見ない限りこの事象が起きたかどうかを確認することはできない。
なお、transaction後に発火する ActiveStorage::AnalyzerJob ではファイルを参照しに行くためそこでErrorが出る。

[ActiveJob] [ActiveStorage::AnalyzeJob] [...] 404
[ActiveJob] [ActiveStorage::AnalyzeJob] [...] #<HTTP::Message:0x0000000116c2e838 @http_header=#<HTTP::Message::Headers:0x0000000116c2e810 @http_version="1.1", @body_size=0, @chunked=false, @request_method="GET", @request_uri=#<Addressable::URI:0x4b00 URI:https://storage.googleapis.com/storage/v1/b/*****?>, @request_query=nil, @request_absolute_uri=nil, @status_code=404, @reason_phrase="Not Found", @body_type=nil, @body_charset=nil, @body_date=nil, @body_encoding=#<Encoding:UTF-8>, @is_request=false, @header_item=[["X-GUploader-UploadID", "****"], ["Content-Type", "application/json; charset=UTF-8"], ["Date", "Tue, 28 Sep 2021 14:44:38 GMT"], ["Vary", "Origin"], ["Vary", "X-Origin"], ["Cache-Control", "no-cache, no-store, max-age=0, must-revalidate"], ["Expires", "Mon, 01 Jan 1990 00:00:00 GMT"], ["Pragma", "no-cache"], ["Content-Length", "289"], ["Server", "UploadServer"], ["Alt-Svc", "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000,h3-T051=\":443\"; ma=2592000,h3-Q050=\":443\"; ma=2592000,h3-Q046=\":443\"; ma=2592000,h3-Q043=\":443\"; ma=2592000,quic=\":443\"; ma=2592000; v=\"46,43\""]], @dumped=false>, @peer_cert=#<OpenSSL::X509::Certificate: subject=#<OpenSSL::X509::Name CN=*.storage.googleapis.com>, issuer=#<OpenSSL::X509::Name CN=GTS CA 1C3,O=Google Trust Services LLC,C=US>, serial=#<OpenSSL::BN:0x0000000116e2c748>, not_before=2021-08-30 02:53:55 UTC, not_after=2021-11-22 02:53:54 UTC>, @http_body=#<HTTP::Message::Body:0x0000000116c2e770 @body="{\n  \"error\": {\n    \"code\": 404,\n    \"message\": \"No such object: ******\",\n    \"errors\": [\n      {\n        \"message\": \"No such object: *******\",\n        \"domain\": \"global\",\n        \"reason\": \"notFound\"\n      }\n    ]\n  }\n}\n", @size=0, @positions=nil, @chunk_size=nil>, @previous=nil>
[ActiveJob] [ActiveStorage::AnalyzeJob] [...] Caught error notFound: No such object: ******

なぜ起きるのか、対応状況

Rails 5.2 -> 6.0 で、ActiveStorageのファイル追加・アップロード処理の発火タイミングが変わった
Store newly-uploaded files on save rather than assignment #33303

これによりデグレが起きたようで、rails/railsにもissueが上がっている
Multiple attachment files are not persisted to storage service when attached separately within a transaction #41661
Calling attach multiple times inside a transaction only uploads the last one #42343

対応するPRも上がっている
Fix #41661 attaching multiple times within transaction #42300

が、どうやら直し方に課題があったのか、優先度が低いのかはわからないが暫く反応が無かったためbotによりCloseされている。

現状は該当のattachの処理部だけをtransactionから外す、になりそう。

Discussion