SELECT 被 OS 信号中断后 Psycopg2 连接不可用

Psycopg2 connection unusable after SELECTs interrupted by OS signal

问题

我正在处理一个长运行宁 python 进程,该进程执行大量数据库访问(主要是读取,偶尔写入)。有时可能需要在进程完成之前终止进程(例如,通过使用 kill 命令),当发生这种情况时,我想在数据库中记录一个值,表明特定的 运行 已被取消。 (我还将发生的事件记录到日志文件中;我希望在这两个地方都有信息。)

我发现如果在数据库连接处于活动状态时中断进程,连接将变得不可用;具体来说,如果我尝试以任何方式使用它,它会挂起进程。

最小工作示例

实际的应用程序相当庞大和复杂,但这个片段可靠地重现了问题。

数据库中的tabletest有两列,id(连续)和message(文本)。我用一行预先填充了它,所以下面的 UPDATE 语句会有一些变化。

import psycopg2
import sys
import signal


pg_host = 'localhost'
pg_user = 'redacted'
pg_password = 'redacted'
pg_database = 'test_db'


def write_message(msg):
    print "Writing: " + msg
    cur.execute("UPDATE test SET message = %s WHERE id = 1", (msg,))
    conn.commit()


def signal_handler(signal, frame):
    write_message('Interrupt!')
    sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)


if __name__ == '__main__':
    conn = psycopg2.connect(host=pg_host, user=pg_user, password=pg_password, database=pg_database)
    cur = conn.cursor()

    write_message("Starting")
    for i in xrange(10000):
        # I press ^C somewhere in here
        cur.execute("SELECT * FROM test")
        cur.fetchall()
    write_message("Finishing")

当我 运行 这个脚本没有中断时,它按预期完成。也就是说,数据库中的行更新为 "Starting" 然后 "Finishing".

如果我在注释指示的循环中按 ctrl-C,python 将无限期挂起。它不再响应键盘输入,必须从其他地方终止该进程。查看我的 postgresql 日志,数据库服务器从未收到带有 "Interrupted!" 的 UPDATE 语句。

如果我在 signal_handler() 的开头添加调试断点,我可以看到在该点对数据库连接执行几乎所有操作都会导致相同的挂起。尝试 execute 一个 SELECT、发出一个 conn.rollback()conn.commit()conn.close()conn.reset() 都会导致挂起。执行 conn.cancel() 不会导致挂起,但不会改善情况;随后使用该连接仍然会导致挂起。如果我从 write_message() 中删除数据库访问权限,那么脚本可以在中断时正常退出,因此挂起肯定与数据库连接有关。

另外值得注意的是:如果我更改脚本以便中断数据库 activity 以外的其他内容,它会按预期工作,将 "Interrupted!" 记录到数据库。例如,如果我用一个简单的 sleep(10) 替换 for i in xrange(10000) 循环并中断它,它就可以正常工作。因此,问题似乎与在执行数据库访问时用信号中断 psycopg2 特别相关,然后 尝试使用连接。

问题

有没有什么办法可以挽救现有的 psycopg2 连接,并在这种中断后用它来更新数据库?

如果不是,是否至少有一种方法可以干净地终止它,以便如果某些后续代码尝试使用它,它不会导致挂起?

最后,这是某种预期的行为,还是应该报告的错误?对我来说,在这种中断之后连接可能处于不良状态是有道理的,但理想情况下它会抛出一个异常来指示问题而不是挂起。

解决方法

与此同时,我发现如果我在中断后创建与 psycopg2.connect() 的全新连接并且注意不要访问旧连接,我仍然可以从中断的进程更新数据库。这可能是我现在要做的,但感觉不整洁。

环境

我为此在 psycopg2 github 上提交了 issue 并收到了开发人员的有用回复。总结:

  • 信号处理程序中现有连接的行为是 OS 依赖的,可能无法可靠地使用旧连接;创建一个新的是推荐的解决方案。
  • 使用 psycopg2.extensions.set_wait_callback(psycopg2.extras.wait_select) 通过使从信号处理程序中调用的 execute() 语句抛出异常而不是挂起来稍微改善这种情况(至少在我的环境中)。然而,用连接做其他事情(例如 reset())仍然让我挂起,所以最终最好还是在信号处理程序中创建一个新连接而不是试图挽救现有的连接。