I’m trying to simplify the use case here as much as possible but I have the following model:
class Product < ApplicationRecord
belongs_to :company
has_many_attached :images
after_create_commit -> do
broadcast_prepend_to(
[company, "products_list"],
partial: "products/summary",
locals: {
product: self,
},
target: "products_list"
)
end
end
Output:
When creating a product this produces an image that can’t be served, S3 returns a 404 not found. If I refresh the page, it works. I’ve added a sleep
before calling broadcast_prepend_to
thinking it was an eventual consistency issue, it doesn’t change anything. Is it because the sleep needs to be on the image callback?
Interestingly, removing the after_create_commit
and moving it into a create.turbo_stream.erb completely removes the issue:
<%= turbo_stream.prepend "products_list",
partial: "products/summary",
locals: {
product: @product,
}
%>
Both are producing the exact same redirect URIs.
Any other theories/ideas I could try?
Do you have the same issue if you use service: "Disk"
? That could help pin point the problem. Also are you using direct uploads or server side uploads?
@nickjanetakis ya, changing it to :local
- works fine.
I fetch the images from Amazon and attach it before saving like this:
def fetch_images
files = []
external_response["Images"]["Primary"].each do |size, obj|
retries = 0
begin
Rails.logger.debug "Fetching image: #{obj["URL"]}"
filext = File.extname(obj["URL"])
filename = "#{external_id}_#{size}#{filext}"
io = URI.open(obj["URL"])
files << { filename: filename, file: io }
rescue Net::OpenTimeout => e
retry if (retries += 1) < 3
end
end
files.each do |f|
images.attach(
io: f[:file],
filename: f[:filename],
content_type: "image/jpeg",
)
end
end
You probably have a race condition between the rendering of the broadcast and the creation of the attachment between the product and the blob.
If your form creates the product and upload your images at the same time, especially with direct upload, the sequence is:
- Create blob
- Upload images
- Submit form
- Create product
- Create attachment between product and blobs
So your callback is running immediately after 4 and didn’t leave enough time for 5 to happen, which means that when the HTML is rendered, the “images” relation is still empty.
@brenogazzola thanks for that. Can you share more, i’m obviously doing images.attach
so isn’t the product object hydrated correctly? Is there a fix you’d recommend?
Could you share the controller code so I can see the order that things are happening?
Yes, it’s:
def create
@product = current_company.products.build(product_params)
@product.fetch_from_amazon
if @product.save
respond_to do |format|
format.html { redirect_to products_path, notice: "Product created " }
format.turbo_stream { flash.now[:notice] = "Product created" }
end
else
...
end
fetch_from_amazon
will:
- go get product attributes
- calls
fetch_images
(code in previous post)
Ok, it’s not quite what I said. When you call save, the product is saved first, which causes the callback to fire and send the HTML. While this is happening, Rails is creating the blobs and attachments, and only then uploading the file.
Which means that by the time the HTML arrives in your browser and it requests the the image, Rails hasn’t had the time to upload the files yet. The reason putting the code in the controller code works, is because by the time turbo_stream
is called, the files have already been uploaded.
Either keep the code in the controller or wrap the call to save
in a transaction, which will delay the commit (and therefore the callback) to after the files have been uploaded.