Devshard Gateway Postgres Backend#1426
Open
a-kuprin wants to merge 13 commits into
Open
Conversation
…#1314) * feat(devshard): settle depleted escrow when last active request drains * fix(devshard): skip settlement reconcile on restart when settlement disabled
* fix(devshard): retire runtime escrow * review(devshard): copilot suggestions & todo for future task implementation * fix(devshard): retire runtime escrow when in-fligh request completed
…ing_split (#1226) * feat(devshard): MiniMax-M2.7 route — per-model dispatch + tool-message shape + reasoning_split * fix(devshard): reject structural_tag string form and whitespace-only names that hang/crash the engine * refactor(devshard): address review nits — clearer validator names + shared testutil
… instead of vLLM (#1318) * feat(devshard): validate and clamp chat-completion params at the gateway, unify and extract filter handlers * fix(devshard): mirror max_completion_tokens into max_tokens so the Kimi thinking-budget bounds reasoning (no empty output)
…t classification (#1240) * fix(devshard): reassemble fragmented SSE events for raceWriter content classification * fix(devshard): resolve comments * fix(devshard): bound SSE classify buffer with per-attempt 1 MiB + global 100 MiB atomic cap * fix(devshard): log reassembly-buffer drops when classify caps are exceeded * fix(devshard): sample reassembled SSE buffer, parallel subtests, stdlib strconv/rand * fix(devshard): flush newline-less final SSE event on stream completion * fix(devshard): flush newline-less final SSE event and release dropped classify buffers * fix(devshard): isolate SSE classify buffers per participant, make caps env-tunable, degrade gracefully on cap drop
* fix(devshard): count background race finalizers in the drain barrier * fix(devshard): rename vars to make more clear naming
…-minting on chain (#1343) * fix(devshard): recover orphaned rotation escrows on DB persist failure instead of re-minting on chain * fix(devshard): don't clear rotation commitment on transient 404; only after unordered-tx TTL elapses to avoid orphaning escrows * fix(devshard): GetTxEscrowID not found error
…llback Enable hybrid persistence for devshardctl gateway state when PGHOST is set: Postgres primary, SQLite fallback, one-shot migration from gateway.db, and a SQLite sync journal that replays outage-era writes to Postgres before PG resumes as primary. Harden reconnect with dropPg, in-request write retries, per-operation query timeouts, and a shorter gateway reconnect interval. Also make PG_CONNECT_TIMEOUT configurable for decentralized-api payload storage.
f195da9 to
2aeed2c
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
devshardctlgateway management state (settings, devshards, throttles, rotationstatus, suspicious hosts, escrow commitments) can now persist to PostgreSQL
when
PGHOSTis set, with SQLite as a durable fallback. The design mirrorsthe hybrid pattern already used for payload storage: Postgres is primary during
normal operation; SQLite absorbs writes when Postgres is unavailable and is used
for reads when Postgres is empty or errors.
Activation is env-driven (
PGHOSTset → hybrid; unset → SQLite-only). Aone-shot startup migration copies an existing
gateway.dbinto Postgres on firstconnect. A sync journal (SQLite outbox) records only the rows changed during
a Postgres outage and replays them to Postgres on reconnect — before Postgres
resumes as primary — so we never blindly overwrite healthy PG data from stale
SQLite.
Also makes
PG_CONNECT_TIMEOUTconfigurable for decentralized-api payloadstorage (unchanged behavior when unset).
Architecture
Healthy path. Writes and reads go to Postgres. SQLite is not updated on every
write — it is a fallback copy, not a live replica.
Fallback path. On Postgres failure (after in-request retries), the write goes
to SQLite and a journal entry
(table, row_key, op)is appended in the sameSQLite transaction as the data change.
Recovery path. When Postgres comes back, the hybrid store drains the journal
(coalesce last op per key → apply upserts/deletes to Postgres in one PG
transaction → delete drained journal rows) and only then publishes the Postgres
connection as primary again.
What was added
PostgresGatewayStoreGatewayStoreSQLiteGatewayStoreHybridGatewayStoreMigrateGatewaySQLiteToPostgresgateway.dbgateway_pg_sync_journalNewGatewayStorefactoryEdge cases and how they are handled
Brief Postgres blip (sub-second reset / failover)
A single failed write does not immediately fall back.
PG_WRITE_RETRY_BUDGET(default
2s) retries with exponential backoff sopgxpoolcan hand out afresh connection. Each attempt is bounded by
PG_OPERATION_TIMEOUT(default2s) so a hung query cannot block indefinitely.If all retries succeed, the write never touches SQLite or the journal.
Sustained Postgres outage
After the retry budget is exhausted, the hybrid store drops the cached Postgres
connection (
dropPg) and routes writes to SQLite + journal. This is required:if we kept
h.pgset,pgxpoolcould transparently reconnect and serve PG writeswhile stale journal entries still existed — a later drain would replay outdated
SQLite rows over newer PG data (lost update).
While
h.pgis nil, writes skip Postgres entirely (fast SQLite path). Reconnectis rate-limited by
PG_RETRY_INTERVAL(gateway default15s).PG commit succeeds but journal delete fails
Drain order: apply to Postgres → commit PG tx → delete journal rows up to
max(seq). If the delete fails or the process crashes in between, entries remainand are re-applied on the next drain. Replays are idempotent (
ON CONFLICT DO UPDATE; deletes are no-ops if absent). JournalseqisAUTOINCREMENT, sopost-snapshot writes always get a higher seq and are not accidentally deleted.
Journal write vs data write atomicity
writeWithSyncJournalruns the data mutation and journal inserts in one SQLitetransaction. A failure rolls back both — no orphan journal rows, no journaled
changes without persisted data.
Reads needed inside that transaction (e.g. returning the updated suspicious-host
list) use the transaction handle, not a separate
db.Query, because SQLite iscapped at
SetMaxOpenConns(1)and a second connection would deadlock.Split-brain / stale SQLite after healthy PG period
During healthy operation SQLite is not kept in sync. A naive "copy all of
SQLite back to Postgres on reconnect" would clobber PG-only updates from before
the outage. The journal records only keys mutated while Postgres was down;
drain replays those keys' current SQLite row (last-writer-wins per key), not
a full snapshot.
One-shot migration vs journal
Migration (
MigrateGatewaySQLiteToPostgres) is the bulk initial seed whenenabling Postgres on an existing
gateway.db(guarded by marker + PG emptiness).The journal handles incremental outage deltas afterward. Both use the same
idempotent per-row upsert helpers; overlap is harmless.
Process restart during outage
The journal lives in SQLite and survives restarts. On startup with
PGHOSTset,the factory connects to Postgres, runs migration if needed, then drains any
pending journal before serving traffic.
Rollback to SQLite-only (
PGHOSTunset after PG period)If an operator disables Postgres after running with it, PG-only writes are not
in SQLite — this is an explicit escape hatch, not automatic bidirectional
sync. Re-enabling Postgres later relies on PG as source of truth plus any journal
entries from subsequent outages. Documented as an operator caveat.
Drain blocks concurrent publish
Journal drain runs under the hybrid mutex;
h.pgis set only after the journal isempty. Concurrent writers also take the mutex when probing reconnect, so no new
journal entry can slip in between "journal empty" and "PG published". Steady-state
hot path only holds the mutex to read the connection pointer — not across I/O.
Kill switch: Postgres-only mode
PG_TO_SQLITE_FALLBACK=falsedisables SQLite fallback on Postgres errors. Writesand reads return errors when Postgres is unavailable; startup fails if Postgres
cannot be reached. The sync journal is not used in this mode. Use when Postgres
is the sole source of truth and operators prefer hard failures over degraded
SQLite service.