Daniel Doubrovkine bio photo

Daniel Doubrovkine

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

Email Twitter LinkedIn Github

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?)
db.collection("widgets").drop()

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)
db_connection("widgets").drop()

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.

production:
  config:
    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"

staging:
  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?
    config_env["config"]
  end

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

  # 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"])
    db_connection
    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?)
    db_connection
    else
    raise "missing MONGOHQ_URL or MONGOHQ_HOST_LIST for #{env} environment"
    end
  end

end

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