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. Lets 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()
Lets 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.