From: Mike Bayer Date: Mon, 11 Nov 2019 21:17:50 +0000 (-0500) Subject: Detect PyMySQL connection was killed X-Git-Tag: rel_1_4_0b1~627^2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=31a66b5add39b9193975f9995ce5cd9235179f00;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git Detect PyMySQL connection was killed Added "Connection was killed" message interpreted from the base pymysql.Error class in order to detect closed connection, based on reports that this message is arriving via a pymysql.InternalError() object which indicates pymysql is not handling it correctly. Change-Id: If6bbe0eb5993e1996c0c5de752eebaf7446cf93e References: https://github.com/PyMySQL/PyMySQL/issues/816 Fixes: #4945 --- diff --git a/doc/build/changelog/unreleased_13/4945.rst b/doc/build/changelog/unreleased_13/4945.rst new file mode 100644 index 0000000000..f94efca20b --- /dev/null +++ b/doc/build/changelog/unreleased_13/4945.rst @@ -0,0 +1,8 @@ +.. change:: + :tags: bug, mysql + :tickets: 4945 + + Added "Connection was killed" message interpreted from the base + pymysql.Error class in order to detect closed connection, based on reports + that this message is arriving via a pymysql.InternalError() object which + indicates pymysql is not handling it correctly. diff --git a/lib/sqlalchemy/dialects/mysql/pymysql.py b/lib/sqlalchemy/dialects/mysql/pymysql.py index 0b812cbb03..62de1529f2 100644 --- a/lib/sqlalchemy/dialects/mysql/pymysql.py +++ b/lib/sqlalchemy/dialects/mysql/pymysql.py @@ -67,7 +67,10 @@ class MySQLDialect_pymysql(MySQLDialect_mysqldb): ): return True elif isinstance(e, self.dbapi.Error): - return "Already closed" in str(e) + str_e = str(e).lower() + return ( + "already closed" in str_e or "connection was killed" in str_e + ) else: return False diff --git a/test/dialect/mysql/test_dialect.py b/test/dialect/mysql/test_dialect.py index 26c7d38b8c..aac1f64892 100644 --- a/test/dialect/mysql/test_dialect.py +++ b/test/dialect/mysql/test_dialect.py @@ -16,6 +16,7 @@ from sqlalchemy.testing import engines from sqlalchemy.testing import eq_ from sqlalchemy.testing import expect_warnings from sqlalchemy.testing import fixtures +from sqlalchemy.testing import mock from ...engine import test_execute @@ -23,6 +24,33 @@ class DialectTest(fixtures.TestBase): __backend__ = True __only_on__ = "mysql" + @testing.combinations( + (None, "cONnection was kILLEd", "InternalError", "pymysql", True), + (None, "cONnection aLREady closed", "InternalError", "pymysql", True), + (None, "something broke", "InternalError", "pymysql", False), + (2006, "foo", "OperationalError", "mysqldb", True), + (2006, "foo", "OperationalError", "pymysql", True), + (2007, "foo", "OperationalError", "mysqldb", False), + (2007, "foo", "OperationalError", "pymysql", False), + ) + def test_is_disconnect( + self, arg0, message, exc_cls_name, dialect_name, is_disconnect + ): + class Error(Exception): + pass + + dbapi = mock.Mock() + dbapi.Error = Error + dbapi.ProgrammingError = type("ProgrammingError", (Error,), {}) + dbapi.OperationalError = type("OperationalError", (Error,), {}) + dbapi.InterfaceError = type("InterfaceError", (Error,), {}) + dbapi.InternalError = type("InternalError", (Error,), {}) + + dialect = getattr(mysql, dialect_name).dialect(dbapi=dbapi) + + error = getattr(dbapi, exc_cls_name)(arg0, message) + eq_(dialect.is_disconnect(error, None, None), is_disconnect) + def test_ssl_arguments_mysqldb(self): from sqlalchemy.dialects.mysql import mysqldb