Skip to content

Persist trading state to Postgres with boot rehydrate#166

Merged
ozpool merged 1 commit into
mainfrom
feat/postgres-persistence
May 29, 2026
Merged

Persist trading state to Postgres with boot rehydrate#166
ozpool merged 1 commit into
mainfrom
feat/postgres-persistence

Conversation

@ozpool
Copy link
Copy Markdown
Owner

@ozpool ozpool commented May 29, 2026

What

Backs the edge's in-memory stores (orders, positions, fills, vault balances, public trade tape, funding history) with Postgres so a restart rehydrates instead of starting empty.

How

  • Reads stay on the hot in-memory path — unchanged.
  • Writes mirror through a channel: each mutator emits a PersistEvent; one background writer task drains it and runs the SQL in order. Keeps the sync, lock-guarded mutators off async I/O.
  • Boot loads a full snapshot from Postgres, repopulates the maps, and rebuilds each market's orderbook from the rehydrated resting orders.
  • Schema in crates/perplex-edge/migrations/0001_init.sql, applied idempotently on connect.

Derived/ephemeral state is deliberately not persisted: the orderbook is rebuilt from open_orders, index prices come from the live oracle, SIWE nonces expire in seconds.

The binary now requires DATABASE_URL (the bundled Postgres already ships in docker-compose.yml). Unit tests are untouched — AppState::new stays a pure in-memory path with no channel.

Test

  • 31 edge unit/integration tests green, including new coverage for snapshot rehydration and event emission (no live DB needed).
  • Manual end-to-end against local Postgres: placed a resting order and matched a taker against a maker, killed the process, restarted — orders, decremented maker remaining, both positions, fills, balances, and the rebuilt orderbook all survived.

Notes

  • Durability has a small window: the write channel is in-memory, so a hard crash can drop the last few unflushed events. Acceptable at this stage; synchronous-commit durability would need the mutators made async.

The edge held all orders, positions, fills, balances, the public trade
tape, and funding history in memory only, so a restart wiped every
user's state and emptied the book. Back those stores with Postgres.

Reads stay on the hot in-memory path. Each mutator emits a PersistEvent
onto an unbounded channel; a single background writer task drains it and
applies the SQL in order, so the sync lock-guarded mutators never block
on async I/O. On boot the edge loads a full snapshot from Postgres,
repopulates the maps, and rebuilds each market's orderbook from the
rehydrated resting orders.

Derived/ephemeral state is intentionally not persisted: the orderbook is
rebuilt from open_orders, index prices come from the live oracle, and
SIWE nonces expire in seconds.

The binary now requires DATABASE_URL (bundled Postgres already ships in
docker-compose). Unit tests are unaffected — AppState::new stays a pure
in-memory path with no channel.
@ozpool ozpool force-pushed the feat/postgres-persistence branch from dde4f76 to d2694f9 Compare May 29, 2026 08:28
@ozpool ozpool merged commit b50adad into main May 29, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant