Copycopter's client: so fast

Joe Ferris

Copycopter

We recently launched our latest product to great critical acclaim. We’re happy to see people climbing aboard the Copycopter, and feedback has been largely positive.

However, one question we’ve had to field a few times is, “how will Copycopter affect the performance of my application?” The answer is a good one: it won’t. However, that answer may seem too good to be true to some folks, so let’s find out how Copycopter manages to stay out of your application’s way.

Integration

One of Copycopter’s nicer features is that the Ruby client is deeply integrated into the Rails stack the second you install it. This is made possible by the excellent Rails I18n API. We hook in our own I18n backend so that whenever Rails looks for a string, we use the text you’ve set up on Copycopter. This all happens in copycopter_client’s I18nBackend class. If you read quickly through that class, you’ll see that fetching or storing copy doesn’t make a request to the Copycopter server; it looks for content in the hash-like sync object.

def lookup(locale, key, scope = [], options = {})
  parts = I18n.normalize_keys(locale, key, scope, options[:separator])
  key_with_locale = parts.join('.')
  content = sync[key_with_locale] || super
  sync[key_with_locale] = "" if content.nil?
  content
end

Behind the scenes

The client’s performance is acheived by using a background thread. When your Rails application starts up, the client spins up a thread in the Sync class.

    until @stop
      sync
      logger.flush if logger.respond_to?(:flush)
      sleep(polling_delay)
    end

Every five minutes, the background thread synchronizes with the Copycopter server. It uses mutexes to make sure it isn’t updating copy while the application is using it. However, the mutex is only locked when already downloaded copy is being swapped in, so the main thread won’t be waiting for a lock to release.

def download
  client.download do |downloaded_blurbs|
    downloaded_blurbs.reject! { |key, value| value == "" }
    lock { @blurbs = downloaded_blurbs }
  end
rescue ConnectionError => error
  logger.error(error.message)
end

HTTP friendly

We also don’t want to waste any bandwidth or cycles by repeatedly downloading unchanged copy. The Client class is responsible for actually talking to the Copycopter server, and it speaks fluent HTTP.

def download
  connect do |http|
    request = Net::HTTP::Get.new(uri(download_resource))
    request['If-None-Match'] = @etag
    response = http.request(request)
    if check(response)
      log("Downloaded translations")
      yield JSON.parse(response.body)
    else
      log("No new translations")
    end
    @etag = response['ETag']
  end
end

Each running client tracks the latest ETag when it downloads copy, so most requests simply return a 304 Not Modified response without sending any copy data.

when Net::HTTPNotModified
  false

Developing

It should be mentioned that this passive behavior is only used in production environments. During development and on a staging server, we found a five minute delay between copy updates to be unacceptable. During development, we wrap each request using a little piece of Rack middleware:

def call(env)
  @sync.download
  response = @app.call(env)
  @sync.flush
  response
end

Although this could potentially add a slight delay to local requests, we found that the faster feedback was worth the tradeoff, and the smart HTTP handling, caching, and timeouts ensure that developing still feels snappy.

Putting it all together

By integrating with I18n, using a background thread, and using the HTTP protocol to our advantage, we achieve a number of performance and stability benefits:

  • Slow copy downloads don’t mean slow applications
  • Server errors don’t mean application errors
  • Lots of application traffic doesn’t mean many Copycopter requests
  • Up-to-date copy doesn’t cost much in terms of bandwidth
  • Rails uses the I18n stack, so Rails engines and plugins support Copycopter by default

If you haven’t given Copycopter a try yet, don’t let apprehensions about performance stop you. Check out the code, install the client, and see for yourself.

Get to the choppa!