mirror of
https://github.com/element-hq/synapse.git
synced 2026-08-15 04:50:25 +00:00
`RustConnectionPool` runs synchronous Synapse transaction functions off the reactor thread against the native Rust connection pool. Each `run_with_connection(func, *args)` call runs, on a dedicated Twisted thread pool, `func(conn, *args)` with a `Connection` checked out of the Rust pool, and returns a Deferred that fires on the reactor with the result (or an errback if it raised). It builds on `defer_to_threadpool`, so log contexts are preserved across the hop, and returns the connection to the pool afterwards (clean → reused, mid-transaction/broken → discarded, per the shim's disposal rules). The Rust pool sizes 1:1 with the thread count, and `PyConnectionPool.close()` (new) closes all idle connections so the owner can drop the server connections deterministically. The bridge is lifecycle-owner-managed (start/close) and takes no clock, keeping it trivially testable; slotting it into `DatabasePool.make_pool` — which also needs engine-level support for the shim connection (in_transaction, is_connection_closed, autocommit/isolation, reconnect) — is left to a follow-up. Tested against a live Postgres over the real reactor (Synapse's in-memory test reactor mocks the DB thread pool out, so a plain Twisted trial TestCase is used): result/return-value, arg forwarding, exception-to-errback, connection reuse across calls, and concurrent checkouts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W3G4M92AmwSSZCbmtMJU3d
148 lines
5.2 KiB
Python
148 lines
5.2 KiB
Python
# This file is licensed under the Affero General Public License (AGPL) version 3.
|
|
#
|
|
# Copyright (C) 2026 Element Creations Ltd
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Affero General Public License as
|
|
# published by the Free Software Foundation, either version 3 of the
|
|
# License, or (at your option) any later version.
|
|
#
|
|
# See the GNU Affero General Public License for more details:
|
|
# <https://www.gnu.org/licenses/agpl-3.0.html>.
|
|
|
|
"""Tests for the Rust-backed Twisted connection pool adapter
|
|
(:mod:`synapse.storage.rust_pool`).
|
|
|
|
These drive real worker threads talking to a real Postgres over the real
|
|
reactor, so they use a plain Twisted trial ``TestCase`` (Synapse's in-memory
|
|
test reactor deliberately mocks the database thread pool out) and are skipped
|
|
unless the suite is configured to run against Postgres.
|
|
"""
|
|
|
|
from typing import TYPE_CHECKING, Any, cast
|
|
|
|
# The reactor the trial runner spins up; real, so threads and callFromThread work.
|
|
from twisted.internet import reactor as _reactor
|
|
from twisted.internet.defer import gatherResults, inlineCallbacks
|
|
from twisted.trial import unittest as trial_unittest
|
|
|
|
from synapse.storage.rust_pool import RustConnectionPool
|
|
|
|
from tests.unittest import skip_unless
|
|
from tests.utils import (
|
|
POSTGRES_BASE_DB,
|
|
POSTGRES_HOST,
|
|
POSTGRES_PASSWORD,
|
|
POSTGRES_PORT,
|
|
POSTGRES_USER,
|
|
USE_POSTGRES_FOR_TESTS,
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
from synapse.types import ISynapseReactor
|
|
|
|
# `twisted.internet.reactor` is a module-level singleton that is the reactor
|
|
# object; narrow it for the type checker.
|
|
reactor = cast("ISynapseReactor", _reactor)
|
|
|
|
|
|
def _build_dsn() -> str:
|
|
"""Build a libpq keyword/value connection string from the test config."""
|
|
|
|
parts = [f"dbname={POSTGRES_BASE_DB}"]
|
|
if POSTGRES_USER is not None:
|
|
parts.append(f"user={POSTGRES_USER}")
|
|
if POSTGRES_HOST is not None:
|
|
parts.append(f"host={POSTGRES_HOST}")
|
|
if POSTGRES_PORT is not None:
|
|
parts.append(f"port={POSTGRES_PORT}")
|
|
if POSTGRES_PASSWORD is not None:
|
|
parts.append(f"password={POSTGRES_PASSWORD}")
|
|
return " ".join(parts)
|
|
|
|
|
|
@skip_unless(
|
|
bool(USE_POSTGRES_FOR_TESTS), "requires a Postgres server (set SYNAPSE_POSTGRES)"
|
|
)
|
|
class RustConnectionPoolTestCase(trial_unittest.TestCase):
|
|
"""The execution bridge: run blocking DB functions off the reactor thread."""
|
|
|
|
def setUp(self) -> None:
|
|
self.pool = RustConnectionPool(
|
|
reactor, _build_dsn(), name="test-rust-db", threads=4
|
|
)
|
|
self.pool.start()
|
|
self.addCleanup(self.pool.close)
|
|
|
|
@inlineCallbacks
|
|
def test_runs_function_against_a_live_connection(self) -> Any:
|
|
# The function gets a usable connection: it can open a cursor, run a
|
|
# query, commit, and its return value comes back through the Deferred.
|
|
def txn(conn: Any) -> Any:
|
|
cursor = conn.cursor()
|
|
cursor.execute("SELECT 42::int")
|
|
row = cursor.fetch_one()
|
|
conn.commit()
|
|
return row
|
|
|
|
result = yield self.pool.run_with_connection(txn)
|
|
self.assertEqual(result, (42,))
|
|
|
|
@inlineCallbacks
|
|
def test_forwards_args_and_kwargs(self) -> Any:
|
|
def txn(conn: Any, a: int, b: int, c: int = 0) -> int:
|
|
return a + b + c
|
|
|
|
result = yield self.pool.run_with_connection(txn, 1, 2, c=3)
|
|
self.assertEqual(result, 6)
|
|
|
|
@inlineCallbacks
|
|
def test_exception_propagates_as_errback(self) -> Any:
|
|
class MarkerError(Exception):
|
|
pass
|
|
|
|
def txn(conn: Any) -> None:
|
|
raise MarkerError("boom")
|
|
|
|
# The failure crosses the thread boundary and surfaces as an errback.
|
|
failure = yield self.assertFailure(
|
|
self.pool.run_with_connection(txn), MarkerError
|
|
)
|
|
self.assertEqual(str(failure), "boom")
|
|
|
|
@inlineCallbacks
|
|
def test_connection_is_reusable_across_calls(self) -> Any:
|
|
# Connections are returned to the pool after each call, so a second call
|
|
# (which may reuse the same underlying connection) works and sees a
|
|
# clean session rather than a leftover transaction.
|
|
def one(conn: Any) -> Any:
|
|
cursor = conn.cursor()
|
|
cursor.execute("SELECT 1::int")
|
|
row = cursor.fetch_one()
|
|
conn.commit()
|
|
return row
|
|
|
|
self.assertEqual((yield self.pool.run_with_connection(one)), (1,))
|
|
self.assertEqual((yield self.pool.run_with_connection(one)), (1,))
|
|
|
|
@inlineCallbacks
|
|
def test_concurrent_calls_are_serviced(self) -> Any:
|
|
# Fire more calls than we have threads/connections at once; the pool and
|
|
# thread pool should service them all and return the right answers.
|
|
def txn(conn: Any, n: int) -> Any:
|
|
cursor = conn.cursor()
|
|
cursor.execute("SELECT $1::int", [n])
|
|
row = cursor.fetch_one()
|
|
conn.commit()
|
|
return row
|
|
|
|
results = yield gatherResults(
|
|
[self.pool.run_with_connection(txn, n) for n in range(10)]
|
|
)
|
|
self.assertEqual(results, [(n,) for n in range(10)])
|
|
|
|
def test_run_when_not_running_raises(self) -> None:
|
|
self.pool.close()
|
|
with self.assertRaises(RuntimeError):
|
|
self.pool.run_with_connection(lambda conn: None)
|