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']) # rubocop:disable Security/Eval -- Trusted legacy configuration.
puts "[#{Time.now}] connecting to #{config['MONGOHQ_DATABASE']} on #{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.
