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

Speed up SQLite unit test CI #15334

Merged
merged 7 commits into from
Mar 30, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/15334.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Speed up unit tests when using SQLite3.
20 changes: 16 additions & 4 deletions synapse/storage/engines/sqlite.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ def __init__(self, database_config: Mapping[str, Any]):
":memory:",
)

# A connection to a database that has already been prepared, to use as a
# base for an in-memory connection. This is used during unit tests to
# speed up setting up the DB.
self._prepped_conn: Optional[sqlite3.Connection] = database_config.get(
"_TEST_PREPPED_CONN"
)

if platform.python_implementation() == "PyPy":
# pypy's sqlite3 module doesn't handle bytearrays, convert them
# back to bytes.
Expand Down Expand Up @@ -81,10 +88,15 @@ def on_new_connection(self, db_conn: "LoggingDatabaseConnection") -> None:
from synapse.storage.prepare_database import prepare_database

if self._is_in_memory:
# In memory databases need to be rebuilt each time. Ideally we'd
# reuse the same connection as we do when starting up, but that
# would involve using adbapi before we have started the reactor.
prepare_database(db_conn, self, config=None)
if self._prepped_conn is not None:
# Initialise the new DB from the pre-prepared DB.
assert isinstance(db_conn.conn, sqlite3.Connection)
self._prepped_conn.backup(db_conn.conn)
else:
# In memory databases need to be rebuilt each time. Ideally we'd
# reuse the same connection as we do when starting up, but that
# would involve using adbapi before we have started the reactor.
clokep marked this conversation as resolved.
Show resolved Hide resolved
prepare_database(db_conn, self, config=None)

db_conn.create_function("rank", 1, _rank)
db_conn.execute("PRAGMA foreign_keys = ON;")
Expand Down
23 changes: 23 additions & 0 deletions tests/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import logging
import os
import os.path
import sqlite3
import time
import uuid
import warnings
Expand Down Expand Up @@ -79,7 +80,9 @@
from synapse.logging.context import ContextResourceUsage
from synapse.server import HomeServer
from synapse.storage import DataStore
from synapse.storage.database import LoggingDatabaseConnection
from synapse.storage.engines import PostgresEngine, create_engine
from synapse.storage.prepare_database import prepare_database
from synapse.types import ISynapseReactor, JsonDict
from synapse.util import Clock

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

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


class TimedOutException(Exception):
"""
Expand Down Expand Up @@ -899,6 +906,22 @@ def setup_test_homeserver(
"args": {"database": test_db_location, "cp_min": 1, "cp_max": 1},
}

# Check if we have set up a DB that we can use as a template.
global PREPPED_SQlITE_DB_CONN
erikjohnston marked this conversation as resolved.
Show resolved Hide resolved
if PREPPED_SQlITE_DB_CONN is None:
temp_engine = create_engine(database_config)
PREPPED_SQlITE_DB_CONN = LoggingDatabaseConnection(
sqlite3.connect(":memory:"), temp_engine, "PREPPED_CONN"
)

database = DatabaseConnectionConfig("master", database_config)
config.database.databases = [database]
prepare_database(
PREPPED_SQlITE_DB_CONN, create_engine(database_config), config
)

database_config["_TEST_PREPPED_CONN"] = PREPPED_SQlITE_DB_CONN

if "db_txn_limit" in kwargs:
database_config["txn_limit"] = kwargs["db_txn_limit"]

Expand Down
16 changes: 13 additions & 3 deletions tests/unittest.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,9 @@ def setUp(orig: Callable[[], R]) -> R:
% (current_context(),)
)

# Disable GC for duration of test. See below for why.
gc.disable()

old_level = logging.getLogger().level
if level is not None and old_level != level:

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

return orig()

# We want to force a GC to workaround problems with deferreds leaking
# logcontexts when they are GCed (see the logcontext docs).
#
# The easiest way to do this would be to do a full GC after each test
# run, but that is very expensive. Instead, we disable GC (above) for
# the duration of the test so that we only need to run a gen-0 GC, which
# is a lot quicker.

@around(self)
def tearDown(orig: Callable[[], R]) -> R:
ret = orig()
# force a GC to workaround problems with deferreds leaking logcontexts when
# they are GCed (see the logcontext docs)
gc.collect()
gc.collect(0)
gc.enable()
set_current_context(SENTINEL_CONTEXT)

return ret
Expand Down