NoSQL Zone is brought to you in partnership with:

Daniel Doubrovkine (aka dB.) is one of the tallest engineers at Art.sy. He founded and exited a successful Swiss start-up in the 90s, worked for Microsoft Corp. in Redmond, specializing in security and authentication, dabbled in large scale social networking and ran a big team that developed an expensive Enterprise product in NYC. After turning open-source cheerleader a few years ago in the worlds of C++, Java and .NET, he converted himself to Ruby and has been slowly unlearning everything he learned in the last 15 years of software practice. Daniel has posted 46 posts at DZone. You can read more from them at their website. View Full User Profile

Being Lazy: Using the Ruby driver to connect to MongoDB

01.06.2013
| 1850 views |
  • submit to reddit

Curator's Note: The content of this post was originally written back in 2011 - please feel free to suggest constructive updates by commenting below.

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.

 

Published at DZone with permission of its author, Daniel Doubrovkine. (source)

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)