Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit 91c3f32

Browse files
authored
Speed up SQLite unit test CI (#15334)
Tests now take 40% of the time.
1 parent ae4acda commit 91c3f32

File tree

4 files changed

+53
-4
lines changed

4 files changed

+53
-4
lines changed

changelog.d/15334.misc

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Speed up unit tests when using SQLite3.

synapse/storage/engines/sqlite.py

+16-1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@ def __init__(self, database_config: Mapping[str, Any]):
3434
":memory:",
3535
)
3636

37+
# A connection to a database that has already been prepared, to use as a
38+
# base for an in-memory connection. This is used during unit tests to
39+
# speed up setting up the DB.
40+
self._prepped_conn: Optional[sqlite3.Connection] = database_config.get(
41+
"_TEST_PREPPED_CONN"
42+
)
43+
3744
if platform.python_implementation() == "PyPy":
3845
# pypy's sqlite3 module doesn't handle bytearrays, convert them
3946
# back to bytes.
@@ -84,7 +91,15 @@ def on_new_connection(self, db_conn: "LoggingDatabaseConnection") -> None:
8491
# In memory databases need to be rebuilt each time. Ideally we'd
8592
# reuse the same connection as we do when starting up, but that
8693
# would involve using adbapi before we have started the reactor.
87-
prepare_database(db_conn, self, config=None)
94+
#
95+
# If we have a `prepped_conn` we can use that to initialise the DB,
96+
# otherwise we need to call `prepare_database`.
97+
if self._prepped_conn is not None:
98+
# Initialise the new DB from the pre-prepared DB.
99+
assert isinstance(db_conn.conn, sqlite3.Connection)
100+
self._prepped_conn.backup(db_conn.conn)
101+
else:
102+
prepare_database(db_conn, self, config=None)
88103

89104
db_conn.create_function("rank", 1, _rank)
90105
db_conn.execute("PRAGMA foreign_keys = ON;")

tests/server.py

+23
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import logging
1717
import os
1818
import os.path
19+
import sqlite3
1920
import time
2021
import uuid
2122
import warnings
@@ -79,7 +80,9 @@
7980
from synapse.logging.context import ContextResourceUsage
8081
from synapse.server import HomeServer
8182
from synapse.storage import DataStore
83+
from synapse.storage.database import LoggingDatabaseConnection
8284
from synapse.storage.engines import PostgresEngine, create_engine
85+
from synapse.storage.prepare_database import prepare_database
8386
from synapse.types import ISynapseReactor, JsonDict
8487
from synapse.util import Clock
8588

@@ -104,6 +107,10 @@
104107
# the type of thing that can be passed into `make_request` in the headers list
105108
CustomHeaderType = Tuple[Union[str, bytes], Union[str, bytes]]
106109

110+
# A pre-prepared SQLite DB that is used as a template when creating new SQLite
111+
# DB each test run. This dramatically speeds up test set up when using SQLite.
112+
PREPPED_SQLITE_DB_CONN: Optional[LoggingDatabaseConnection] = None
113+
107114

108115
class TimedOutException(Exception):
109116
"""
@@ -899,6 +906,22 @@ def setup_test_homeserver(
899906
"args": {"database": test_db_location, "cp_min": 1, "cp_max": 1},
900907
}
901908

909+
# Check if we have set up a DB that we can use as a template.
910+
global PREPPED_SQLITE_DB_CONN
911+
if PREPPED_SQLITE_DB_CONN is None:
912+
temp_engine = create_engine(database_config)
913+
PREPPED_SQLITE_DB_CONN = LoggingDatabaseConnection(
914+
sqlite3.connect(":memory:"), temp_engine, "PREPPED_CONN"
915+
)
916+
917+
database = DatabaseConnectionConfig("master", database_config)
918+
config.database.databases = [database]
919+
prepare_database(
920+
PREPPED_SQLITE_DB_CONN, create_engine(database_config), config
921+
)
922+
923+
database_config["_TEST_PREPPED_CONN"] = PREPPED_SQLITE_DB_CONN
924+
902925
if "db_txn_limit" in kwargs:
903926
database_config["txn_limit"] = kwargs["db_txn_limit"]
904927

tests/unittest.py

+13-3
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,9 @@ def setUp(orig: Callable[[], R]) -> R:
146146
% (current_context(),)
147147
)
148148

149+
# Disable GC for duration of test. See below for why.
150+
gc.disable()
151+
149152
old_level = logging.getLogger().level
150153
if level is not None and old_level != level:
151154

@@ -163,12 +166,19 @@ def tearDown(orig: Callable[[], R]) -> R:
163166

164167
return orig()
165168

169+
# We want to force a GC to workaround problems with deferreds leaking
170+
# logcontexts when they are GCed (see the logcontext docs).
171+
#
172+
# The easiest way to do this would be to do a full GC after each test
173+
# run, but that is very expensive. Instead, we disable GC (above) for
174+
# the duration of the test so that we only need to run a gen-0 GC, which
175+
# is a lot quicker.
176+
166177
@around(self)
167178
def tearDown(orig: Callable[[], R]) -> R:
168179
ret = orig()
169-
# force a GC to workaround problems with deferreds leaking logcontexts when
170-
# they are GCed (see the logcontext docs)
171-
gc.collect()
180+
gc.collect(0)
181+
gc.enable()
172182
set_current_context(SENTINEL_CONTEXT)
173183

174184
return ret

0 commit comments

Comments
 (0)