1# Copyright 2014-2016 OpenMarket Ltd 2# Copyright 2020-2021 The Matrix.org Foundation C.I.C. 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15import argparse 16import logging 17import os 18 19from synapse.config._base import Config, ConfigError 20 21logger = logging.getLogger(__name__) 22 23NON_SQLITE_DATABASE_PATH_WARNING = """\ 24Ignoring 'database_path' setting: not using a sqlite3 database. 25-------------------------------------------------------------------------------- 26""" 27 28DEFAULT_CONFIG = """\ 29## Database ## 30 31# The 'database' setting defines the database that synapse uses to store all of 32# its data. 33# 34# 'name' gives the database engine to use: either 'sqlite3' (for SQLite) or 35# 'psycopg2' (for PostgreSQL). 36# 37# 'txn_limit' gives the maximum number of transactions to run per connection 38# before reconnecting. Defaults to 0, which means no limit. 39# 40# 'args' gives options which are passed through to the database engine, 41# except for options starting 'cp_', which are used to configure the Twisted 42# connection pool. For a reference to valid arguments, see: 43# * for sqlite: https://docs.python.org/3/library/sqlite3.html#sqlite3.connect 44# * for postgres: https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-PARAMKEYWORDS 45# * for the connection pool: https://twistedmatrix.com/documents/current/api/twisted.enterprise.adbapi.ConnectionPool.html#__init__ 46# 47# 48# Example SQLite configuration: 49# 50#database: 51# name: sqlite3 52# args: 53# database: /path/to/homeserver.db 54# 55# 56# Example Postgres configuration: 57# 58#database: 59# name: psycopg2 60# txn_limit: 10000 61# args: 62# user: synapse_user 63# password: secretpassword 64# database: synapse 65# host: localhost 66# port: 5432 67# cp_min: 5 68# cp_max: 10 69# 70# For more information on using Synapse with Postgres, 71# see https://matrix-org.github.io/synapse/latest/postgres.html. 72# 73database: 74 name: sqlite3 75 args: 76 database: %(database_path)s 77""" 78 79 80class DatabaseConnectionConfig: 81 """Contains the connection config for a particular database. 82 83 Args: 84 name: A label for the database, used for logging. 85 db_config: The config for a particular database, as per `database` 86 section of main config. Has three fields: `name` for database 87 module name, `args` for the args to give to the database 88 connector, and optional `data_stores` that is a list of stores to 89 provision on this database (defaulting to all). 90 """ 91 92 def __init__(self, name: str, db_config: dict): 93 db_engine = db_config.get("name", "sqlite3") 94 95 if db_engine not in ("sqlite3", "psycopg2"): 96 raise ConfigError("Unsupported database type %r" % (db_engine,)) 97 98 if db_engine == "sqlite3": 99 db_config.setdefault("args", {}).update( 100 {"cp_min": 1, "cp_max": 1, "check_same_thread": False} 101 ) 102 103 data_stores = db_config.get("data_stores") 104 if data_stores is None: 105 data_stores = ["main", "state"] 106 107 self.name = name 108 self.config = db_config 109 110 # The `data_stores` config is actually talking about `databases` (we 111 # changed the name). 112 self.databases = data_stores 113 114 115class DatabaseConfig(Config): 116 section = "database" 117 118 def __init__(self, *args, **kwargs): 119 super().__init__(*args, **kwargs) 120 121 self.databases = [] 122 123 def read_config(self, config, **kwargs) -> None: 124 # We *experimentally* support specifying multiple databases via the 125 # `databases` key. This is a map from a label to database config in the 126 # same format as the `database` config option, plus an extra 127 # `data_stores` key to specify which data store goes where. For example: 128 # 129 # databases: 130 # master: 131 # name: psycopg2 132 # data_stores: ["main"] 133 # args: {} 134 # state: 135 # name: psycopg2 136 # data_stores: ["state"] 137 # args: {} 138 139 multi_database_config = config.get("databases") 140 database_config = config.get("database") 141 database_path = config.get("database_path") 142 143 if multi_database_config and database_config: 144 raise ConfigError("Can't specify both 'database' and 'databases' in config") 145 146 if multi_database_config: 147 if database_path: 148 raise ConfigError("Can't specify 'database_path' with 'databases'") 149 150 self.databases = [ 151 DatabaseConnectionConfig(name, db_conf) 152 for name, db_conf in multi_database_config.items() 153 ] 154 155 if database_config: 156 self.databases = [DatabaseConnectionConfig("master", database_config)] 157 158 if database_path: 159 if self.databases and self.databases[0].name != "sqlite3": 160 logger.warning(NON_SQLITE_DATABASE_PATH_WARNING) 161 return 162 163 database_config = {"name": "sqlite3", "args": {}} 164 self.databases = [DatabaseConnectionConfig("master", database_config)] 165 self.set_databasepath(database_path) 166 167 def generate_config_section(self, data_dir_path, **kwargs) -> str: 168 return DEFAULT_CONFIG % { 169 "database_path": os.path.join(data_dir_path, "homeserver.db") 170 } 171 172 def read_arguments(self, args: argparse.Namespace) -> None: 173 """ 174 Cases for the cli input: 175 - If no databases are configured and no database_path is set, raise. 176 - No databases and only database_path available ==> sqlite3 db. 177 - If there are multiple databases and a database_path raise an error. 178 - If the database set in the config file is sqlite then 179 overwrite with the command line argument. 180 """ 181 182 if args.database_path is None: 183 if not self.databases: 184 raise ConfigError("No database config provided") 185 return 186 187 if len(self.databases) == 0: 188 database_config = {"name": "sqlite3", "args": {}} 189 self.databases = [DatabaseConnectionConfig("master", database_config)] 190 self.set_databasepath(args.database_path) 191 return 192 193 if self.get_single_database().name == "sqlite3": 194 self.set_databasepath(args.database_path) 195 else: 196 logger.warning(NON_SQLITE_DATABASE_PATH_WARNING) 197 198 def set_databasepath(self, database_path: str) -> None: 199 200 if database_path != ":memory:": 201 database_path = self.abspath(database_path) 202 203 self.databases[0].config["args"]["database"] = database_path 204 205 @staticmethod 206 def add_arguments(parser: argparse.ArgumentParser) -> None: 207 db_group = parser.add_argument_group("database") 208 db_group.add_argument( 209 "-d", 210 "--database-path", 211 metavar="SQLITE_DATABASE_PATH", 212 help="The path to a sqlite database to use.", 213 ) 214 215 def get_single_database(self) -> DatabaseConnectionConfig: 216 """Returns the database if there is only one, useful for e.g. tests""" 217 if not self.databases: 218 raise Exception("More than one database exists") 219 220 return self.databases[0] 221