diff --git a/synapse/config/database.py b/synapse/config/database.py
index 118aafbd4a..1064c2697b 100644
--- a/synapse/config/database.py
+++ b/synapse/config/database.py
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright 2014-2016 OpenMarket Ltd
+# Copyright 2020 The Matrix.org Foundation C.I.C.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -12,73 +13,185 @@
# 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
-import yaml
+from synapse.config._base import Config, ConfigError
-from ._base import Config
+logger = logging.getLogger(__name__)
+NON_SQLITE_DATABASE_PATH_WARNING = """\
+Ignoring 'database_path' setting: not using a sqlite3 database.
+--------------------------------------------------------------------------------
+"""
-class DatabaseConfig(Config):
- def read_config(self, config, **kwargs):
- self.event_cache_size = self.parse_size(config.get("event_cache_size", "10K"))
+DEFAULT_CONFIG = """\
+## Database ##
+
+# The 'database' setting defines the database that synapse uses to store all of
+# its data.
+#
+# 'name' gives the database engine to use: either 'sqlite3' (for SQLite) or
+# 'psycopg2' (for PostgreSQL).
+#
+# 'args' gives options which are passed through to the database engine,
+# except for options starting 'cp_', which are used to configure the Twisted
+# connection pool. For a reference to valid arguments, see:
+# * for sqlite: https://docs.python.org/3/library/sqlite3.html#sqlite3.connect
+# * for postgres: https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-PARAMKEYWORDS
+# * for the connection pool: https://twistedmatrix.com/documents/current/api/twisted.enterprise.adbapi.ConnectionPool.html#__init__
+#
+#
+# Example SQLite configuration:
+#
+#database:
+# name: sqlite3
+# args:
+# database: /path/to/homeserver.db
+#
+#
+# Example Postgres configuration:
+#
+#database:
+# name: psycopg2
+# args:
+# user: synapse
+# password: secretpassword
+# database: synapse
+# host: localhost
+# cp_min: 5
+# cp_max: 10
+#
+# For more information on using Synapse with Postgres, see `docs/postgres.md`.
+#
+database:
+ name: sqlite3
+ args:
+ database: %(database_path)s
+"""
+
+
+class DatabaseConnectionConfig:
+ """Contains the connection config for a particular database.
- self.database_config = config.get("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 three fields: `name` for database
+ module name, `args` for the args to give to the database
+ connector, and optional `data_stores` that is a list of stores to
+ provision on this database (defaulting to all).
+ """
- if self.database_config is None:
- self.database_config = {"name": "sqlite3", "args": {}}
+ def __init__(self, name: str, db_config: dict):
+ db_engine = db_config.get("name", "sqlite3")
- name = self.database_config.get("name", None)
- if name == "psycopg2":
- pass
- elif name == "sqlite3":
- self.database_config.setdefault("args", {}).update(
+ if db_engine not in ("sqlite3", "psycopg2"):
+ raise ConfigError("Unsupported database type %r" % (db_engine,))
+
+ if db_engine == "sqlite3":
+ db_config.setdefault("args", {}).update(
{"cp_min": 1, "cp_max": 1, "check_same_thread": False}
)
- else:
- raise RuntimeError("Unsupported database type '%s'" % (name,))
-
- self.set_databasepath(config.get("database_path"))
-
- def generate_config_section(self, data_dir_path, database_conf, **kwargs):
- if not database_conf:
- database_path = os.path.join(data_dir_path, "homeserver.db")
- database_conf = (
- """# The database engine name
- name: "sqlite3"
- # Arguments to pass to the engine
- args:
- # Path to the database
- database: "%(database_path)s"
- """
- % locals()
- )
- else:
- database_conf = indent(yaml.dump(database_conf), " " * 10).lstrip()
- return (
- """\
- ## Database ##
+ data_stores = db_config.get("data_stores")
+ if data_stores is None:
+ data_stores = ["main", "state"]
+
+ self.name = name
+ self.config = db_config
+ self.data_stores = data_stores
+
+
+class DatabaseConfig(Config):
+ section = "database"
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
- database:
- %(database_conf)s
- # Number of events to cache in memory.
+ self.databases = []
+
+ def read_config(self, config, **kwargs):
+ # We *experimentally* support specifying multiple databases via the
+ # `databases` key. This is a map from a label to database config in the
+ # same format as the `database` config option, plus an extra
+ # `data_stores` key to specify which data store goes where. For example:
#
- #event_cache_size: 10K
- """
- % locals()
- )
+ # databases:
+ # master:
+ # name: psycopg2
+ # data_stores: ["main"]
+ # args: {}
+ # state:
+ # name: psycopg2
+ # data_stores: ["state"]
+ # args: {}
+
+ multi_database_config = config.get("databases")
+ database_config = config.get("database")
+ database_path = config.get("database_path")
+
+ if multi_database_config and database_config:
+ raise ConfigError("Can't specify both 'database' and 'databases' in config")
+
+ if multi_database_config:
+ if database_path:
+ raise ConfigError("Can't specify 'database_path' with 'databases'")
+
+ self.databases = [
+ DatabaseConnectionConfig(name, db_conf)
+ for name, db_conf in multi_database_config.items()
+ ]
+
+ if database_config:
+ self.databases = [DatabaseConnectionConfig("master", database_config)]
+
+ if database_path:
+ if self.databases and self.databases[0].name != "sqlite3":
+ logger.warning(NON_SQLITE_DATABASE_PATH_WARNING)
+ return
+
+ database_config = {"name": "sqlite3", "args": {}}
+ self.databases = [DatabaseConnectionConfig("master", database_config)]
+ self.set_databasepath(database_path)
+
+ def generate_config_section(self, data_dir_path, **kwargs):
+ return DEFAULT_CONFIG % {
+ "database_path": os.path.join(data_dir_path, "homeserver.db")
+ }
def read_arguments(self, args):
- self.set_databasepath(args.database_path)
+ """
+ Cases for the cli input:
+ - If no databases are configured and no database_path is set, raise.
+ - No databases and only database_path available ==> sqlite3 db.
+ - If there are multiple databases and a database_path raise an error.
+ - If the database set in the config file is sqlite then
+ overwrite with the command line argument.
+ """
+
+ if args.database_path is None:
+ if not self.databases:
+ raise ConfigError("No database config provided")
+ return
+
+ if len(self.databases) == 0:
+ database_config = {"name": "sqlite3", "args": {}}
+ self.databases = [DatabaseConnectionConfig("master", database_config)]
+ self.set_databasepath(args.database_path)
+ return
+
+ if self.get_single_database().name == "sqlite3":
+ self.set_databasepath(args.database_path)
+ else:
+ logger.warning(NON_SQLITE_DATABASE_PATH_WARNING)
def set_databasepath(self, database_path):
+
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
+
+ self.databases[0].config["args"]["database"] = database_path
@staticmethod
def add_arguments(parser):
@@ -89,3 +202,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 not self.databases:
+ raise Exception("More than one database exists")
+
+ return self.databases[0]
|