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.