This is the notes about what I experienced last week.
Background
I work with Ruby on Rails normally. Last week, I worked on a project to upload images to S3 using Carrierwave.
Carrierwave is a quite popular gem to upload images and S3 is also popular for the place where the images are stored. I have had many projects using those techniques before and I thought I was enough familiar with them. However, I experienced a situation where the uploaded images disappeared before uploading to S3. The following code snippets are what I had at the beginning. They are simplified but I can show the cause with the snippets.
ActiveRecord.transaction do
user.update!(remote_avatar_url: 'https://smaple_image.jpg')
# More saves/updates here
Users::DoSomethinService.new(user: user.reload).call
end
Then I started investigating it.
What I did
First of all, I needed to figure out which code caused the problem. So I removed one line by one line and executed the code snippets until the uploading succeeded. Then it succeeded when I removed the following code line.
Users::DoSomethingService.new(user: user.reload).call
Then it succeeded when I removed the user.reload
. Based on that, I assumed something went wrong with the reload
method. So I googled with reload
and Carrierwave and I got the issue in its GitHub repository.
From the conversation, Carrierwave uploads the images to S3 using an after_commit
callback. I also checked the code itself and currently, the afrer_save
callback is used for storing images in the cloud.
But I still use an old version of Carrierwave so the after_commit
does in my case. Then I checked the order of commits for my code snippets with logs, which is below.
D, [2024-04-11T16:15:48.691945 #8092] DEBUG -- : (8.6ms) BEGIN
D, [2024-04-11T16:15:48.719280 #8092] DEBUG -- : User Load (8.9ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 100], ["LIMIT", 1]]
D, [2024-04-11T16:15:56.640862 #8092] DEBUG -- : User Update (9.9ms) UPDATE "users" SET "avatar" = $1, "updated_at" = $2 WHERE "users"."id" = $3 [["avatar", "\"b28d4650dd6f-4776FA66_1DD8_423D_B934_A92DED482C4A.jpeg\""], ["updated_at", "2024-04-11 16:15:56.629934"], ["id", 100]]
D, [2024-04-11T16:15:56.650267 #8092] DEBUG -- : User Load (8.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2 [["id", 100], ["LIMIT", 1]]
D, [2024-04-11T16:15:56.881915 #8092] DEBUG -- : (10.5ms) COMMIT
Then I noticed what happened to me. The reload was called before the database commit was executed because of the transaction
block, which deleted the image data from the instance of the user
active record. So for this case, I fixed my codes like below.
ActiveRecord.transaction do
user.update!(remote_avatar_url: 'https://smaple_image.jpg')
# More saves/updates here
end
Users::DoSomethinService.new(user: user.reload).call
In my case, there weren’t any database transactions in DoSomethinService
class. It was just to communicate the data with a third-party service. So I just put it outside of the transaction
, let it commit first, and let the user record reload. The solution is totally up to your codes so I won’t dig further.
That’s it!