class I18n::Backend::Http

Constants

ALLOWED_STATS
FAILED_GET
STATS_NAMESPACE
VERSION

Public Class Methods

new(options) click to toggle source
# File lib/i18n/backend/http.rb, line 17
def initialize(options)
  @options = {
    http_open_timeout: 1,
    http_read_timeout: 1,
    http_open_retries: 0,
    http_read_retries: 0,
    polling_interval: 10*60,
    cache: nil,
    poll: true,
    exception_handler: -> (e) { $stderr.puts e },
    memory_cache_size: 10,
  }.merge(options)

  @http_client = EtagHttpClient.new(@options)
  @translations = LRUCache.new(@options[:memory_cache_size])
  start_polling if @options[:poll]
end

Public Instance Methods

available_locales() click to toggle source
# File lib/i18n/backend/http.rb, line 35
def available_locales
  @translations.keys.map(&:to_sym).select { |l| l != :i18n }
end
stop_polling() click to toggle source
# File lib/i18n/backend/http.rb, line 39
def stop_polling
  @stop_polling = true
end

Protected Instance Methods

cache_key(locale) click to toggle source
# File lib/i18n/backend/http.rb, line 106
def cache_key(locale)
  "i18n/backend/http/translations/#{locale}/v2"
end
download_translations(locale, etag:) click to toggle source
# File lib/i18n/backend/http.rb, line 110
def download_translations(locale, etag:)
  download_path = path(locale)

  with_retry do
    result, new_etag = @http_client.download(download_path, etag: etag)
    [parse_response(result), new_etag] if result
  end
rescue => e
  record(:download_fail, tags: ["exception:#{e.class}", "path:#{download_path}"])
  @options.fetch(:exception_handler).call(e)
  [self.class::FAILED_GET, nil]
end
fetch_and_update_cached_translations(locale, old_etag, update:) click to toggle source
# File lib/i18n/backend/http.rb, line 63
def fetch_and_update_cached_translations(locale, old_etag, update:)
  if cache = @options.fetch(:cache)
    key = cache_key(locale)
    interval = @options.fetch(:polling_interval)
    now = Time.now # capture time before we do slow work to stay on schedule
    old_value, old_etag, expires_at = cache.read(key) # assumes the cache is more recent then our local storage

    if old_value && (!update || expires_at > now || !updater?(cache, key, interval))
      return [old_value, old_etag]
    end

    new_value, new_etag = download_translations(locale, etag: old_etag)
    new_expires_at = now + interval
    cache.write(key, [new_value, new_etag, new_expires_at])
    [new_value, new_etag]
  else
    download_translations(locale, etag: old_etag)
  end
end
lookup(locale, key, scope = [], options = {}) click to toggle source
# File lib/i18n/backend/http.rb, line 54
def lookup(locale, key, scope = [], options = {})
  key = ::I18n.normalize_keys(locale, key, scope, options[:separator])[1..-1].join('.')
  lookup_key translations(locale), key
end
lookup_key(translations, key) click to toggle source

hook for extension with other resolution method

# File lib/i18n/backend/http.rb, line 132
def lookup_key(translations, key)
  translations[key]
end
parse_response(body) click to toggle source
# File lib/i18n/backend/http.rb, line 123
def parse_response(body)
  raise "implement parse_response"
end
path(locale) click to toggle source
# File lib/i18n/backend/http.rb, line 127
def path(locale)
  raise "implement path"
end
record(event, options = {}) click to toggle source
# File lib/i18n/backend/http.rb, line 152
def record(event, options = {})
  return unless statsd = @options[:statsd_client]
  raise "Unknown statsd event type to record" unless ALLOWED_STATS.include?(event)
  statsd.increment("#{STATS_NAMESPACE}.#{event}", tags: options[:tags])
end
start_polling() click to toggle source
# File lib/i18n/backend/http.rb, line 45
def start_polling
  Thread.new do
    until @stop_polling
      sleep(@options.fetch(:polling_interval))
      update_caches
    end
  end
end
translations(locale) click to toggle source
# File lib/i18n/backend/http.rb, line 59
def translations(locale)
  (@translations[locale] ||= fetch_and_update_cached_translations(locale, nil, update: false)).first
end
update_caches() click to toggle source

when download fails we keep our old caches since they are most likely better then nothing

# File lib/i18n/backend/http.rb, line 96
def update_caches
  @translations.keys.each do |locale|
    _, old_etag = @translations[locale]
    result = fetch_and_update_cached_translations(locale, old_etag, update: true)
    if result && result.first != self.class::FAILED_GET
      @translations[locale] = result
    end
  end
end
updater?(cache, key, interval) click to toggle source

sync with the cache who is going to update the cache this overlaps with the expiration interval, so worst case we will get 2x the interval if all servers are in sync and check updater at the same time

# File lib/i18n/backend/http.rb, line 86
def updater?(cache, key, interval)
  cache.write(
    "#{key}-lock",
    true,
    expires_in: interval,
    unless_exist: true
  )
end
with_retry() { || ... } click to toggle source
# File lib/i18n/backend/http.rb, line 136
def with_retry
  open_tries ||= 0
  read_tries ||= 0
  yield
rescue Faraday::ConnectionFailed => e
  raise unless e.instance_variable_get(:@wrapped_exception).is_a?(Net::OpenTimeout)
  raise if (open_tries += 1) > @options[:http_open_retries]
  record :open_retry
  retry
rescue Faraday::TimeoutError => e
  raise unless e.instance_variable_get(:@wrapped_exception).is_a?(Net::ReadTimeout)
  raise if (read_tries += 1) > @options[:http_read_retries]
  record :read_retry
  retry
end