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:
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.
Pingback: How to handle replicaset fail over with MongoEngine – Code Kolev
Pingback: How to handle Auto Reconnect in MongoEngine? – program faq