diff --git a/frappe/core/doctype/user/test_user.py b/frappe/core/doctype/user/test_user.py index 2d12508fb5..83f0aa6237 100644 --- a/frappe/core/doctype/user/test_user.py +++ b/frappe/core/doctype/user/test_user.py @@ -496,6 +496,7 @@ def test_user( user.append_roles(*roles) user.insert() yield user + commit and frappe.db.commit() finally: user.delete(force=True, ignore_permissions=True) commit and frappe.db.commit() diff --git a/frappe/database/mariadb/database.py b/frappe/database/mariadb/database.py index 9d675eb1aa..5cfe4a3fbf 100644 --- a/frappe/database/mariadb/database.py +++ b/frappe/database/mariadb/database.py @@ -25,7 +25,8 @@ class MariaDBExceptionUtil: @staticmethod def is_deadlocked(e: pymysql.Error) -> bool: - return e.args[0] == ER.LOCK_DEADLOCK + # Snapshot isolation is also treated as deadlock from User POV + return e.args[0] in (ER.LOCK_DEADLOCK, ER.CHECKREAD) @staticmethod def is_timedout(e: pymysql.Error) -> bool: diff --git a/frappe/database/mariadb/mysqlclient.py b/frappe/database/mariadb/mysqlclient.py index 513f826a99..5f16c74a24 100644 --- a/frappe/database/mariadb/mysqlclient.py +++ b/frappe/database/mariadb/mysqlclient.py @@ -28,7 +28,8 @@ class MariaDBExceptionUtil: @staticmethod def is_deadlocked(e: MySQLdb.Error) -> bool: - return e.args[0] == ER.LOCK_DEADLOCK + # Snapshot isolation is also treated as deadlock from User POV + return e.args[0] in (ER.LOCK_DEADLOCK, ER.CHECKREAD) @staticmethod def is_timedout(e: MySQLdb.Error) -> bool: diff --git a/frappe/integrations/doctype/connected_app/test_connected_app.py b/frappe/integrations/doctype/connected_app/test_connected_app.py index 2e427793f7..2079b3a521 100644 --- a/frappe/integrations/doctype/connected_app/test_connected_app.py +++ b/frappe/integrations/doctype/connected_app/test_connected_app.py @@ -137,6 +137,8 @@ class TestConnectedApp(IntegrationTestCase): if doc: doc.delete(force=True) + frappe.db.commit() # Avoid snapshot violation issues + delete_if_exists("token_cache") delete_if_exists("connected_app") diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py index 4199b2ed5c..4306b3c051 100644 --- a/frappe/model/delete_doc.py +++ b/frappe/model/delete_doc.py @@ -107,7 +107,7 @@ def delete_doc( # Lock the doc without waiting try: frappe.db.get_value(doctype, name, for_update=True, wait=False) - except frappe.QueryTimeoutError: + except (frappe.QueryTimeoutError, frappe.QueryDeadlockError): frappe.throw( _( "This document can not be deleted right now as it's being modified by another user. Please try again after some time." diff --git a/frappe/public/js/frappe/request.js b/frappe/public/js/frappe/request.js index 086ba131ae..43d442bdd5 100644 --- a/frappe/public/js/frappe/request.js +++ b/frappe/public/js/frappe/request.js @@ -251,7 +251,9 @@ frappe.request.call = function (opts) { frappe.msgprint({ title: __("Deadlock Occurred"), indicator: "red", - message: __("Server was too busy to process this request. Please try again."), + message: __( + "Server failed to process this request because of a concurrent conflicting request. Please try again." + ), }); }, }; diff --git a/frappe/tests/test_api.py b/frappe/tests/test_api.py index 7a478b80f8..aa40817bc4 100644 --- a/frappe/tests/test_api.py +++ b/frappe/tests/test_api.py @@ -130,6 +130,10 @@ class FrappeAPITestCase(IntegrationTestCase): def delete(self, path, **kwargs) -> TestResponse: return make_request(target=self.TEST_CLIENT.delete, args=(path,), kwargs=kwargs) + def tearDown(self) -> None: + frappe.db.rollback() + return super().tearDown() + class TestResourceAPI(FrappeAPITestCase): DOCTYPE = "ToDo" @@ -146,6 +150,7 @@ class TestResourceAPI(FrappeAPITestCase): @classmethod def tearDownClass(cls): + frappe.db.commit() for name in cls.GENERATED_DOCUMENTS: frappe.delete_doc_if_exists(cls.DOCTYPE, name) frappe.db.commit() diff --git a/frappe/tests/test_api_v2.py b/frappe/tests/test_api_v2.py index 6cee357a03..c5f66806b4 100644 --- a/frappe/tests/test_api_v2.py +++ b/frappe/tests/test_api_v2.py @@ -33,6 +33,7 @@ class TestResourceAPIV2(FrappeAPITestCase): @classmethod def tearDownClass(cls): + frappe.db.commit() for name in cls.GENERATED_DOCUMENTS: frappe.delete_doc_if_exists(cls.DOCTYPE, name) frappe.db.commit() diff --git a/frappe/tests/test_auth.py b/frappe/tests/test_auth.py index 016b46528c..5ce77a17f1 100644 --- a/frappe/tests/test_auth.py +++ b/frappe/tests/test_auth.py @@ -49,10 +49,12 @@ class TestAuth(IntegrationTestCase): @classmethod def tearDownClass(cls): + frappe.db.rollback() frappe.delete_doc("User", cls.test_user_email, force=True) frappe.local.request_ip = None frappe.form_dict.email = None frappe.local.response["http_status_code"] = None + frappe.db.commit() def set_system_settings(self, k, v): frappe.db.set_single_value("System Settings", k, v) diff --git a/frappe/tests/test_frappe_client.py b/frappe/tests/test_frappe_client.py index cc32404baf..a5e0a9c966 100644 --- a/frappe/tests/test_frappe_client.py +++ b/frappe/tests/test_frappe_client.py @@ -105,7 +105,9 @@ class TestFrappeClient(IntegrationTestCase): self.assertEqual( server.get_value("Website Settings", "title_prefix").get("title_prefix"), "test-prefix" ) + frappe.db.rollback() # Clear snapshot isolation frappe.db.set_single_value("Website Settings", "title_prefix", "") + frappe.db.commit() def test_update_doc(self): server = FrappeClient(get_url(), "Administrator", self.PASSWORD, verify=False)