Improving Autocomplete Cache Hit Rates by Looking up Parent Caches

When a user asks for all the words starting with “foobar” let’s check the cache for fooba,foob,foo,fo,f and see if any of these results already include a full set of words.
This saved us ~30% of our db queries and makes the ui more responsive.

# given a start word of foobar check caches for ["fooba", "foob", "foo", "fo", "f"]
# to see if any of the simpler queries already had a complete response
def cached_autocomplete(start)
  limit = 1000
  short = start.dup
  shorter = Array.new(start.size - 1){ short.chop!.dup }

  cached = Rails.cache.read_multi(*shorter.map { |s| "autocomplete-#{s}" })
  cached.each_value do |tags|
    if tags.size < limit # complete response / nothing is missing
      tags.select! { |t| t.start_with?(start) } # filter non-matches
      return tags
    end
  end

  Rails.cache.fetch("autocomplete-#{start}", expires_in: 15.minutes, race_condition_ttl: 1.minute) do
    ... hit the db ... limit(limit) ...
    ... prevent multiple cache refreshes with race_condition_ttl ...
  end
end

ActionMailer / Rails: No paths in my mails please

Always paying attention that mails only use urls is a bit annoying/dangerous and also means we cannot reuse partials and cannot use nice resource routes like `link_to user.name, user`

Make ActionMailer always use full urls:

# we only want urls in our emails, never paths
module OnlyAbsoluteUrls
  def url_for(*args)
    url = super
    if url.include?("://")
      url
    else
      "#{ActionMailer::Base.default_url_options.fetch(:host)}#{url}"
    end
  end
end

class ApplicationMailer < ActionMailer::Base
  helper OnlyAbsoluteUrls
end

and to make sure it works let’s verify in all mailer tests that we did not actually generated paths:

after { deliveries.map(&:body).map(&:to_s).join.wont_include '"/' }

Running karma js with rails asset pipeline / sprockets

# test/karma.conf.js
...
    basePath: '<%= Bundler.root %>',
...
    files: [
      '<%= resolve_asset('vis.js') %>',
      'app/assets/javascripts/app.js',
      'test/**/*_spec.js'
    ],

# Rakefile
namespace :test do
  task js: :environment do
    with_tmp_karma_config do |config|
      sh "./node_modules/karma/bin/karma start #{config} --single-run"
    end
  end

  private

  def with_tmp_karma_config
    Tempfile.open('karma.js') do |f|
      f.write ERB.new(File.read('test/karma.conf.js')).result(binding)
      f.flush
      yield f.path
    end
  end

  def resolve_asset(file)
    Rails.application.assets.find_asset(file).to_a.first.pathname.to_s
  end
end

Refile upload with jquery-ui and progress

Ran into a few gotschas while implementing this, so I wanted to share :)

– buttons need to be disabled via the button method
– e.originalEvent.detail holds progress information


$(document).on("upload:start", "form", function(e) {
  $(e.target).prev().prev('img').hide(); // hide old image we are replacing
  $(this).find("input[type=submit]").
    button({disabled: true}). // do not let user press submit until image is uploaded
    after('<img src="/images/loading.gif" />') // indicate we are waiting
});

$(document).on("upload:progress", "form", function(e) {
  // http://stackoverflow.com/questions/10420352/converting-file-size-in-bytes-to-human-readable
  function humanFileSize(bytes) {
    var thresh = 1024;
    if(bytes < thresh) return bytes + ' B';
    var units = ['kB','MB','GB','TB','PB','EB','ZB','YB'];
    var u = -1;
    do {
      bytes /= thresh;
      ++u;
    } while(bytes >= thresh);
    return bytes.toFixed(1)+' '+units[u];
  };

  var detail = e.originalEvent.detail;
  var percentage = Math.round((detail.loaded / detail.total) * 100);
  $(e.target).next().text(percentage + "% of " + humanFileSize(detail.total))
});

$(document).on("upload:complete", "form", function(e) {
  if(!$(this).find("input.uploading").length) {
    $(this).find("input[type=submit]").
      button({disabled: false}). // all images uploaded, user can submit the form
      next().remove(); // remove loading
  }
});