Files
meida_backend/packages/domain/ids.py
T
xsl 12583e9739 feat(shared): cross-cutting plumbing and pure domain layer
Typed pydantic-settings config, structlog JSON logging with request/trace id
propagation and redaction of phone numbers, tokens and face keys, the unified
response envelope and error-code table from 架构文档 §9.2, async SQLAlchemy
session management, ORM models for the eight foundation tables, and the
OpenTelemetry/Prometheus wiring.

packages/domain stays pure logic -- snowflake ids, the quota three-phase
protocol and the tk_task state machine -- so it can be tested without a
database and is held to mypy strict.
2026-08-14 00:26:39 +08:00

86 lines
2.5 KiB
Python
Executable File

"""Snowflake IDs (架构文档 §6.1 原则 2).
Auto-increment keys invite enumeration attacks and collide when the database is
later sharded; UUID primary keys have poor index locality. A 64-bit snowflake
avoids both.
Layout: 41 bits ms since epoch | 10 bits node id | 12 bits sequence.
"""
from __future__ import annotations
import threading
import time
from collections.abc import Callable
#: 2025-01-01T00:00:00Z in milliseconds. Do not change once IDs are issued.
DEFAULT_EPOCH_MS = 1_735_689_600_000
_NODE_BITS = 10
_SEQUENCE_BITS = 12
_MAX_NODE_ID = (1 << _NODE_BITS) - 1
_SEQUENCE_MASK = (1 << _SEQUENCE_BITS) - 1
def _wall_clock_ms() -> int:
return int(time.time() * 1000)
class SnowflakeGenerator:
"""Thread-safe monotonic 64-bit id generator."""
__slots__ = ("_clock", "_epoch_ms", "_last_ms", "_lock", "_node_id", "_sequence")
def __init__(
self,
node_id: int = 1,
epoch_ms: int = DEFAULT_EPOCH_MS,
clock: Callable[[], int] | None = None,
) -> None:
if not 0 <= node_id <= _MAX_NODE_ID:
msg = f"node_id must be within 0..{_MAX_NODE_ID}"
raise ValueError(msg)
self._node_id = node_id
self._epoch_ms = epoch_ms
self._sequence = 0
self._last_ms = -1
self._lock = threading.Lock()
self._clock = clock or _wall_clock_ms
def _now_ms(self) -> int:
return self._clock()
def next_id(self) -> int:
with self._lock:
now = self._now_ms()
if now < self._last_ms:
# Clock moved backwards: wait it out rather than issue duplicates.
now = self._last_ms
if now == self._last_ms:
self._sequence = (self._sequence + 1) & _SEQUENCE_MASK
if self._sequence == 0:
while now <= self._last_ms:
now = self._now_ms()
else:
self._sequence = 0
self._last_ms = now
elapsed = now - self._epoch_ms
return (
(elapsed << (_NODE_BITS + _SEQUENCE_BITS))
| (self._node_id << _SEQUENCE_BITS)
| self._sequence
)
_default_generator = SnowflakeGenerator()
def next_id() -> int:
"""Process-wide default generator."""
return _default_generator.next_id()
def timestamp_ms_of(snowflake_id: int, epoch_ms: int = DEFAULT_EPOCH_MS) -> int:
"""Extract the creation timestamp from an id (useful for partition pruning)."""
return (snowflake_id >> (_NODE_BITS + _SEQUENCE_BITS)) + epoch_ms