Store instance database engine in database
This commit is contained in:
parent
0663b680ab
commit
f74a67dd79
@ -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"]
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
25
maubot/db/upgrade/v02_instance_database_engine.py
Normal file
25
maubot/db/upgrade/v02_instance_database_engine.py
Normal 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")
|
@ -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:
|
||||||
|
try:
|
||||||
await self.start_database(cls.get_db_upgrade_table())
|
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(
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
Loading…
Reference in New Issue
Block a user