From 855f5a36e7c8e7a8ce3f62d6ef8c9ae3e073ae3d Mon Sep 17 00:00:00 2001 From: baldychristophe Date: Fri, 18 Nov 2022 18:26:59 +0100 Subject: [PATCH] Fixed #29062 -- Prevented possibility of database lock when using LiveServerTestCase with in-memory SQLite database. Thanks Chris Jerdonek for the implementation idea. --- django/db/backends/sqlite3/features.py | 10 ++++++++ django/test/testcases.py | 4 +++- tests/servers/tests.py | 32 +++++++++++++++++++++++--- 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/django/db/backends/sqlite3/features.py b/django/db/backends/sqlite3/features.py index 1f7c6c012f..ae347c30f5 100644 --- a/django/db/backends/sqlite3/features.py +++ b/django/db/backends/sqlite3/features.py @@ -111,6 +111,16 @@ class DatabaseFeatures(BaseDatabaseFeatures): }, } ) + else: + skips.update( + { + "Only connections to in-memory SQLite databases are passed to the " + "server thread.": { + "servers.tests.LiveServerInMemoryDatabaseLockTest." + "test_in_memory_database_lock", + }, + } + ) return skips @cached_property diff --git a/django/test/testcases.py b/django/test/testcases.py index a9349d82f1..c78c2300a7 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -1778,7 +1778,9 @@ class LiveServerThread(threading.Thread): try: # Create the handler for serving static and media files handler = self.static_handler(_MediaFilesHandler(WSGIHandler())) - self.httpd = self._create_server() + self.httpd = self._create_server( + connections_override=self.connections_override, + ) # If binding to port zero, assign the port allocated by the OS. if self.port == 0: self.port = self.httpd.server_address[1] diff --git a/tests/servers/tests.py b/tests/servers/tests.py index 91f766926b..66f0af1604 100644 --- a/tests/servers/tests.py +++ b/tests/servers/tests.py @@ -5,6 +5,7 @@ import errno import os import socket import threading +import unittest from http.client import HTTPConnection from urllib.error import HTTPError from urllib.parse import urlencode @@ -12,7 +13,7 @@ from urllib.request import urlopen from django.conf import settings from django.core.servers.basehttp import ThreadedWSGIServer, WSGIServer -from django.db import DEFAULT_DB_ALIAS, connections +from django.db import DEFAULT_DB_ALIAS, connection, connections from django.test import LiveServerTestCase, override_settings from django.test.testcases import LiveServerThread, QuietWSGIRequestHandler @@ -107,8 +108,33 @@ class LiveServerTestCloseConnectionTest(LiveServerBase): self.assertIsNone(conn.connection) +@unittest.skipUnless(connection.vendor == "sqlite", "SQLite specific test.") +class LiveServerInMemoryDatabaseLockTest(LiveServerBase): + def test_in_memory_database_lock(self): + """ + With a threaded LiveServer and an in-memory database, an error can + occur when 2 requests reach the server and try to lock the database + at the same time, if the requests do not share the same database + connection. + """ + conn = self.server_thread.connections_override[DEFAULT_DB_ALIAS] + # Open a connection to the database. + conn.connect() + # Create a transaction to lock the database. + cursor = conn.cursor() + cursor.execute("BEGIN IMMEDIATE TRANSACTION") + try: + with self.urlopen("/create_model_instance/") as f: + self.assertEqual(f.status, 200) + except HTTPError: + self.fail("Unexpected error due to a database lock.") + finally: + # Release the transaction. + cursor.execute("ROLLBACK") + + class FailingLiveServerThread(LiveServerThread): - def _create_server(self): + def _create_server(self, connections_override=None): raise RuntimeError("Error creating server.") @@ -150,7 +176,7 @@ class LiveServerAddress(LiveServerBase): class LiveServerSingleThread(LiveServerThread): - def _create_server(self): + def _create_server(self, connections_override=None): return WSGIServer( (self.host, self.port), QuietWSGIRequestHandler, allow_reuse_address=False )