feat(db): partition hot tables and add shard routing layer - #112
Open
Darkdruce wants to merge 1 commit into
Open
feat(db): partition hot tables and add shard routing layer#112Darkdruce wants to merge 1 commit into
Darkdruce wants to merge 1 commit into
Conversation
Convert bets and blockchain_events to monthly declarative range partitions, keyed on placed_at and ledger_close_time. Postgres cannot ALTER a table into a partitioned one, so each is rebuilt via rename-aside, create parent, copy, drop. Add a slot-based sharding layer: markets hash to one of 4096 slots and slots are assigned to shards, so rebalancing moves a known set of rows without rehashing. market_id is the shard key so a market and its bets stay co-located. Ships with a single seeded shard, making the router a plain pool today and a deployment change later. Restore global tx_hash uniqueness via tx_hash_registry. A partitioned table's UNIQUE constraint must include the partition key, so UNIQUE (tx_hash, placed_at) only holds within a partition — a retry that re-defaults placed_at lands elsewhere and is accepted, which double-credits a bet. The registry is unpartitioned so its primary key is genuinely global, and is pruned alongside partition retention. Also fix 1723_add-token-columns, which called pool.query on the MigrationBuilder that node-pg-migrate passes and so had never applied. It is ordered before this migration and its columns must survive the table rebuild. Verified against Postgres 15: migration chain applies, rows route to the correct partitions, pruning scans one partition, the cross-partition duplicate is rejected, and down round-trips with data intact. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.
Closes #97
What
Implements the partitioning half of #97 as working code, and the sharding half as a routing layer that is live but single-shard.
Partitioning —
betsandblockchain_eventsbecome monthly declarative range partitions, keyed onplaced_atandledger_close_time. Postgres cannotALTERa table into a partitioned one, so each is rebuilt: rename aside, create parent, copy, drop. ADEFAULTcatch-all absorbs stray historical rows so the copy cannot fail.src/db/partitionManager.tshandles the rolling window: create months ahead, detach-then-drop past retention, andcountDefaultPartitionRowsas an alerting hook — a non-zero count means maintenance stopped running and writes are landing outside every defined range.Sharding — a 4096-slot space, Redis-Cluster style: keys hash to slots, and slots (not keys) are assigned to shards, so rebalancing moves a known set of rows without rehashing everything.
market_idis the shard key because a market and its bets must co-locate, otherwise the hot read path becomes a cross-shard join.ShardRouteris the interface services depend on instead of aPool. With the seeded single-shard row it degenerates to a plain connection pool, so it is safe to route everything through it now — adding a second shard later becomes a deployment change rather than a rewrite of every query site. Map validation rejects gaps and overlaps at startup, since a gap silently drops writes and an overlap sends one key to two shards.The correctness issue this surfaced
Partitioning silently weakens the
tx_hashdedup guard. A partitioned table'sUNIQUEconstraint must contain the partition key, soUNIQUE (tx_hash, placed_at)only holds within a partition. A write retried with a re-defaultedplaced_atlands in a different partition and is accepted — on a betting ledger, a double-credited bet. Confirmed empirically, not theoretical.Fixed with
tx_hash_registry: unpartitioned, so its primary key is genuinely global, trigger-enforced on insert, backfilled from copied rows, and pruned in step with partition retention so it does not grow without bound.This is worth a look during review. It adds a trigger to the write path. The alternative is making
placed_atdeterministic at the service layer so retries land in the same partition — cleaner, but it depends on services that are still blueprint stubs.Pre-existing bugs found
The migration chain cannot currently run from scratch. Two unrelated breakages:
1723000000000_add-token-columnsdestructured its argument as apg.Pooland calledpool.query, but node-pg-migrate passes aMigrationBuilder. It threwpool.query is not a functionand has never applied. Fixed here — it is ordered before this migration and its columns had to survive the table rebuild. The SQL is unchanged.1719360000000_add-cleanup-tablesaltersnotification_jobs, but no migration ever creates it — the table exists only indb/schema.sql. Not fixed here, as where that table should be defined is a judgment call. It was worked around locally to verify this branch. Worth its own issue.Verification
Against a real Postgres 15 (
docker-compose.test.yml), full chain from an empty schema:betsreportsrelkind = 'p'defaultas designedEXPLAINon a one-month range scans a single partition — pruning workstx_hashrejected by the registry guard; legitimate inserts unaffecteddownround-trips: data intact, globalbets_tx_hash_keyrestoredtests/db/partition.test.tsadds 26 unit tests, all passing.tsc --noEmitandeslintclean.Full
jestreports 24 failing suites — all pre-existingCannot find moduleagainst blueprint services that do not exist yet (StellarService,cache.service, …). None reference this code.🤖 Generated with Claude Code