views:

638

answers:

3

Some of the users of our Ruby on Rails app have complained that page requests occasionally hang indefinitely under Safari (a couple have noticed it under Firefox, but it's overwhelmingly Safari users). After some investigation it seems that these requests are being served correctly by our Rails application and the hang occurs when fetching image assets (which are hosted on the same server) which are referenced in the HTML.

We have configured Apache to serve the image assets directly and bypass the Rails app for performance. We have also enabled gzip compression on text/javascript/css assets. Below are the relevant settings from our Apache Virtual Host configuration -- perhaps we have configured this in such a way which might explain these arbitrary hanging requests?

RewriteEngine On

# Correct behaviour of IE under SSL
SetEnvIf User-Agent ".*MSIE.*" \
    nokeepalive ssl-unclean-shutdown \
    downgrade-1.0 force-response-1.0

SSLEngine On
SSLCertificateFile /etc/httpd/conf/ssl/_.mycert.com.crt
SSLCertificateKeyFile /etc/httpd/conf/ssl/_. mycert.com.key
SSLCertificateChainFile /etc/httpd/conf/ssl/gd_bundle.crt

RequestHeader set X_ORIGINAL_PROTOCOL 'https'
RequestHeader set X_FORWARDED_PROTO 'https'

# Rewrite index to check for static
RewriteRule ^/$ /index.html [QSA] 
RewriteRule "^/(images|stylesheets|javascripts|system)/?(.*)" "$0" [L]

# Rewrite to check for Rails cached page
RewriteRule ^([^.]+)$ $1.html [QSA]

# Deflate
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript application/x-javascript
BrowserMatch ^Mozilla/4 gzip-only-text/html
BrowserMatch ^Mozilla/4\.0[678] no-gzip
BrowserMatch \bMSIE !no-gzip !gzip-only-text/html

ExpiresActive On
<FilesMatch "\.(ico|gif|jpe?g|png|js|css)$">
  ExpiresDefault "access plus 1 year"
  Header append Cache-Control "public"
</FilesMatch>

Has anyone experienced a similar problem before?

Our Ruby on Rails web application runs using mod_rails and Apache 2.2.3 on RedHat Enterprise Linux 5.

Update: I have now tried removing the following block and the problem still persists, so it looks like we can exclude the expires header from being the problem:

ExpiresActive On
<FilesMatch "\.(ico|gif|jpe?g|png|js|css)$">
  ExpiresDefault "access plus 1 year"
  Header append Cache-Control "public"
</FilesMatch>
A: 

Some things that might help, in no particular order:

  1. Use asset hosts, they are extremely easy to setup: http://api.rubyonrails.org/classes/ActionView/Helpers/AssetTagHelper.html
  2. Make sure that Javascript is loaded near the bottom of the page, when possible.
  3. Make sure that Apache/Nginx/whatever correctly sets the Expires-In HTTP header.
  4. Run Google Speed Page under Firefox, this will provide lots of good info.
pantulis
Thanks.1. We are already using asset hosts.2. Javascript is included in the <HEAD> section of each page3. When you say 'correct sets the expires header' what do you mean? We were setting this to 1 year but to try an debug this, I've actually turned off the setting of the expires header entirely and the problem still persists.4. Doesn't explain why we're seeing intermittent hangs for some users only unfortunately
Olly
In our experience, when the browser is loading Javascript files it won't download other static assets. Dont know if this will manifest as the problem you report in Safari when the user loads the page with a cold cache... (i.e., it wouldnt download the images). Reference here: http://developer.yahoo.net/blog/archives/2007/07/high_performanc_5.htmlMaybe using a plugin like asset_packager (http://github.com/sbecker/asset_packager/tree/master) will be of help, as all your Javascript will be packaged into one big file reducing overhead due to downloading many files.
pantulis
Using asset_packer is an interesting option, but I would like to know _why_ this is happening. I have just seen another case of it where the all the assets were served except one which caused the hang. This request -- to a Javascript file at https://assets3.mydomain.com -- does not appear in the mod_rewrite log at all so it looks like the browser hasn't actually requested this resource from Apache at all? Might SSL be an issue?
Olly
+2  A: 

Olly,

This may or may not be related, but browsers have a limit of 2 (by default) simultaneous connections that they can make. If there are connections kept open for communication, and you are also fetching images, the call to the image may not go through till the browser has one of its stipulated connections free. The hang during image fetch may actually be triggered by some other server connections which are not completing or are being held open by the server and browser. So you may actually be hunting in the wrong place.

If you are able to reproduce the error, try switching to HTTP 1.0 on your dev server and see if it fixes the issue. Also try moving some of the assets to another domain/subdomain and fetch from there.

Hope that gives you another angle.

Regards, Narayan

Narayan Raman
Of all current browsers, only IE6 and IE7 have such a low limit. See http://stackoverflow.com/questions/985431/max-parallel-http-connections-in-a-browser/985704#985704 for the numbers (no citation, sadly). Your point still stands, of course, but the limit is higher than 2.
RichieHindle
This is my immediate hunch, too. I believe the usual number is four connections per domain, for most browsers. If you're using Comet or other long-lived HTTP connections, this can end up starving other requests.You can work around this by creating a subdomain for some of your resources, for example create "static.myapp.com" for static images. That way the static and dynamic resources won't count against each other.
Jens Alfke
Hi everyone. We are using virtual asset servers for serving images, javascript and CSS, so I'm not convinced this is the answer. In our case we have 12 JS and CSS includes (and many other images) and these are being served from assets0.mydomain.com, assets1.mydomain.com, assets2.mydomain.com, assets3.mydomain.com -- Rails does this for me.
Olly
A: 

I was having a spectacularly good time debugging a similar issue on our site today. Safari users--but seemingly only those on a Mac--would complain that our site would "hang" randomly while loading a page. It usually appeared to be hanging on an image--2 of 3 items completed, etc.--but then I disabled the cache in Safari, JavaScript, and CSS, and guess what? The page still hung.

First, a note on Safari caching. Even if you have "Disable Caching" selected in the "Develop" menu and have selected "Empty Cache" from the "Safari" menu, you still have a RAM-based cache. This means that if you REALLY want to simulate an empty cache, you have to quit Safari with each request or hold down the Option + Shift + Prayer-To-Deity-of-Choice keys while pressing the Reload button. This took me a while to figure out, and until I figured it out, I had a hard time consistently reproducing the issue.

So! Without JavaScript, CSS, or images, what do you have left? Not much, aside from the actual HTML. This is what turned me onto the fact that is was probably compression related. And sure enough, turning off compression in IIS 6.0 resulted in the page loading instantaneously. Since IIS 6.0 doesn't have a convenient way of turning off compression based on the user agent, I used IIRF (an ISAPI filter that rewrites URLs) to rewrite the Accept-Encoding header when it comes from Safari:

# Safari doesn't handle gzip compression properly; we turn it off by setting 
# the q value to zero for all agents identifying themselves as Safari
RewriteCond %{HTTP_USER_AGENT} Safari
RewriteHeader Accept-Encoding: .* *;q=0

The issue seems to be that if Safari is receiving static compressed content (that is, the server sends the Content-Length header), then Safari deals with it just fine. But if Safari is receiving dynamic compressed content (that is, the server is serving a response being rendered by ASP.NET and the content length is not known until it's done, so the server sends Transfer-Encoding: chunked) then Safari goes into Flaky Sir-Hangs-A-Lot Mode.

Why? I have no idea. But this is how I work around it.

Nicholas Piasecki