Files
dpb/backend/app/utils/number_gen.py
T
34047007@qq.com 0b50e0adfa checkpoint: SQLite 清理——业务/测试层统一 PostgreSQL(删 aiosqlite/fakeredis)
- setting: DATABASE_TYPE 收窄为 postgres,DB_URI/ASYNC_DB_URI 去 sqlite 分支
- database: create_async_engine_and_session 去 sqlite 分支(同步 psycopg 引擎保留给 jobstore)
- number_gen: 去 DATABASE_TYPE 非 postgres 早退(advisory lock 恒定走 PG)
- chat/crud: 删 SqliteDb 分支(agno 无硬依赖)
- pyproject/requirements/uv.lock: 删 aiosqlite、fakeredis;conftest 口径注释同步
- test_stat_analysis_tc: docstring 口径 SQLite→PG
2026-08-07 21:47:42 +08:00

58 lines
2.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""统一编号服务 —— 原子取号,替代各处分散扫描式生成(§3.3 编号约定)。
取号基于 bre_sequencename/current_seq)表 + PostgreSQL 事务级 advisory lock
pg_advisory_xact_lock 在事务内串行化同名取号,锁随事务提交/回滚自动释放,
与 CRUD 同事务提交 ⇒ 并发请求不重复、失败自动回滚不留空号。
存量数据迁移:首次使用某序列名时由 seed_fn(服务端传入)从既有记录推算
起始号(原扫描逻辑),此后全部走原子递增,不再扫描。
"""
from collections.abc import Awaitable, Callable
from sqlalchemy import select, text
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.v1.module_bre.sequence.model import SequenceModel
class NumberGenService:
"""原子序号发生器。实例化于 HTTP/服务层,随当前事务取号。"""
def __init__(self, db: AsyncSession) -> None:
self.db = db
@staticmethod
async def _acquire_lock(db: AsyncSession, name: str) -> None:
# hashtextextended 对文本做稳定 64 位哈希,同名序列恒映射同一把锁
await db.execute(text("SELECT pg_advisory_xact_lock(hashtextextended(:n, 0))"), {"n": name})
async def next_seq(
self,
name: str,
*,
start: int = 1,
seed_fn: Callable[[], Awaitable[int | None]] | None = None,
) -> int:
"""原子取号。返回本序列名下一个应使用的序号。
name: 序列名(如 "combination:2026" / "tree:3"),按编号维度天然分片;
start: 新序列的默认起始值;
seed_fn: 仅当序列行尚不存在时调用,从存量数据推算起始号(兼容既有数据迁移)。
"""
await self._acquire_lock(self.db, name)
row = await self.db.scalar(select(SequenceModel).where(SequenceModel.name == name))
if row is None:
init = await seed_fn() if seed_fn is not None else start
if init is None or init < 1:
init = start
self.db.add(SequenceModel(name=name, current_seq=init))
await self.db.flush()
return init
row.current_seq += 1
await self.db.flush()
return int(row.current_seq)
async def exists(self, name: str) -> bool:
"""序列行是否已初始化(供服务端判断是否需要 seed_fn 兜底)。"""
return (await self.db.scalar(select(SequenceModel.id).where(SequenceModel.name == name))) is not None