views:

545

answers:

5

I have a python script which is querying a MySQL server on a shared linux host. For some reason, queries to MySQL often return a "server has gone away" error:

_mysql_exceptions.OperationalError: (2006, 'MySQL server has gone away')

If you try the query again immediately afterwards, it usually succeeds. So, I'd like to know if there's a sensible way in python to try to execute a query, and if it fails, to try again, up to a fixed number of tries. Probably I'd want it to try 5 times before giving up altogether.

Here's the kind of code I have:

conn = MySQLdb.connect(host, user, password, database)
cursor = conn.cursor()

try:
    cursor.execute(query)
    rows = cursor.fetchall()
    for row in rows:
        # do something with the data
except MySQLdb.Error, e:
    print "MySQL Error %d: %s" % (e.args[0], e.args[1])

Clearly I could do it by having another attempt in the except clause, but that's incredibly ugly, and I have a feeling there must be a decent way to achieve this.

Thanks,

Ben

+8  A: 

How about:

conn = MySQLdb.connect(host, user, password, database)
cursor = conn.cursor()
attempts = 0

while attempts < 3:
    try:
        cursor.execute(query)
        rows = cursor.fetchall()
        for row in rows:
            # do something with the data
        break
    except MySQLdb.Error, e:
        attempts += 1
        print "MySQL Error %d: %s" % (e.args[0], e.args[1])
Dana
Or `for attempt_number in range(3)`
cdleary
Well, I kinda like mine because it makes it explicit that the attempts are only increased in the event of an exception.
Dana
Yeah, I guess I'm more paranoid about infinite `while` loops creeping in than most people.
cdleary
-1: Don't like break. Like "while not done and attempts < 3:" better.
S.Lott
I like the break, but not the while. This more like C-ish than pythonic. for i in range is better imho.
hasen j
A: 

I'd refactor it like so:

def callee(cursor):
    cursor.execute(query)
    rows = cursor.fetchall()
    for row in rows:
        # do something with the data

def caller(attempt_count=3, wait_interval=20):
    """:param wait_interval: In seconds."""
    conn = MySQLdb.connect(host, user, password, database)
    cursor = conn.cursor()
    for attempt_number in range(attempt_count):
        try:
            callee(cursor)
        except MySQLdb.Error, e:
            logging.warn("MySQL Error %d: %s", e.args[0], e.args[1])
            time.sleep(wait_interval)
        else:
            break

Factoring out the callee function seems to break up the functionality so that it's easy to see the business logic without getting bogged down in the retry code.

cdleary
-1: else and break... icky. Prefer a clearer "while not done and count != attempt_count" than break.
S.Lott
Really? I thought it made more sense this way -- if the exception doesn't occur, break out of the loop. I may be overly afraid of infinite while loops.
cdleary
+14  A: 

Building on Dana's answer, you might want to do this as a decorator:

def retry(howmany):
    def tryIt(func):
        def f():
            attempts = 0
            while attempts < howmany:
                try:
                    return func()
                except:
                    attempts += 1
        return f
    return tryIt

Then...

@retry(5)
the_db_func()

Enhanced version that uses the decorator module

import decorator, time

def retry(howmany, *exception_types, **kwargs):
    timeout = kwargs.get('timeout', 0.0) # seconds
    @decorator.decorator
    def tryIt(func, *fargs, **fkwargs):
        for _ in xrange(howmany):
            try: return func(*fargs, **fkwargs)
            except exception_types or Exception:
                if timeout is not None: time.sleep(timeout)
    return tryIt

Then...

@retry(5, MySQLdb.Error, timeout=0.5)
the_db_func()

To install the decorator module:

$ easy_install decorator
dwc
The decorator should probably take an exception class as well, so you don't have to use a bare except; i.e. @retry(5, MySQLdb.Error)
cdleary
Good call, cdleary
dwc
Nifty! I never think to use decorators :P
Dana
-1: if it works, it retries anyway.
S.Lott
That should be "return func() in the try block, not just "func()".
Robert Rossney
Bah! Thanks for the heads up.
dwc
Did you actually try running this? It doesn't work. The problem is that the func() call in the tryIt function gets executed *as soon as you decorate* the function, and *not* when you actually call the decorated function. You need another nested function.
Steve Losh
I get ImportError: No module named decorator in 2.6 and 3.0
recursive
J.F.Sebastian has edited (as in completely rewritten) the above code snippet. Doesn't work here on 2.6.1 either. You might want to look in the edits, start with a previous version and then fix things as seen in comments.
dwc
I'm sorry It doesn't occur to me that `decorator` is not in stdlib. http://pypi.python.org/pypi/decorator I will add version without it.
J.F. Sebastian
I've added fixed original @dwc's version.
J.F. Sebastian
+2  A: 
conn = MySQLdb.connect(host, user, password, database)
cursor = conn.cursor()

for i in range(3):
    try:
        cursor.execute(query)
        rows = cursor.fetchall()
        for row in rows:
            # do something with the data
        break
    except MySQLdb.Error, e:
        print "MySQL Error %d: %s" % (e.args[0], e.args[1])
webjunkie
A: 

Like S.Lott, I like a flag to check if we're done:

conn = MySQLdb.connect(host, user, password, database)
cursor = conn.cursor()

success = False
attempts = 0

while attempts < 3 and not success:
    try:
        cursor.execute(query)
        rows = cursor.fetchall()
        for row in rows:
            # do something with the data
        success = True 
    except MySQLdb.Error, e:
        print "MySQL Error %d: %s" % (e.args[0], e.args[1])
        attempts += 1
Kiv