Daniel Doubrovkine bio photo

Daniel Doubrovkine

aka dB., CTO at artsy.net, fun at playplay.io, NYC

Email Twitter LinkedIn Github Strava

One of my rake tasks failed with an interesting error this morning.

uncaught exception: error: { "$err" : "not master and slaveok=false", "code" : 13435 }

Qu’est-ce que c’est?

The problem is that I am connecting to a slave in a replica set and trying to execute a write operation that must happen on a master node. That’s because I was lazy and was executing command-line update queries, such as this one. I was being lazy. Shame on me.

system "mongo #{db_host}:#{db_port}/#{db_name} -u #{db_user} -p#{db_password} --eval 'db.widgets.drop()'"

This is run in a Rake task. Let’s replace this with some Ruby code, the way it’s ought to be.

db = Mongo::Connection.new(db_host, db_port).db(db_name)
db.authenticate(db_user, db_password) unless (db.user.nil? || db.user.blank?)

It’s actually a lot cleaner, I am not sure why I was hung up on the command line thing. Unfortunately it doesn’t fix our problem. In a replica set we need to use a ReplSetConnection that will automatically load-balance requests and send writes to the master. It takes a list of hosts, something like

db_connection = Mongo::ReplSetConnection.new(db_host_list).db(db_name)
db_connection.authenticate(db_user, db_password)

Let’s try to write something usable in all of our rake tasks. What I have is a YML file with the Heroku MongoHQ configuration. The first environment is a replica set, while the second is a single MongoDB.

    MONGOHQ_URL:      "mongodb://heroku:password@replica.mongohq.com:12345/production-name"
    MONGOHQ_DATABASE:   "db-name"
    MONGOHQ_HOST_LIST:  "[['node0.replica.mongohq.com', 12345], ['node1.replica.mongohq.com', 12345]]"
    MONGOHQ_PASSWD:     "password"
    MONGOHQ_USER:     "heroku"

  config: &default
    MONGOHQ_URL:      "mongodb://heroku:password@small.mongohq.com:12345/staging-name"

We can write a basic connect method that picks up the right configuration, as a Rake task.

namespace :mongohq do

  def heroku_config(env = Rails.env)
    @@config ||= YAML.load_file(Rails.root.join("config/heroku.yml")).symbolize_keys
    config_env = @@config[env.to_sym]
    raise "missing '#{env}' section in config/heroku.yml" if config_env.nil?

  def parse_mongohq_url(url)
    uri = URI.parse(url)
    [uri, uri.path.gsub("/", "")]

  # connect to a MongoDB
  def mongohq_connect(env = Rails.env)
    config = heroku_config(env)
    if ! config["MONGOHQ_HOST_LIST"].blank?
    mongohq_host_list = eval(config["MONGOHQ_HOST_LIST"])
    puts "[#{Time.now}] connecting to #{config["MONGOHQ_DATABASE"]} on #{eval(config["MONGOHQ_HOST_LIST"])}"
    db_connection = Mongo::ReplSetConnection.new(\* mongohq_host_list).db(config["MONGOHQ_DATABASE"])
    db_connection.authenticate(config["MONGOHQ_USER"], config["MONGOHQ_PASSWD"])
    elsif ! config["MONGOHQ_URL"].blank?
    puts "[#{Time.now}] connecting to #{config["MONGOHQ_URL"]}"
    db, db_name = parse_mongohq_url(config["MONGOHQ_URL"])
    db_connection = Mongo::Connection.new(db.host, db.port).db(db_name)
    db_connection.authenticate(db.user, db.password) unless (db.user.nil? || db.user.blank?)
    raise "missing MONGOHQ_URL or MONGOHQ_HOST_LIST for #{env} environment"


Now, our rake tasks can call mongohq_connect(:production) or mongohq_connect(:staging) without having to worry about the kind of setup we have.