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.