Subversion Timestamps + Capistrano finalize_update

Update 2008/06/13: Jamis released Capistrano 2.4.0, and it includes the :normalize_asset_timestamps patch that I submitted!

Update 2008/06/11: Here's a link back to the Google Groups Discussion regarding this topic.

Subversion has a lesser-known feature that allows you to specify that checkouts/exports/switches/reverts should timestamp files with the last committed timestamp. By default, this setting is turned off. As such, when you checkout a repository, every file is timestamped with the current time on your local machine.

To be honest, I'm not quite sure why this is the default. The pertinent section of the Subversion manual (you have to scroll to the bottom) describes the setting as such:


use-commit-times

Normally your working copy files have timestamps that reflect the last time they were touched by any process, whether that be your own editor or by some svn subcommand. This is generally convenient for people developing software, because build systems often look at timestamps as a way of deciding which files need to be recompiled.

In other situations, however, it's sometimes nice for the working copy files to have timestamps that reflect the last time they were changed in the repository. The svn export command always places these “last-commit timestamps” on trees that it produces. By setting this config variable to yes, the svn checkout, svn update, svn switch, and svn revert commands will also set last-commit timestamps on files that they touch.


It's not clear to me why the default aids in Makefiles. What is clear to me though is that Jamis Buck has taken this default behavior into account in Capistrano, the wonderful deployment tool we use at SLS.

The following code snippets will require a bit of understanding of the built-in Capistrano deployment recipes. Let's take a look at the code for the :finalize_update task. This task is invoked after the code has been updated on the server (for Subversion, either by an export or update).

task :finalize_update, :except => { :no_release => true } do  
    # ... other details omitted

    stamp = Time.now.utc.strftime("%Y%m%d%H%M.%S")
    asset_paths = %w(images stylesheets javascripts).map { |p| "#{latest_release}/public/#{p}" }.join(" ")
    run "find #{asset_paths} -exec touch -t #{stamp} {} ';'; true", :env => { "TZ" => "UTC" }
  end

What this snippet of code does is compute a timestamp, and then touch each asset file on the server with that timestamp (-t #{stamp}). The intention for doing this was to handle the scenario where you have multiple asset servers. Since an export/checkout updates the timestamp with the local machine's current time, it's possible for the same asset to have different timestamps on separate asset servers.

So what's the big deal? First, rails serves up images (when using the image_tag helper) with a querystring appended to it. This querystring is simply a timestamp. The reason for this is to support client-side caching (you're doing this, right?). This basically allows you to set the "expires" attribute of that file several years (decades or millenniums, in fact) into the future. The reason this is so is because if that file ever changes, it's last modified attribute will also change, effectively changing the querystring rails appends automatically, and causing your browser to download a 'new asset.' So, when the finalize_update task is invoked (which happens every time you re-deploy), all of these last-modified timestamps are reset, causing any repeat visitors to re-download these very same assets again.

I have submitted a patch to Jamis (which I hope he'll apply soon!) that exposes an extra Capistrano parameter (:normalize_asset_timestamps), which would be set to true by default, leaving the original behavior in tact. The new :finalize_update task looks like:

task :finalize_update, :except => { :no_release => true } do  
    # ... other details omitted

    if fetch(:normalize_asset_timestamps, true)
      stamp = Time.now.utc.strftime("%Y%m%d%H%M.%S")
      asset_paths = %w(images stylesheets javascripts).map { |p| "#{latest_release}/public/#{p}" }.join(" ")
      run "find #{asset_paths} -exec touch -t #{stamp} {} ';'; true", :env => { "TZ" => "UTC" }
    end
  end

I'll follow up when/if Jamis accepts the patch. Hopefully it can make it into version 2.4!

comments powered by Disqus