Image loaded using send_data are half broken when loading multiple images in iOS

Viewed 142

For reasons I don't want to get into here I need to resize images that are coming from users cell phones (and are stupid large) from blobs on the fly when loading them using this tag

image_tag thumbnail_document_path(document.id), alt: "#{document.document_title}", class: "document-thumbnail"

this calls the thumbnail controller action

  def thumbnail
    document = Document.find params[:id]
    image_meta = document.image_details.first
    image = Document.get_image_for_display image_meta
    send_data image, type: "image/jpg", disposition: "inline"
  end

Document.get_image_for_display pulls the blob from a database, checks the metadata we save separately to see if it's a PDF that needs to be converted to an image or a huge image that needs resizing, and then does all that and asynchronously sends the blob. This works fine on everything except for iOS. on iOS safari and Chrome if you're loading multiple images this way at the same time this happens. If anyone has an explanation or solution for this I'd love to hear it!

Edit: This app shares a server and database with a C++ desktop app and does not(and can not) have access to the locations on the server where these files are stored and there are hundreds of thousands of files across hundreds of client servers so storing a resized version next to the full size one is off the table due to physical storage limitations. The issue only appears to happen when trying to convert and load more than 3 large images and then only the first image stays broken, the others will fully load, although it takes a while for them to do it.

2 Answers

Your thumbnail action code implies that get_image_for_display must be synchronous, because it's return value is used right away. Also it should be reenterable if you're using a threaded server (like puma) - so that two instances can run concurrently inside one process. This requires it to not write any state (class variables, other globals), or at least do it with synchronization. (mutexes and so on).

But even if your server is single-threaded, in production usually there're multiple processes. So for each step of the function think of how a concurrent run can interfere.

For example: I can guess that the function runs some external apps to process the data. First thing is to check that there's no temporary file name collision - when two instances write data to the same file (even if the data is going to be the same) - obviously it'll be broken.

This usually happens when the servers breaks the connection with the browser while streaming the data due to several reasons like networking or others.

Recommended approach here is to use send_file or use buffer by doing something like this. Streaming in chunks always ensures the data is completely streamed to the client

File.open('filename.jpeg', 'rb') do |file|
  send_data(file.read, type: "image/jpg", disposition: "inline")
end
Related