Store instance database engine in database

This commit is contained in:
Tulir Asokan 2022-05-06 16:18:45 +03:00
parent 0663b680ab
commit f74a67dd79
7 changed files with 113 additions and 19 deletions

View file

@ -1,7 +1,7 @@
from mautrix.util.async_db import Database from mautrix.util.async_db import Database
from .client import Client from .client import Client
from .instance import Instance from .instance import DatabaseEngine, Instance
from .upgrade import upgrade_table from .upgrade import upgrade_table
@ -10,4 +10,4 @@ def init(db: Database) -> None:
table.db = db table.db = db
__all__ = ["upgrade_table", "init", "Client", "Instance"] __all__ = ["upgrade_table", "init", "Client", "Instance", "DatabaseEngine"]

View file

@ -16,6 +16,7 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING, ClassVar from typing import TYPE_CHECKING, ClassVar
from enum import Enum
from asyncpg import Record from asyncpg import Record
from attr import dataclass from attr import dataclass
@ -26,6 +27,11 @@ from mautrix.util.async_db import Database
fake_db = Database.create("") if TYPE_CHECKING else None fake_db = Database.create("") if TYPE_CHECKING else None
class DatabaseEngine(Enum):
SQLITE = "sqlite"
POSTGRES = "postgres"
@dataclass @dataclass
class Instance: class Instance:
db: ClassVar[Database] = fake_db db: ClassVar[Database] = fake_db
@ -35,21 +41,31 @@ class Instance:
enabled: bool enabled: bool
primary_user: UserID primary_user: UserID
config_str: str config_str: str
database_engine: DatabaseEngine | None
@property
def database_engine_str(self) -> str | None:
return self.database_engine.value if self.database_engine else None
@classmethod @classmethod
def _from_row(cls, row: Record | None) -> Instance | None: def _from_row(cls, row: Record | None) -> Instance | None:
if row is None: if row is None:
return None return None
return cls(**row) data = {**row}
db_engine = data.pop("database_engine", None)
return cls(**data, database_engine=DatabaseEngine(db_engine) if db_engine else None)
_columns = "id, type, enabled, primary_user, config, database_engine"
@classmethod @classmethod
async def all(cls) -> list[Instance]: async def all(cls) -> list[Instance]:
rows = await cls.db.fetch("SELECT id, type, enabled, primary_user, config FROM instance") q = f"SELECT {cls._columns} FROM instance"
rows = await cls.db.fetch(q)
return [cls._from_row(row) for row in rows] return [cls._from_row(row) for row in rows]
@classmethod @classmethod
async def get(cls, id: str) -> Instance | None: async def get(cls, id: str) -> Instance | None:
q = "SELECT id, type, enabled, primary_user, config FROM instance WHERE id=$1" q = f"SELECT {cls._columns} FROM instance WHERE id=$1"
return cls._from_row(await cls.db.fetchrow(q, id)) return cls._from_row(await cls.db.fetchrow(q, id))
async def update_id(self, new_id: str) -> None: async def update_id(self, new_id: str) -> None:
@ -58,17 +74,27 @@ class Instance:
@property @property
def _values(self): def _values(self):
return self.id, self.type, self.enabled, self.primary_user, self.config_str return (
self.id,
self.type,
self.enabled,
self.primary_user,
self.config_str,
self.database_engine_str,
)
async def insert(self) -> None: async def insert(self) -> None:
q = ( q = (
"INSERT INTO instance (id, type, enabled, primary_user, config) " "INSERT INTO instance (id, type, enabled, primary_user, config, database_engine) "
"VALUES ($1, $2, $3, $4, $5)" "VALUES ($1, $2, $3, $4, $5, $6)"
) )
await self.db.execute(q, *self._values) await self.db.execute(q, *self._values)
async def update(self) -> None: async def update(self) -> None:
q = "UPDATE instance SET type=$2, enabled=$3, primary_user=$4, config=$5 WHERE id=$1" q = """
UPDATE instance SET type=$2, enabled=$3, primary_user=$4, config=$5, database_engine=$6
WHERE id=$1
"""
await self.db.execute(q, *self._values) await self.db.execute(q, *self._values)
async def delete(self) -> None: async def delete(self) -> None:

View file

@ -2,4 +2,4 @@ from mautrix.util.async_db import UpgradeTable
upgrade_table = UpgradeTable() upgrade_table = UpgradeTable()
from . import v01_initial_revision from . import v01_initial_revision, v02_instance_database_engine

View file

@ -0,0 +1,25 @@
# maubot - A plugin-based Matrix bot system.
# Copyright (C) 2022 Tulir Asokan
#
# 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.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
from mautrix.util.async_db import Connection
from . import upgrade_table
@upgrade_table.register(description="Store instance database engine")
async def upgrade_v2(conn: Connection) -> None:
await conn.execute("ALTER TABLE instance ADD COLUMN database_engine TEXT")

View file

@ -34,7 +34,7 @@ from mautrix.util.config import BaseProxyConfig, RecursiveDict
from mautrix.util.logging import TraceLogger from mautrix.util.logging import TraceLogger
from .client import Client from .client import Client
from .db import Instance as DBInstance from .db import DatabaseEngine, Instance as DBInstance
from .lib.plugin_db import ProxyPostgresDatabase from .lib.plugin_db import ProxyPostgresDatabase
from .loader import DatabaseType, PluginLoader, ZippedPluginLoader from .loader import DatabaseType, PluginLoader, ZippedPluginLoader
from .plugin_base import Plugin from .plugin_base import Plugin
@ -71,10 +71,21 @@ class PluginInstance(DBInstance):
started: bool started: bool
def __init__( def __init__(
self, id: str, type: str, enabled: bool, primary_user: UserID, config: str = "" self,
id: str,
type: str,
enabled: bool,
primary_user: UserID,
config: str = "",
database_engine: DatabaseEngine | None = None,
) -> None: ) -> None:
super().__init__( super().__init__(
id=id, type=type, enabled=bool(enabled), primary_user=primary_user, config_str=config id=id,
type=type,
enabled=bool(enabled),
primary_user=primary_user,
config_str=config,
database_engine=database_engine,
) )
def __hash__(self) -> int: def __hash__(self) -> int:
@ -111,6 +122,8 @@ class PluginInstance(DBInstance):
"database": ( "database": (
self.inst_db is not None and self.maubot.config["api_features.instance_database"] self.inst_db is not None and self.maubot.config["api_features.instance_database"]
), ),
"database_interface": self.loader.meta.database_type_str if self.loader else "unknown",
"database_engine": self.database_engine_str,
} }
def _introspect_sqlalchemy(self) -> dict: def _introspect_sqlalchemy(self) -> dict:
@ -269,12 +282,27 @@ class PluginInstance(DBInstance):
self, upgrade_table: UpgradeTable | None = None, actually_start: bool = True self, upgrade_table: UpgradeTable | None = None, actually_start: bool = True
) -> None: ) -> None:
if self.loader.meta.database_type == DatabaseType.SQLALCHEMY: if self.loader.meta.database_type == DatabaseType.SQLALCHEMY:
if self.database_engine is None:
await self.update_db_engine(DatabaseEngine.SQLITE)
elif self.database_engine == DatabaseEngine.POSTGRES:
raise RuntimeError(
"Instance database engine is marked as Postgres, but plugin uses legacy "
"database interface, which doesn't support postgres."
)
self.inst_db = sql.create_engine(f"sqlite:///{self._sqlite_db_path}") self.inst_db = sql.create_engine(f"sqlite:///{self._sqlite_db_path}")
elif self.loader.meta.database_type == DatabaseType.ASYNCPG: elif self.loader.meta.database_type == DatabaseType.ASYNCPG:
if self.database_engine is None:
if os.path.exists(self._sqlite_db_path) or not self.maubot.plugin_postgres_db:
await self.update_db_engine(DatabaseEngine.SQLITE)
else:
await self.update_db_engine(DatabaseEngine.POSTGRES)
instance_db_log = db_log.getChild(self.id) instance_db_log = db_log.getChild(self.id)
# TODO should there be a way to choose between SQLite and Postgres if self.database_engine == DatabaseEngine.POSTGRES:
# for individual instances? Maybe checking the existence of the SQLite file. if not self.maubot.plugin_postgres_db:
if self.maubot.plugin_postgres_db: raise RuntimeError(
"Instance database engine is marked as Postgres, but this maubot isn't "
"configured to support Postgres for plugin databases"
)
self.inst_db = ProxyPostgresDatabase( self.inst_db = ProxyPostgresDatabase(
pool=self.maubot.plugin_postgres_db, pool=self.maubot.plugin_postgres_db,
instance_id=self.id, instance_id=self.id,
@ -334,7 +362,12 @@ class PluginInstance(DBInstance):
self.log.debug("Disabling webapp after plugin meta reload") self.log.debug("Disabling webapp after plugin meta reload")
self.disable_webapp() self.disable_webapp()
if self.loader.meta.database: if self.loader.meta.database:
await self.start_database(cls.get_db_upgrade_table()) try:
await self.start_database(cls.get_db_upgrade_table())
except Exception:
self.log.exception("Failed to start instance database")
await self.update_enabled(False)
return
config_class = cls.get_config_class() config_class = cls.get_config_class()
if config_class: if config_class:
try: try:
@ -455,6 +488,11 @@ class PluginInstance(DBInstance):
self.enabled = enabled self.enabled = enabled
await self.update() await self.update()
async def update_db_engine(self, db_engine: DatabaseEngine | None) -> None:
if db_engine is not None and db_engine != self.database_engine:
self.database_engine = db_engine
await self.update()
@classmethod @classmethod
@async_getter_lock @async_getter_lock
async def get( async def get(

View file

@ -13,7 +13,7 @@
# #
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
from typing import List from typing import List, Optional
from attr import dataclass from attr import dataclass
from packaging.version import InvalidVersion, Version from packaging.version import InvalidVersion, Version
@ -63,3 +63,7 @@ class PluginMeta(SerializableAttrs):
extra_files: List[str] = [] extra_files: List[str] = []
dependencies: List[str] = [] dependencies: List[str] = []
soft_dependencies: List[str] = [] soft_dependencies: List[str] = []
@property
def database_type_str(self) -> Optional[str]:
return self.database_type.value if self.database else None

View file

@ -43,7 +43,7 @@ class Instance extends BaseMainView {
} }
get entryKeys() { get entryKeys() {
return ["id", "primary_user", "enabled", "started", "type", "config"] return ["id", "primary_user", "enabled", "started", "type", "config", "database_engine"]
} }
get initialState() { get initialState() {
@ -54,6 +54,7 @@ class Instance extends BaseMainView {
started: true, started: true,
type: "", type: "",
config: "", config: "",
database_engine: "",
saving: false, saving: false,
deleting: false, deleting: false,