mirror of
https://github.com/Donkie/Spoolman.git
synced 2026-10-22 01:52:17 -05:00
Several DB write functions mutated and then sent their websocket notification (or
just returned) without committing, relying on the request session's teardown
commit. On async postgres that commit races the HTTP response, so a fast
follow-up read could still see the pre-write state — e.g. `DELETE /vendor/{id}`
returns 200 but an immediate `GET` still returns 200. Latent since 2023 (a6d527a);
the async-timing dependency bump widened the window enough to intermittently fail
`test (postgres)` in CI.
Commit inline before notifying, matching the functions that already did
(filament.delete, vendor/spool create/update) and the commit-first invariant from
the earlier websocket-ordering fix:
- vendor.delete, spool.delete
- setting.update, setting.delete
- vendor/filament/spool.clear_extra_field
- spool.rename_location
(use/measure already commit via their wrappers; reads are unaffected.)
Verified via `SPOOLMAN_CONTAINER_ENGINE=podman poe itest`: full 4-DB suite green
plus 7/7 repeat postgres runs with zero flakes (was ~66% failing before).
75 lines
2.5 KiB
Python
75 lines
2.5 KiB
Python
"""Helper functions for interacting with vendor database objects."""
|
|
|
|
from datetime import datetime
|
|
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from spoolman.api.v1.models import EventType, SettingEvent, SettingKV
|
|
from spoolman.database import models
|
|
from spoolman.exceptions import ItemNotFoundError
|
|
from spoolman.settings import SettingDefinition
|
|
from spoolman.ws import websocket_manager
|
|
|
|
SETTING_MAX_LENGTH = 2**16 - 1
|
|
|
|
|
|
async def update(
|
|
*,
|
|
db: AsyncSession,
|
|
definition: SettingDefinition,
|
|
value: str,
|
|
) -> None:
|
|
"""Set a setting in the database."""
|
|
if len(value) > SETTING_MAX_LENGTH:
|
|
raise ValueError(f"Setting value is too big, max size is {SETTING_MAX_LENGTH} characters.")
|
|
|
|
setting = models.Setting(
|
|
key=definition.key,
|
|
value=value,
|
|
last_updated=datetime.utcnow().replace(microsecond=0),
|
|
)
|
|
await db.merge(setting)
|
|
# Commit before notifying so the setting is durable and visible to subsequent
|
|
# requests; post-commit notification must be the last, infallible step.
|
|
await db.commit()
|
|
await setting_changed(definition, value, EventType.UPDATED)
|
|
|
|
|
|
async def get(db: AsyncSession, definition: SettingDefinition) -> models.Setting:
|
|
"""Get a specific setting from the database."""
|
|
setting = await db.get(models.Setting, definition.key)
|
|
if setting is None:
|
|
raise ItemNotFoundError(f"Setting with key {definition.key} has not been set.")
|
|
return setting
|
|
|
|
|
|
async def get_all(db: AsyncSession) -> list[models.Setting]:
|
|
"""Get all set settings in the database."""
|
|
stmt = select(models.Setting)
|
|
rows = await db.execute(stmt)
|
|
return list(rows.scalars().all())
|
|
|
|
|
|
async def delete(db: AsyncSession, definition: SettingDefinition) -> None:
|
|
"""Delete a setting from the database."""
|
|
setting = await get(db, definition)
|
|
await db.delete(setting)
|
|
# Commit before notifying so the deletion is durable and visible to subsequent
|
|
# requests; post-commit notification must be the last, infallible step.
|
|
await db.commit()
|
|
await setting_changed(definition, None, EventType.DELETED)
|
|
|
|
|
|
async def setting_changed(definition: SettingDefinition, set_value: str | None, typ: EventType) -> None:
|
|
"""Notify websocket clients that a setting has changed."""
|
|
await websocket_manager.send(
|
|
("setting", str(definition.key)),
|
|
SettingEvent(
|
|
type=typ,
|
|
resource="setting",
|
|
date=datetime.utcnow(),
|
|
payload=SettingKV.from_db(definition, set_value),
|
|
),
|
|
)
|