Fix: resolved sqlite concurrency errors (table users already exists) by moving DDL to explicit async initialize() and implementing lazy singleton dependency.

This commit is contained in:
2026-04-12 07:59:45 -04:00
parent b2e4706a14
commit 03f5a7826f
7 changed files with 45 additions and 41 deletions

Binary file not shown.

Binary file not shown.

View File

@@ -305,16 +305,18 @@ def mutate(
from decnet.mutator import mutate_decky, mutate_all, run_watch_loop from decnet.mutator import mutate_decky, mutate_all, run_watch_loop
from decnet.web.dependencies import repo from decnet.web.dependencies import repo
async def _run() -> None:
await repo.initialize()
if watch: if watch:
asyncio.run(run_watch_loop(repo)) await run_watch_loop(repo)
return elif decky_name:
await mutate_decky(decky_name, repo)
if decky_name:
asyncio.run(mutate_decky(decky_name, repo))
elif force_all: elif force_all:
asyncio.run(mutate_all(force=True, repo=repo)) await mutate_all(force=True, repo=repo)
else: else:
asyncio.run(mutate_all(force=False, repo=repo)) await mutate_all(force=False, repo=repo)
asyncio.run(_run())
@app.command() @app.command()

View File

@@ -13,7 +13,7 @@ from decnet.env import DECNET_ADMIN_USER, DECNET_ADMIN_PASSWORD
from decnet.web.auth import get_password_hash from decnet.web.auth import get_password_hash
from decnet.web.db.repository import BaseRepository from decnet.web.db.repository import BaseRepository
from decnet.web.db.models import User, Log, Bounty, State from decnet.web.db.models import User, Log, Bounty, State
from decnet.web.db.sqlite.database import get_async_engine, init_db from decnet.web.db.sqlite.database import get_async_engine
class SQLiteRepository(BaseRepository): class SQLiteRepository(BaseRepository):
@@ -25,34 +25,27 @@ class SQLiteRepository(BaseRepository):
self.session_factory = async_sessionmaker( self.session_factory = async_sessionmaker(
self.engine, class_=AsyncSession, expire_on_commit=False self.engine, class_=AsyncSession, expire_on_commit=False
) )
self._initialize_sync()
def _initialize_sync(self) -> None:
"""Initialize the database schema synchronously."""
init_db(self.db_path)
from decnet.web.db.sqlite.database import get_sync_engine
engine = get_sync_engine(self.db_path)
with engine.connect() as conn:
conn.execute(
text(
"INSERT OR IGNORE INTO users (uuid, username, password_hash, role, must_change_password) "
"VALUES (:uuid, :u, :p, :r, :m)"
),
{
"uuid": str(uuid.uuid4()),
"u": DECNET_ADMIN_USER,
"p": get_password_hash(DECNET_ADMIN_PASSWORD),
"r": "admin",
"m": 1,
},
)
conn.commit()
async def initialize(self) -> None: async def initialize(self) -> None:
"""Async warm-up / verification.""" """Async warm-up / verification. Creates tables if they don't exist."""
from sqlmodel import SQLModel
async with self.engine.begin() as conn:
await conn.run_sync(SQLModel.metadata.create_all)
async with self.session_factory() as session: async with self.session_factory() as session:
await session.execute(text("SELECT 1")) # Check if admin exists
result = await session.execute(
select(User).where(User.username == DECNET_ADMIN_USER)
)
if not result.scalar_one_or_none():
session.add(User(
uuid=str(uuid.uuid4()),
username=DECNET_ADMIN_USER,
password_hash=get_password_hash(DECNET_ADMIN_PASSWORD),
role="admin",
must_change_password=True,
))
await session.commit()
async def reinitialize(self) -> None: async def reinitialize(self) -> None:
"""Initialize the database schema asynchronously (useful for tests).""" """Initialize the database schema asynchronously (useful for tests)."""

View File

@@ -9,11 +9,16 @@ from decnet.web.db.repository import BaseRepository
from decnet.web.db.factory import get_repository from decnet.web.db.factory import get_repository
# Shared repository singleton # Shared repository singleton
repo: BaseRepository = get_repository() _repo: Optional[BaseRepository] = None
def get_repo() -> BaseRepository: def get_repo() -> BaseRepository:
"""FastAPI dependency to inject the configured repository.""" """FastAPI dependency to inject the configured repository."""
return repo global _repo
if _repo is None:
_repo = get_repository()
return _repo
repo = get_repo()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login") oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")

View File

@@ -14,8 +14,10 @@ from ..conftest import _FUZZ_SETTINGS
@pytest.fixture @pytest.fixture
def repo(tmp_path): async def repo(tmp_path):
return get_repository(db_path=str(tmp_path / "histogram_test.db")) r = get_repository(db_path=str(tmp_path / "histogram_test.db"))
await r.initialize()
return r
def _log(decky="d", service="ssh", ip="1.2.3.4", timestamp=None): def _log(decky="d", service="ssh", ip="1.2.3.4", timestamp=None):

View File

@@ -10,8 +10,10 @@ from .conftest import _FUZZ_SETTINGS
@pytest.fixture @pytest.fixture
def repo(tmp_path): async def repo(tmp_path):
return get_repository(db_path=str(tmp_path / "test.db")) r = get_repository(db_path=str(tmp_path / "test.db"))
await r.initialize()
return r
@pytest.mark.anyio @pytest.mark.anyio