Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions docs/getting-started/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ ExecStart=/opt/vox/.venv/bin/uvicorn vox.api.app:create_app \
--factory \
--host 127.0.0.1 \
--port 8000 \
--workers 1
--workers 4
Restart=on-failure
RestartSec=5

Expand All @@ -181,8 +181,11 @@ WantedBy=multi-user.target
sudo systemctl enable --now vox
```

!!! warning "Single worker"
Vox uses in-process state for the gateway hub, rate limiter, and presence. Always run with `--workers 1`.
!!! info "Multi-worker support (PostgreSQL required)"
When using PostgreSQL, rate-limit buckets, presence, and gateway event
fan-out are stored in shared **unlogged tables** with cross-worker
notification via `LISTEN/NOTIFY`. You can safely run with `--workers N`.
With SQLite the state remains in-memory; use `--workers 1` in that case.

### Docker

Expand All @@ -202,7 +205,7 @@ COPY . .
RUN pip install --no-cache-dir .

EXPOSE 8000
CMD ["uvicorn", "vox.api.app:create_app", "--factory", "--host", "0.0.0.0", "--port", "8000", "--workers", "1"]
CMD ["uvicorn", "vox.api.app:create_app", "--factory", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
```

```yaml title="docker-compose.yml"
Expand Down
24 changes: 19 additions & 5 deletions src/vox/api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ async def _periodic_cleanup(db_factory):

try:
from vox.ratelimit import evict_stale, evict_token_cache
evict_stale()
await evict_stale()
evict_token_cache()
except Exception:
logger.error("Periodic cleanup: rate limiter eviction failed", exc_info=True)
Expand Down Expand Up @@ -150,17 +150,29 @@ async def lifespan(app: FastAPI):
from vox.config import load_config
async with get_session_factory()() as db:
await load_config(db)
# Initialize shared state (unlogged tables + LISTEN/NOTIFY on PG)
from vox.db.shared_state import init_shared_state, shutdown_shared_state, is_pg
await init_shared_state()

# Initialize the gateway hub
init_hub()

# Startup warnings
from vox.config import config
if config.webauthn.rp_id is None:
logger.warning("WebAuthn is not configured (VOX_WEBAUTHN_RP_ID / VOX_WEBAUTHN_ORIGIN not set)")
logger.warning(
"Vox uses in-memory state (rate limiter, gateway hub, presence). "
"Run with a single worker process only."
)
if is_pg():
from vox.db.shared_state import WORKER_ID
logger.info(
"Shared state backed by PostgreSQL unlogged tables (worker=%s). "
"Multi-worker deployment is supported.",
WORKER_ID,
)
else:
logger.warning(
"SQLite backend — rate limiter, gateway hub, and presence are in-memory. "
"Run with a single worker process only."
)

# Start background cleanup task
cleanup_task = asyncio.create_task(_periodic_cleanup(get_session_factory()))
Expand All @@ -177,6 +189,8 @@ async def lifespan(app: FastAPI):
pass
# Shutdown SFU
stop_sfu()
# Shutdown shared state (unregister worker)
await shutdown_shared_state()
# Close federation HTTP client
from vox.federation.service import close_http_client
await close_http_client()
Expand Down
18 changes: 16 additions & 2 deletions src/vox/api/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,25 @@ async def _is_safe_url(url: str) -> tuple[bool, str, str]:
resolved_ip = str(addr)
return True, resolved_ip, hostname

# Simple snowflake: 42-bit timestamp (ms) + 22-bit sequence
# Snowflake: 42-bit timestamp (ms) | 10-bit worker | 12-bit sequence
# The 10-bit worker id is derived from WORKER_ID at first use, giving up to
# 1024 distinct workers. Within a single worker the 12-bit sequence allows
# 4096 IDs per millisecond which is more than enough.
_seq = 0
_last_ts = 0
_worker_bits: int | None = None
_snowflake_lock = asyncio.Lock()


def _get_worker_bits() -> int:
global _worker_bits
if _worker_bits is None:
from vox.db.shared_state import WORKER_ID
# Deterministic 10-bit hash of the worker id string
_worker_bits = (hash(WORKER_ID) & 0x3FF)
return _worker_bits


async def _snowflake() -> int:
global _seq, _last_ts
async with _snowflake_lock:
Expand All @@ -76,7 +89,8 @@ async def _snowflake() -> int:
else:
_seq = 0
_last_ts = ts
return (ts << 22) | (_seq & 0x3FFFFF)
wid = _get_worker_bits()
return (ts << 22) | (wid << 12) | (_seq & 0xFFF)


async def _update_search_vector(db, msg_id: int, body: str | None) -> None:
Expand Down
Loading