Handling MongoDB AutoReconnect-exceptions in Python using a proxy

When using MongoDB in a production environment you will almost always want to set up a replica set to get better persistance and read-scaling. In a replica set you have one primary and one or more secondaries. Writes are always routed to the primary so if something should happen to the primary it becomes impossible to write to the database. When that happens a new primary is elected automatically, (if possible).

During failover and election of a new primary MongoDB raises a AutoReconnect-exception in response to any operations on the primary to signal that the operation failed. Your code therefore needs to be prepared to handle this exception. Often the correct thing to do is to wait for a little while and try the operation again, for example:

import time
import pymongo
db = pymongo.MongoReplicaSetClient(replicaSet='blog_rs').blogs

for i in range(5):
  try:
    db.posts.insert(post)
    break
  except pymongo.errors.AutoReconnect:
    time.sleep(pow(2, i))

This gets a bit annoying if you need to repeat the try-except for every line of code that calls MongoDB so a standard way is to put it in a decorator:

def safe_mongocall(call):
  def _safe_mongocall(*args, **kwargs):
    for i in xrange(5):
      try:
        return call(*args, **kwargs)
      except pymongo.AutoReconnect:
        time.sleep(pow(2, i))
    print 'Error: Failed operation!'
  return _safe_mongocall

You would then need to decorate all functions that calls MongoDB:

@safe_mongocall
def insert_blog_post(post):
  db.posts.insert(post)

But another way to do it that might be viewed as cleaner is to create a proxy around the connection to MongoDB. In that way, you could move all handling of AutoReconnects to this proxy and not have to care about catching the exception in the code.

Lets start with creating a class that can encapsulate any MongoDB-method and handle AutoReconnect-exceptions transparently using the decorator:

class Executable:
  def __init__(self, method):
    self.method = method

  @safe_mongocall
  def __call__(self, *args, **kwargs):
    return self.method(*args, **kwargs)

The Executable-class overrides the magic method __call__ that is called whenever an instance of the class is called, for example like this:

safe_post_insert = Executable(db.posts.insert)
safe_post_insert(post)

This will by itself not help us much since we would need to create safe inserts, updates, etc for every collection we want to use. The next step is therefore to create a proxy class that contains a MongoDB-connection and encapsulates all executable methods automatically.

We start by defining which of MongoDBs methods that should be wrapped in by the proxy class. We want to wrap all methods in pymongo, pymongo.Connection and pymongo.collection.Collection that do not start with “_”.

EXECUTABLE_MONGO_METHODS = set([typ for typ in dir(pymongo.collection.Collection) if not typ.startswith('_')])
EXECUTABLE_MONGO_METHODS.update(set([typ for typ in dir(pymongo.Connection) if not typ.startswith('_')]))
EXECUTABLE_MONGO_METHODS.update(set([typ for typ in dir(pymongo) if not typ.startswith('_')]))

And now for the MongoProxy-class:

class MongoProxy:
    """ Proxy for MongoDB connection.
    Methods that are executable, i.e find, insert etc, get wrapped in an
    Executable-instance that handles AutoReconnect-exceptions transparently.

    """
    def __init__(self, conn):
        """ conn is an ordinary MongoDB-connection.

        """
        self.conn = conn

    def __getitem__(self, key):
        """ Create and return proxy around the method in the connection
        named "key".

        """
        return MongoProxy(getattr(self.conn, key))

    def __getattr__(self, key):
        """ If key is the name of an executable method in the    MongoDB connection, for instance find or insert, wrap this method in the Executable-class. 
        Else call __getitem__(key).

        """
        if key in EXECUTABLE_MONGO_METHODS:
            return Executable(getattr(self.conn, key))
        return self[key]

    def __call__(self, *args, **kwargs):
        return self.conn(*args, **kwargs)

The MongoProxy-class is instantiated with a MongoDB-connection object that is saved in self.conn. So to create a safe connection to MongoDB we would do like this:

safe_conn = MongoProxy(pymongo.ReplicaSetConnection(replicaSet='blogs')

This safe_conn can then be used in the exact same way that you use the ordinary MongoDB-connection with the added benefit of not having to deal with AutoReconnects.

Lets take a closer look at what happens when we do an insert using our new safe connection:

safe_conn.blogs.posts.insert(post)

First the attribute blogs is accessed which causes a call to __getattr__. Since blogs is not found in the set EXECUTABLE_MONGO_METHODS, the call is sent to __getitem__ which returns a new proxy around the internal MongoDB-connections blogs-attribute. This is then repeated also for posts. We then get to the call to insert, this attribute is found in EXECUTABLE_MONGO_METHODS so instead of returning another proxy we finally wrap the call to insert in the Executable-class which performs the actual insert.

You should also override the methods __dir__ and __repr__ to make the proxy more transparent:

def __dir__(self):
    return dir(self.conn)

def __repr__(self):
    return self.conn.__repr__()

The complete source code can be found here.

  • Samus_uy

    hi there, I’m trying this out and it looks good, I was thinking about creating a pull request for MongoEngine but maybe you should instead.

    the PyMongo guys don’t seem to like the idea: https://jira.mongodb.org/browse/PYTHON-197 but the MongoEngine folk seem happy about it: https://groups.google.com/forum/?fromgroups#!topic/mongoengine-users/CfxPimmfTRI

    cheers!

    • arngarden

      Hi, thanks for your input. You’re welcome to do a pull request for MongoEngine if you think it would be suitable!

  • manuel

    Hi there, thanks for that great proxy class. This is very useful to me.

    I use an own application wide logger, so this class run into a problem while i wanted it to use my application logger. I change the following block a bit and now it works:

    [..]
    if hasattr(item, ‘__call__’):
    return MongoProxy(item, logger=self.logger)

    [..]

    basically I added the logger to the MongoProxy call.

    Maybe I misunderstand the concept, for it looks better to me now.

  • David Shimon

    Hi,
    Thanks for the post!
    What is the right way to handle “AutoReconnect” while iterating over mongo cursor?
    Do I have to re-generate the cursor?

    Thanks!

  • Anu

    Hi,
    I have one issue here.
    In my application I have replica set with only master and no slaves. If I restart mongodb, the application is not connecting with mongo until we restart the apllication. But I want that to happen.
    Please help me anyone.

    • arngarden

      I’m not sure myself, but could be that the exception that is generated when it looses connection is not a AutoReconnect but of some other type. Only AutoReconnect is handled automatically in the code.

  • Pingback: How to handle replicaset fail over with MongoEngine – Code Kolev