views:

195

answers:

1

I have a webservice method that reads a photo and returns its byte data. I'm currently doing the following:

@photo_bytes = IO.read("/path/to/file")
send_data(@photo_bytes, :filename => "filename", :type => "filetype", :disposition => "inline")

I'm getting some strange behavior when calling this a lot... occasionally send_data is returning null. I'm thinking that maybe I'm getting read contention if a file hasn't been closed yet. Do I need to explicitly close the file after opening it with IO.read? How could I use read_nonblock to do this and would it be worth it?

UPDATE:

So I did some more logging and occasionally IO.read is returning a value like 1800 bytes when it usually returns ~5800 bytes for a picture. When it return 1800 bytes the picture does not show up on the client. This happens fairly randomly when two users are calling the web service.

Thanks

Tom

+3  A: 

The IO.read method doesn't do any advisory file locking, and so shouldn't be affected by other concurrent readers. However, if you have code elsewhere in your application which writes to the same path, you need to make sure you update the file atomically. Opening a file in write (not append) mode immediately truncates the file to zero bytes, so until the new version had been written, you could well see empty responses generated from the above snippet.

Assuming you're on a *NIX platform like Linux or OS X, though, you can update a file atomically using code like this:

require 'tempfile'
require 'fileutils'

def safe_write(path, data)
  tmp = Tempfile.new
  tmp.write(data)
  tmp.close
  FileUtils.mv(tmp.path, path)
end

This will write data to a temporary file, then move it to the "/path/to/file" location atomically, without readers ever seeing the zero-length truncated version.

rcoder
Once a given file is written, it will never be updated. This is a profile photo system. User uploads photo, it is retrieved multiple times by various users. If the user uploads a new photo, a new file is created. It seems like send_data is sending null occasionally when I have a lot of users hitting the site.
cakeforcerberus
And by "a lot" I mean two. :)
cakeforcerberus
Oh, also, I'm also pulling up the data with <% image_tag filename %> - does THIS lock the file?
cakeforcerberus
I meant <%= image_tag filename %> obviously ^^
cakeforcerberus
No, `image_tag` doesn't do any locking -- in fact, it shouldn't even depend on the file existing; it just munges the path to reflect your local asset path/server configuration.Also, if you're seeing this at concurrency levels of *two* simultaneous requests, that lead me to believe that something else is fundamentally wrong here. What web server are you using to serve these requests? Can you hit higher concurrency levels for HTML view files without any issues? Are only some image files affected?
rcoder
We're using apache with mod_passenger and fastcgi. It's a "webservice" method (meaning many clients could be calling it simultaneously) - All my variables are instance scoped. If someone calls get picture for user 1 twice simultaneously, could the instance scoped photo_bytes get mangled? Or is a new instance of the controller class fired up for every request? Thanks for your help btw, +1 :)
cakeforcerberus
Each invocation of a controller method happens on a new instance. The instance variables are really only used to share state when calling private instance methods and rendering views. Everything you're describing sounds pretty reasonable, though, so I suspect you may be triggering a bug in Passenger itself.As a (much faster) workaround, you could try using the X-Sendfile header to offload the actual image serving from Rails to Apache itself. You'll need to install mod_xsendfile, but it's a cakewalk to configure compared to Passenger.
rcoder
Thanks for your help. I'm going to accept the answer and try to use some of your suggestions. :)
cakeforcerberus