多文件 Python 项目,一个文件一段时间后无法连接到 SQL 服务器

Multi-file Python project, one file unable to connect to SQL Server after a while

我有一个多文件 Python 项目,其中许多文件都连接到 Azure SQL 数据库。该项目运行良好,但由于某种原因,其中一个文件在应用程序 运行 一段时间后无法连接到数据库,我看不出原因;特别是当其他连接尝试正常时。

所有连接(所有文件)的连接字符串定义如下:

SQLServer = os.getenv('SQL_SERVER')
SQLDatabase = os.getenv('SQL_DATABASE')
SQLLogin = os.getenv('SQL_LOGIN')
SQLPassword = os.getenv('SQL_PASSWORD')

SQLConnString = 'Driver={ODBC Driver 17 for SQL Server};Server=' + SQLServer + ';Database=' + SQLDatabase + ';UID='+ SQLLogin +';PWD=' + SQLPassword

sqlConn = pyodbc.connect(SQLConnString,timeout=20)

当错误发生时,我正在调用的函数如下:

def iscaptain(guild,user):
    userRoles = user.roles
    roleParam = ""
    for role in userRoles:
        roleParam = roleParam + "," + str(role.id)
    cursor = sqlConn.cursor()
    roleParam = roleParam[1:]
    cursor.execute('EXEC corgi.GetUserAccess ?, ?;',guild.id,roleParam)
    for row in cursor:
        if row[1] == "Team Captain":
            cursor.close()
            return True
    cursor.close()
    return False

错误特别发生在cursor.execute。我目前收到错误

pyodbc.OperationalError: ('08S01', '[08S01] [Microsoft][ODBC Driver 17 for SQL Server]TCP Provider: Error code 0x68 (104) (SQLExecDirectW)')

以前我没有在有问题的特定文件的连接中超时,我确实得到了一个不同的错误:

Communication link failure

抱歉,我没有完整的之前的错误。

其他连接,在同一项目的其他文件中,工作正常,所以问题是不是网络问题;如果是 none 连接就可以了。该问题仅发生在一个文件中,所有连接尝试均失败。

谷歌搜索最新的错误并没有让我走得太远。例如,有一个 Github issue that gets nowhere, and this question 与其他文件的连接作品无关。

另请注意,这会在一段时间后发生;我真的不知道 这段时间有多长,但肯定是几个小时。重新启动项目也解决了这个问题;上述功能将正常工作。但这并不是真正的解决方案,我不能一直临时重启应用程序。

错误也是立即发生的;好像 Python/PyODBC 不是 尝试 连接。进入 cursor.execute 后立即产生错误;这不像当您超时时要等待几秒钟或更长时间才能发生超时。

我在这里不知所措。为什么文件(只有那个文件)以后无法再连接了?数据库也没有锁,所以我没有挂起的事务;尽管我希望再次出现超时错误,因为该过程将无法锁定数据。

另请注意,如果我手动执行该过程,在 sqlcmd/SSMS/ADS 中,数据也会正常返回,因此该过程可以正常工作。而且,如果我重新启动该应用程序,它将可以正常运行数小时。


编辑:我尝试了下面 Sabik 的回答,但是,不幸的是,这只破坏了应用程序。他们提供的解决方案在函数 validate_conn 上有参数 self,因此调用 validate_conn() 失败,因为我没有这个“self”的参数。他们说的方法,只是 validate_conn 没有做任何事情;它不调用该函数(这是我所期望的)。删除参数和对 self 的引用也破坏了应用程序,声明 sqlConn 没有声明,即使它声明了;请看下图,您可以清楚地看到 sqlConn 有一个值: 然而在那一行之后我立即收到以下错误:

UnboundLocalError: local variable 'sqlConn' referenced before assignment

所以他们的代码似乎有问题,但我不知道什么

评论中讨论的一种可能性是数据库端有30分钟的空闲超时,在这种情况下,一种解决方案是记录连接打开的时间,如果超过则重新连接25分钟。

这将是这样一种方法:

def validate_conn(self):
    if self.sqlConn is None or datetime.datetime.now() > self.conn_expiry:
        try:
            self.sqlConn.close()
        except:  # pylint: disable=broad-except
            # suppress all exceptions; we're in any case about to reconnect,
            # which will either resolve the situation or raise its own error
            pass

        self.sqlConn = pyodbc.connect(...)
        self.conn_expiry = datetime.datetime.now() + datetime.timedelta(minutes=25)

(如果sqlConn是全局的,请适当调整。)

在每个使用sqlConn的函数的开头,先调用validate_conn,然后自由使用连接

注意:这是抑制所有异常是合理的罕见情况之一;在任何情况下,我们都将重新连接到数据库,这要么会圆满解决问题,要么会引发自己的错误。

编辑: 如果 sqlConn 是全局变量,则需要在函数中这样声明:

def validate_conn():
    global sqlConn, conn_expiry
    if sqlConn is None or datetime.datetime.now() > conn_expiry:
        try:
            sqlConn.close()
        except:  # pylint: disable=broad-except
            # suppress all exceptions; we're in any case about to reconnect,
            # which will either resolve the situation or raise its own error
            pass

        sqlConn = pyodbc.connect(...)
        conn_expiry = datetime.datetime.now() + datetime.timedelta(minutes=25)

作为不相关的样式注释,编写函数的更短方法是使用 (a) with 语句和 (b) any 运算符,如下所示:

    with sqlConn.cursor() as cursor:
        roleParam = roleParam[1:]
        cursor.execute('EXEC corgi.GetUserAccess ?, ?;', guild.id, roleParam)
        return any(row[1] == "Team Captain" for row in cursor)

with 语句的优点是无论代码如何退出,游标都保证关闭,即使出现意外异常或以后的修改添加了更多分支。

尽管 Sabik 的解决方案对我不起作用,但答案确实将我推向了正确的方向以找到解决方案。具体来说,就是使用 with 子句。

我没有像我被告知的那样拥有持久的连接,而是现在将这些连接更改为短暂的连接,我用 with 打开,然后还将光标更改为with 还有。因此,对于 iscaptain 函数,我现在的代码如下所示:

def iscaptain(guild,user):
    userRoles = user.roles
    roleParam = ""
    for role in userRoles:
        roleParam = roleParam + "," + str(role.id)
    #sqlConn = pyodbc.connect(SQLConnString,timeout=20)
    with pyodbc.connect(SQLConnString,timeout=20) as sqlConn:
        with sqlConn.cursor() as cursor:
            roleParam = roleParam[1:]
            cursor.execute('EXEC corgi.GetUserAccess ?, ?;', guild.id, roleParam)
            return any(row[1] == "Team Captain" for row in cursor)
    return False

似乎 Azure 在一段时间后关闭了连接,因此当尝试重新使用连接时连接失败。然而,由于两台主机都在 Azure 中,但服务器 运行 Python 应用程序和 SQL 数据库,我很乐意根据需要重新连接,因为速度应该不是一个大问题;当然在过去 48 小时的测试中还没有。

确实意味着我有很多代码需要重构,但为了稳定性,这是必须的。