python MySQLDB查询超时

时间:2009-10-01 23:51:06

标签: python timeout mysql

我正在尝试在python MySQLDB中强制执行查询的时间限制。我有一种情况,我无法控制查询,但需要确保它们不会超过设定的时间限制。我已经尝试使用signal.SIGALRM中断执行的调用,但这似乎不起作用。信号被发送,但在执行完成后才会被捕获。

我写了一个测试用例来证明这种行为:

#!/usr/local/bin/python2.6

import time
import signal

from somewhere import get_dbc

class Timeout(Exception):
    """ Time Exceded """

def _alarm_handler(*args):
    raise Timeout

dbc = get_dbc()

signal.signal(signal.SIGALRM, _alarm_handler)
signal.alarm(1)

try:
    print "START:  ", time.time()
    dbc.execute("SELECT SLEEP(10)")
except Timeout:
    print "TIMEOUT!", time.time()'

“SELECT SLEEP(10)”模拟慢速查询,但我确实看到了与实际慢速查询相同的行为。

结果:

START:   1254440686.69
TIMEOUT! 1254440696.69

正如你所看到的,它已经睡了10秒然后我得到了Timeout Exception。

问题:

  1. 为什么在执行完成之后我才收到信号?
  2. 是否有另一种可靠的方法来限制查询执行时间?

6 个答案:

答案 0 :(得分:8)

@nosklo's twisted-based solution优雅且可行,但如果你想避免依赖扭曲,那么任务仍然可行,例如:

import multiprocessing

def query_with_timeout(dbc, timeout, query, *a, **k):
  conn1, conn2 = multiprocessing.Pipe(False)
  subproc = multiprocessing.Process(target=do_query,
                                    args=(dbc, query, conn2)+a, 
                                    kwargs=k)
  subproc.start()
  subproc.join(timeout)
  if conn1.poll():
    return conn1.recv()
  subproc.terminate()
  raise TimeoutError("Query %r ran for >%r" % (query, timeout))

def do_query(dbc, query, conn, *a, **k):
  cu = dbc.cursor()
  cu.execute(query, *a, **k)
  return cu.fetchall()

答案 1 :(得分:2)

  

我已经尝试使用signal.SIGALRM来中断执行的调用,但这似乎不起作用。信号被发送,但在执行完成后才会被捕获。

mysql库在内部处理中断的系统调用,因此在API调用完成之前你不会看到SIGALRM的副作用(没有杀死当前的线程或进程)

您可以尝试修补MySQL-Python并使用MYSQL_OPT_READ_TIMEOUT选项(在mysql 5.0.25中添加)

答案 2 :(得分:1)

  

为什么在执行完成之后我才收到信号?

查询是通过C函数执行的,该函数阻止Python VM执行直到它返回。

  

是否有另一种可靠的方法来限制查询执行时间?

这是(IMO)一个非常难看的解决方案,但 工作。您可以在单独的过程中运行查询(通过fork()multiprocessing module)。在主进程中运行闹钟计时器,当您收到它时,请向子进程发送SIGINTSIGKILL。如果您使用multiprocessing,则可以使用Process.terminate()方法。

答案 3 :(得分:1)

使用adbapi。它允许您异步执行db调用。

from twisted.internet import reactor
from twisted.enterprise import adbapi

def bogusQuery():
    return dbpool.runQuery("SELECT SLEEP(10)")

def printResult(l):
    # function that would be called if it didn't time out
    for item in l:
        print item

def handle_timeout():
    # function that will be called when it timeout
    reactor.stop()

dbpool = adbapi.ConnectionPool("MySQLdb", user="me", password="myself", host="localhost", database="async")
bogusQuery().addCallback(printResult)
reactor.callLater(4, handle_timeout)
reactor.run()

答案 4 :(得分:1)

通用备注

我最近遇到了同样的问题,我必须遇到几个条件:

  • 解决方案必须是线程安全的
  • 同一台机器的多个数据库连接可能同时处于活动状态,终止一个连接/查询
  • 应用程序包含与许多不同数据库的连接 - 每个数据库主机的可移植处理程序

我们有以下课程布局(遗憾的是我无法发布真实来源):

class AbstractModel: pass 
class FirstDatabaseModel(AbstractModel): pass # Connection to one DB host
class SecondDatabaseModel(AbstractModel): pass # Connection to one DB host

为每个模型创建了几个线程。


解决方案 Python 3.2

在我们的应用程序中一个模型=一个数据库。所以我为每个模型创建了“服务连接”(因此我们可以在并行连接中执行KILL)。因此,如果创建了FirstDatabaseModel的一个实例,则创建了2个数据库连接;如果创建了5个实例,则只使用了6个连接:

class AbstractModel:
    _service_connection = None # Formal declaration

    def __init__(self):
        ''' Somehow load config and create connection
        '''
        self.config = # ...
        self.connection = MySQLFromConfig(self.config)
        self._init_service_connection()

        # Get connection ID (pseudocode)
        self.connection_id = self.connection.FetchOneCol('SELECT CONNECTION_ID()') 

    def _init_service_connection(self):
        ''' Initialize one singleton connection for model
        '''
        cls = type(self)
        if cls._service_connection is not None:
            return

        cls._service_connection = MySQLFromConfig(self.config)

现在我们需要一个杀手:

def _kill_connection(self):
    # Add your own mysql data escaping
    sql = 'KILL CONNECTION {}'.format(self.connection_id)

    # Do your own connection check and renewal
    type(self)._service_connection.execute(sql)

注意:connection.execute =创建游标,执行,关闭游标。

使用threading.Lock确保杀手线程安全:

def _init_service_connection(self):
    ''' Initialize one singleton connection for model
    '''
    cls = type(self)
    if cls._service_connection is not None:
        return

    cls._service_connection = MySQLFromConfig(self.config)
    cls._service_connection_lock = threading.Lock()

def _kill_connection(self):
    # Add your own mysql data escaping
    sql = 'KILL CONNECTION {}'.format(self.connection_id)
    cls = type(self)

    # Do your own connection check and renewal
    try:
        cls._service_connection_lock.acquire()    
        cls._service_connection.execute(sql)
    finally:
        cls._service_connection_lock.release()

最后使用threading.Timer添加定时执行方法:

def timed_query(self, sql, timeout=5):
    kill_query_timer = threading.Timer(timeout, self._kill_connection)
    kill_query_timer.start()

    try:
        self.connection.long_query() 
    finally:
        kill_query_timer.cancel()

答案 5 :(得分:-1)

  

为什么在执行完成之后我才收到信号?

等待网络I / O的进程处于不可中断状态(UNIX事物,与Python或MySQL无关)。它在系统调用结束后得到信号(可能是EINTR错误代码,虽然我不确定。)

  

是否有另一种可靠的方法来限制查询执行时间?

我认为它通常由像mkill这样的外部工具完成,它监视MySQL长时间运行的查询并杀死它们。