114

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.

Ben
  • 66,838
  • 37
  • 84
  • 108
  • 2
    That's a good point. I'd probably put a sleep in for a few seconds. I don't know what's wrong with the MySQL installation on the server, but it does seem that it fails one second, and the next it works. – Ben Feb 19 '09 at 23:03
  • 3
    @Yuval A: It is a common task. I suspect it is even builtin in Erlang. – jfs Feb 19 '09 at 23:10
  • 1
    Just to mention that maybe nothing is wrong, Mysql has a **wait_timeout** variable to configure mysql to drop inactive connections. – andy May 12 '16 at 09:43
  • @MadPhysicist I pointed the duplicate closure the other way, because this version of the question seems clearly better to me by every metric I can think of. – Karl Knechtel Jul 30 '22 at 00:41

10 Answers10

131

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
  • 32,083
  • 17
  • 62
  • 73
  • 27
    Or `for attempt_number in range(3)` – cdleary Feb 19 '09 at 23:05
  • 10
    Well, I kinda like mine because it makes it explicit that the attempts are only increased in the event of an exception. – Dana Feb 19 '09 at 23:06
  • 2
    Yeah, I guess I'm more paranoid about infinite `while` loops creeping in than most people. – cdleary Feb 19 '09 at 23:14
  • 7
    -1: Don't like break. Like "while not done and attempts < 3:" better. – S.Lott Feb 20 '09 at 00:26
  • 5
    I like the break, but not the while. This more like C-ish than pythonic. for i in range is better imho. – hasen Feb 25 '09 at 11:54
  • 2
    I like [Kiv's](http://stackoverflow.com/a/574924/3182836) use of `while and not success:`--with `success = True` at the end of the try clause--as an alternative to `break` as a minor improvement on this answer. – Michelle Welcks Feb 21 '14 at 20:05
  • how do I try to re-connect? I mean what about the error was happen on the MySQLdb.connect ? (oh, sorry, I'm so stupid. I think I found the way ...) – zx1986 Jan 22 '16 at 06:56
  • using the try/except/else and putting the break inside the else block may be cleaner in this case – COCO Apr 27 '17 at 15:18
  • 2
    This is wrong and terrible. Won't throw any errors after 3rd attempt. – Henry Henrinson Apr 24 '20 at 10:33
83

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)
def 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)
def the_db_func():
    # [...]

To install the decorator module:

$ easy_install decorator
habnabit
  • 9,906
  • 3
  • 32
  • 26
dwc
  • 24,196
  • 7
  • 44
  • 55
  • 2
    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 Feb 19 '09 at 23:19
  • Nifty! I never think to use decorators :P – Dana Feb 19 '09 at 23:31
  • That should be "return func() in the try block, not just "func()". – Robert Rossney Feb 20 '09 at 01:59
  • Bah! Thanks for the heads up. – dwc Feb 20 '09 at 15:01
  • 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 Feb 20 '09 at 15:47
  • I get ImportError: No module named decorator in 2.6 and 3.0 – recursive Feb 22 '09 at 15:48
  • 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 Feb 22 '09 at 16:01
  • 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. – jfs Feb 25 '09 at 11:40
  • I've added fixed original @dwc's version. – jfs Feb 25 '09 at 11:51
  • timeout = kwargs.get('timeout', 0.0) # seconds /// I think it should be: /// timeout = kwargs.get('timeout', None) # seconds /// since later you do: /// if timeout is not None – theosp Feb 09 '12 at 14:36
  • Did that mean `def the_db_function()`? I can't execute it on python 2.7.1 even without using `decorator`... It gave me a `SyntaxError`... – Yi H. Nov 12 '12 at 01:45
  • would it be possible to give an explanation of whats going on in this answer? I don't really understand why there are so many def functions or whats really going on and I don't want to just copy and paste :) – wprins Nov 02 '16 at 17:23
16

UPDATE: there is a better maintained fork of the retrying library called tenacity, which supports more features and is in general more flexible.

The API changes slightly:

@retry(stop=stop_after_attempt(7))
def stop_after_7_attempts():
    print("Stopping after 7 attempts")

@retry(wait=wait_fixed(2))
def wait_2_s():
    print("Wait 2 second between retries")

@retry(wait=wait_exponential(multiplier=1, min=4, max=10))
def wait_exponential_1000():
    print("Wait 2^x * 1000 milliseconds between each retry,")
    print("up to 10 seconds, then 10 seconds afterwards")

Yes, there is the retrying library, which has a decorator that implements several kinds of retrying logic that you can combine:

Some examples:

@retry(stop_max_attempt_number=7)
def stop_after_7_attempts():
    print("Stopping after 7 attempts")

@retry(wait_fixed=2000)
def wait_2_s():
    print("Wait 2 second between retries")

@retry(wait_exponential_multiplier=1000, wait_exponential_max=10000)
def wait_exponential_1000():
    print("Wait 2^x * 1000 milliseconds between each retry,")
    print("up to 10 seconds, then 10 seconds afterwards")
Elias Dorneles
  • 22,556
  • 11
  • 85
  • 107
  • 2
    The retrying library has been superseded by the [tenacity library](https://github.com/jd/tenacity). – Seth Sep 09 '17 at 21:58
8
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
  • 6,891
  • 7
  • 46
  • 43
  • 1
    You could add an [else](http://stackoverflow.com/q/9979970/673991) at the bottom: `else: raise TooManyRetriesCustomException` – Bob Stein Aug 28 '15 at 22:23
6

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
  • 69,512
  • 53
  • 163
  • 191
  • -1: else and break... icky. Prefer a clearer "while not done and count != attempt_count" than break. – S.Lott Feb 20 '09 at 01:33
  • 1
    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 Feb 20 '09 at 07:10
  • 4
    +1: I hate flag variables when the language includes the code structures to do it for you. For bonus points, put an else on the for to deal with failing all the attempts. – xorsyst Oct 05 '11 at 15:06
6

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
  • 31,940
  • 6
  • 44
  • 59
1
def successful_transaction(transaction):
    try:
        transaction()
        return True
    except SQL...:
        return False

succeeded = any(successful_transaction(transaction)
                for transaction in repeat(transaction, 3))
Peter Wood
  • 23,859
  • 5
  • 60
  • 99
1

1.Definition:

def try_three_times(express):
    att = 0
    while att < 3:
        try: return express()
        except: att += 1
    else: return u"FAILED"

2.Usage:

try_three_times(lambda: do_some_function_or_express())

I use it for parse html context.

laffuste
  • 16,287
  • 8
  • 84
  • 91
1

You can use a for loop with an else clause for maximum effect:

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

for n in range(3):
    try:
        cursor.execute(query)
    except MySQLdb.Error, e:
        print "MySQL Error %d: %s" % (e.args[0], e.args[1])
    else:
        rows = cursor.fetchall()
        for row in rows:
            # do something with the data
        break
else:
    # All attempts failed, raise a real error or whatever

The key is to break out of the loop as soon as the query succeeds. The else clause will only be triggered if the loop completes without a break.

Mad Physicist
  • 107,652
  • 25
  • 181
  • 264
0

This is my generic solution:

class TryTimes(object):
    ''' A context-managed coroutine that returns True until a number of tries have been reached. '''

    def __init__(self, times):
        ''' times: Number of retries before failing. '''
        self.times = times
        self.count = 0

    def __next__(self):
        ''' A generator expression that counts up to times. '''
        while self.count < self.times:
            self.count += 1
        yield False

    def __call__(self, *args, **kwargs):
        ''' This allows "o() calls for "o = TryTimes(3)". '''
        return self.__next__().next()

    def __enter__(self):
        ''' Context manager entry, bound to t in "with TryTimes(3) as t" '''
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        ''' Context manager exit. '''
        return False # don't suppress exception

This allows code like the following:

with TryTimes(3) as t:
    while t():
        print "Your code to try several times"

Also possible:

t = TryTimes(3)
while t():
    print "Your code to try several times"

This can be improved by handling exceptions in a more intuitive way, I hope. Open to suggestions.