mirror of
https://git.anonymousland.org/anonymousland/synapse.git
synced 2025-08-19 02:57:51 -04:00
Add database config class (#6513)
This encapsulates config for a given database and is the way to get new connections.
This commit is contained in:
parent
91ccfe9f37
commit
2284eb3a53
19 changed files with 286 additions and 208 deletions
|
@ -12,12 +12,43 @@
|
|||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
import logging
|
||||
import os
|
||||
from textwrap import indent
|
||||
from typing import List
|
||||
|
||||
import yaml
|
||||
|
||||
from ._base import Config
|
||||
from synapse.config._base import Config, ConfigError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DatabaseConnectionConfig:
|
||||
"""Contains the connection config for a particular database.
|
||||
|
||||
Args:
|
||||
name: A label for the database, used for logging.
|
||||
db_config: The config for a particular database, as per `database`
|
||||
section of main config. Has two fields: `name` for database
|
||||
module name, and `args` for the args to give to the database
|
||||
connector.
|
||||
data_stores: The list of data stores that should be provisioned on the
|
||||
database.
|
||||
"""
|
||||
|
||||
def __init__(self, name: str, db_config: dict, data_stores: List[str]):
|
||||
if db_config["name"] not in ("sqlite3", "psycopg2"):
|
||||
raise ConfigError("Unsupported database type %r" % (db_config["name"],))
|
||||
|
||||
if db_config["name"] == "sqlite3":
|
||||
db_config.setdefault("args", {}).update(
|
||||
{"cp_min": 1, "cp_max": 1, "check_same_thread": False}
|
||||
)
|
||||
|
||||
self.name = name
|
||||
self.config = db_config
|
||||
self.data_stores = data_stores
|
||||
|
||||
|
||||
class DatabaseConfig(Config):
|
||||
|
@ -26,20 +57,14 @@ class DatabaseConfig(Config):
|
|||
def read_config(self, config, **kwargs):
|
||||
self.event_cache_size = self.parse_size(config.get("event_cache_size", "10K"))
|
||||
|
||||
self.database_config = config.get("database")
|
||||
database_config = config.get("database")
|
||||
|
||||
if self.database_config is None:
|
||||
self.database_config = {"name": "sqlite3", "args": {}}
|
||||
if database_config is None:
|
||||
database_config = {"name": "sqlite3", "args": {}}
|
||||
|
||||
name = self.database_config.get("name", None)
|
||||
if name == "psycopg2":
|
||||
pass
|
||||
elif name == "sqlite3":
|
||||
self.database_config.setdefault("args", {}).update(
|
||||
{"cp_min": 1, "cp_max": 1, "check_same_thread": False}
|
||||
)
|
||||
else:
|
||||
raise RuntimeError("Unsupported database type '%s'" % (name,))
|
||||
self.databases = [
|
||||
DatabaseConnectionConfig("master", database_config, data_stores=["main"])
|
||||
]
|
||||
|
||||
self.set_databasepath(config.get("database_path"))
|
||||
|
||||
|
@ -76,11 +101,24 @@ class DatabaseConfig(Config):
|
|||
self.set_databasepath(args.database_path)
|
||||
|
||||
def set_databasepath(self, database_path):
|
||||
if database_path is None:
|
||||
return
|
||||
|
||||
if database_path != ":memory:":
|
||||
database_path = self.abspath(database_path)
|
||||
if self.database_config.get("name", None) == "sqlite3":
|
||||
if database_path is not None:
|
||||
self.database_config["args"]["database"] = database_path
|
||||
|
||||
# We only support setting a database path if we have a single sqlite3
|
||||
# database.
|
||||
if len(self.databases) != 1:
|
||||
raise ConfigError("Cannot specify 'database_path' with multiple databases")
|
||||
|
||||
database = self.get_single_database()
|
||||
if database.config["name"] != "sqlite3":
|
||||
# We don't raise here as we haven't done so before for this case.
|
||||
logger.warn("Ignoring 'database_path' for non-sqlite3 database")
|
||||
return
|
||||
|
||||
database.config["args"]["database"] = database_path
|
||||
|
||||
@staticmethod
|
||||
def add_arguments(parser):
|
||||
|
@ -91,3 +129,11 @@ class DatabaseConfig(Config):
|
|||
metavar="SQLITE_DATABASE_PATH",
|
||||
help="The path to a sqlite database to use.",
|
||||
)
|
||||
|
||||
def get_single_database(self) -> DatabaseConnectionConfig:
|
||||
"""Returns the database if there is only one, useful for e.g. tests
|
||||
"""
|
||||
if len(self.databases) != 1:
|
||||
raise Exception("More than one database exists")
|
||||
|
||||
return self.databases[0]
|
||||
|
|
|
@ -230,7 +230,7 @@ class PresenceHandler(object):
|
|||
is some spurious presence changes that will self-correct.
|
||||
"""
|
||||
# If the DB pool has already terminated, don't try updating
|
||||
if not self.hs.get_db_pool().running:
|
||||
if not self.store.database.is_running():
|
||||
return
|
||||
|
||||
logger.info(
|
||||
|
|
|
@ -25,7 +25,6 @@ import abc
|
|||
import logging
|
||||
import os
|
||||
|
||||
from twisted.enterprise import adbapi
|
||||
from twisted.mail.smtp import sendmail
|
||||
from twisted.web.client import BrowserLikePolicyForHTTPS
|
||||
|
||||
|
@ -98,7 +97,6 @@ from synapse.server_notices.worker_server_notices_sender import (
|
|||
)
|
||||
from synapse.state import StateHandler, StateResolutionHandler
|
||||
from synapse.storage import DataStores, Storage
|
||||
from synapse.storage.engines import create_engine
|
||||
from synapse.streams.events import EventSources
|
||||
from synapse.util import Clock
|
||||
from synapse.util.distributor import Distributor
|
||||
|
@ -134,7 +132,6 @@ class HomeServer(object):
|
|||
|
||||
DEPENDENCIES = [
|
||||
"http_client",
|
||||
"db_pool",
|
||||
"federation_client",
|
||||
"federation_server",
|
||||
"handlers",
|
||||
|
@ -233,12 +230,6 @@ class HomeServer(object):
|
|||
self.admin_redaction_ratelimiter = Ratelimiter()
|
||||
self.registration_ratelimiter = Ratelimiter()
|
||||
|
||||
self.database_engine = create_engine(config.database_config)
|
||||
config.database_config.setdefault("args", {})[
|
||||
"cp_openfun"
|
||||
] = self.database_engine.on_new_connection
|
||||
self.db_config = config.database_config
|
||||
|
||||
self.datastores = None
|
||||
|
||||
# Other kwargs are explicit dependencies
|
||||
|
@ -247,10 +238,8 @@ class HomeServer(object):
|
|||
|
||||
def setup(self):
|
||||
logger.info("Setting up.")
|
||||
with self.get_db_conn() as conn:
|
||||
self.datastores = DataStores(self.DATASTORE_CLASS, conn, self)
|
||||
conn.commit()
|
||||
self.start_time = int(self.get_clock().time())
|
||||
self.datastores = DataStores(self.DATASTORE_CLASS, self)
|
||||
logger.info("Finished setting up.")
|
||||
|
||||
def setup_master(self):
|
||||
|
@ -284,6 +273,9 @@ class HomeServer(object):
|
|||
def get_datastore(self):
|
||||
return self.datastores.main
|
||||
|
||||
def get_datastores(self):
|
||||
return self.datastores
|
||||
|
||||
def get_config(self):
|
||||
return self.config
|
||||
|
||||
|
@ -433,31 +425,6 @@ class HomeServer(object):
|
|||
)
|
||||
return MatrixFederationHttpClient(self, tls_client_options_factory)
|
||||
|
||||
def build_db_pool(self):
|
||||
name = self.db_config["name"]
|
||||
|
||||
return adbapi.ConnectionPool(
|
||||
name, cp_reactor=self.get_reactor(), **self.db_config.get("args", {})
|
||||
)
|
||||
|
||||
def get_db_conn(self, run_new_connection=True):
|
||||
"""Makes a new connection to the database, skipping the db pool
|
||||
|
||||
Returns:
|
||||
Connection: a connection object implementing the PEP-249 spec
|
||||
"""
|
||||
# Any param beginning with cp_ is a parameter for adbapi, and should
|
||||
# not be passed to the database engine.
|
||||
db_params = {
|
||||
k: v
|
||||
for k, v in self.db_config.get("args", {}).items()
|
||||
if not k.startswith("cp_")
|
||||
}
|
||||
db_conn = self.database_engine.module.connect(**db_params)
|
||||
if run_new_connection:
|
||||
self.database_engine.on_new_connection(db_conn)
|
||||
return db_conn
|
||||
|
||||
def build_media_repository_resource(self):
|
||||
# build the media repo resource. This indirects through the HomeServer
|
||||
# to ensure that we only have a single instance of
|
||||
|
|
|
@ -40,7 +40,7 @@ class SQLBaseStore(object):
|
|||
def __init__(self, database: Database, db_conn, hs):
|
||||
self.hs = hs
|
||||
self._clock = hs.get_clock()
|
||||
self.database_engine = hs.database_engine
|
||||
self.database_engine = database.engine
|
||||
self.db = database
|
||||
self.rand = random.SystemRandom()
|
||||
|
||||
|
|
|
@ -13,24 +13,50 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from synapse.storage.database import Database
|
||||
import logging
|
||||
|
||||
from synapse.storage.database import Database, make_conn
|
||||
from synapse.storage.engines import create_engine
|
||||
from synapse.storage.prepare_database import prepare_database
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DataStores(object):
|
||||
"""The various data stores.
|
||||
|
||||
These are low level interfaces to physical databases.
|
||||
|
||||
Attributes:
|
||||
main (DataStore)
|
||||
"""
|
||||
|
||||
def __init__(self, main_store_class, db_conn, hs):
|
||||
def __init__(self, main_store_class, hs):
|
||||
# Note we pass in the main store class here as workers use a different main
|
||||
# store.
|
||||
database = Database(hs)
|
||||
|
||||
# Check that db is correctly configured.
|
||||
database.engine.check_database(db_conn.cursor())
|
||||
self.databases = []
|
||||
|
||||
prepare_database(db_conn, database.engine, config=hs.config)
|
||||
for database_config in hs.config.database.databases:
|
||||
db_name = database_config.name
|
||||
engine = create_engine(database_config.config)
|
||||
|
||||
self.main = main_store_class(database, db_conn, hs)
|
||||
with make_conn(database_config, engine) as db_conn:
|
||||
logger.info("Preparing database %r...", db_name)
|
||||
|
||||
engine.check_database(db_conn.cursor())
|
||||
prepare_database(
|
||||
db_conn, engine, hs.config, data_stores=database_config.data_stores,
|
||||
)
|
||||
|
||||
database = Database(hs, database_config, engine)
|
||||
|
||||
if "main" in database_config.data_stores:
|
||||
logger.info("Starting 'main' data store")
|
||||
self.main = main_store_class(database, db_conn, hs)
|
||||
|
||||
db_conn.commit()
|
||||
|
||||
self.databases.append(database)
|
||||
|
||||
logger.info("Database %r prepared", db_name)
|
||||
|
|
|
@ -412,7 +412,7 @@ class ClientIpStore(ClientIpBackgroundUpdateStore):
|
|||
def _update_client_ips_batch(self):
|
||||
|
||||
# If the DB pool has already terminated, don't try updating
|
||||
if not self.hs.get_db_pool().running:
|
||||
if not self.db.is_running():
|
||||
return
|
||||
|
||||
to_update = self._batch_row_update
|
||||
|
|
|
@ -24,9 +24,11 @@ from six.moves import intern, range
|
|||
|
||||
from prometheus_client import Histogram
|
||||
|
||||
from twisted.enterprise import adbapi
|
||||
from twisted.internet import defer
|
||||
|
||||
from synapse.api.errors import StoreError
|
||||
from synapse.config.database import DatabaseConnectionConfig
|
||||
from synapse.logging.context import LoggingContext, make_deferred_yieldable
|
||||
from synapse.metrics.background_process_metrics import run_as_background_process
|
||||
from synapse.storage.background_updates import BackgroundUpdater
|
||||
|
@ -74,6 +76,37 @@ UNIQUE_INDEX_BACKGROUND_UPDATES = {
|
|||
}
|
||||
|
||||
|
||||
def make_pool(
|
||||
reactor, db_config: DatabaseConnectionConfig, engine
|
||||
) -> adbapi.ConnectionPool:
|
||||
"""Get the connection pool for the database.
|
||||
"""
|
||||
|
||||
return adbapi.ConnectionPool(
|
||||
db_config.config["name"],
|
||||
cp_reactor=reactor,
|
||||
cp_openfun=engine.on_new_connection,
|
||||
**db_config.config.get("args", {})
|
||||
)
|
||||
|
||||
|
||||
def make_conn(db_config: DatabaseConnectionConfig, engine):
|
||||
"""Make a new connection to the database and return it.
|
||||
|
||||
Returns:
|
||||
Connection
|
||||
"""
|
||||
|
||||
db_params = {
|
||||
k: v
|
||||
for k, v in db_config.config.get("args", {}).items()
|
||||
if not k.startswith("cp_")
|
||||
}
|
||||
db_conn = engine.module.connect(**db_params)
|
||||
engine.on_new_connection(db_conn)
|
||||
return db_conn
|
||||
|
||||
|
||||
class LoggingTransaction(object):
|
||||
"""An object that almost-transparently proxies for the 'txn' object
|
||||
passed to the constructor. Adds logging and metrics to the .execute()
|
||||
|
@ -218,10 +251,11 @@ class Database(object):
|
|||
|
||||
_TXN_ID = 0
|
||||
|
||||
def __init__(self, hs):
|
||||
def __init__(self, hs, database_config: DatabaseConnectionConfig, engine):
|
||||
self.hs = hs
|
||||
self._clock = hs.get_clock()
|
||||
self._db_pool = hs.get_db_pool()
|
||||
self._database_config = database_config
|
||||
self._db_pool = make_pool(hs.get_reactor(), database_config, engine)
|
||||
|
||||
self.updates = BackgroundUpdater(hs, self)
|
||||
|
||||
|
@ -234,7 +268,7 @@ class Database(object):
|
|||
# to watch it
|
||||
self._txn_perf_counters = PerformanceCounters()
|
||||
|
||||
self.engine = hs.database_engine
|
||||
self.engine = engine
|
||||
|
||||
# A set of tables that are not safe to use native upserts in.
|
||||
self._unsafe_to_upsert_tables = set(UNIQUE_INDEX_BACKGROUND_UPDATES.keys())
|
||||
|
@ -255,6 +289,11 @@ class Database(object):
|
|||
self._check_safe_to_upsert,
|
||||
)
|
||||
|
||||
def is_running(self):
|
||||
"""Is the database pool currently running
|
||||
"""
|
||||
return self._db_pool.running
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _check_safe_to_upsert(self):
|
||||
"""
|
||||
|
|
|
@ -16,8 +16,6 @@
|
|||
import struct
|
||||
import threading
|
||||
|
||||
from synapse.storage.prepare_database import prepare_database
|
||||
|
||||
|
||||
class Sqlite3Engine(object):
|
||||
single_threaded = True
|
||||
|
@ -25,6 +23,9 @@ class Sqlite3Engine(object):
|
|||
def __init__(self, database_module, database_config):
|
||||
self.module = database_module
|
||||
|
||||
database = database_config.get("args", {}).get("database")
|
||||
self._is_in_memory = database in (None, ":memory:",)
|
||||
|
||||
# The current max state_group, or None if we haven't looked
|
||||
# in the DB yet.
|
||||
self._current_state_group_id = None
|
||||
|
@ -59,7 +60,16 @@ class Sqlite3Engine(object):
|
|||
return sql
|
||||
|
||||
def on_new_connection(self, db_conn):
|
||||
prepare_database(db_conn, self, config=None)
|
||||
|
||||
# We need to import here to avoid an import loop.
|
||||
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)
|
||||
|
||||
db_conn.create_function("rank", 1, _rank)
|
||||
|
||||
def is_deadlock(self, error):
|
||||
|
|
|
@ -41,7 +41,7 @@ class UpgradeDatabaseException(PrepareDatabaseException):
|
|||
pass
|
||||
|
||||
|
||||
def prepare_database(db_conn, database_engine, config):
|
||||
def prepare_database(db_conn, database_engine, config, data_stores=["main"]):
|
||||
"""Prepares a database for usage. Will either create all necessary tables
|
||||
or upgrade from an older schema version.
|
||||
|
||||
|
@ -54,11 +54,10 @@ def prepare_database(db_conn, database_engine, config):
|
|||
config (synapse.config.homeserver.HomeServerConfig|None):
|
||||
application config, or None if we are connecting to an existing
|
||||
database which we expect to be configured already
|
||||
data_stores (list[str]): The name of the data stores that will be used
|
||||
with this database. Defaults to all data stores.
|
||||
"""
|
||||
|
||||
# For now we only have the one datastore.
|
||||
data_stores = ["main"]
|
||||
|
||||
try:
|
||||
cur = db_conn.cursor()
|
||||
version_info = _get_or_create_schema_state(cur, database_engine)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue