Convert indexer tables to TimescaleDB hypertables#486
Conversation
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…into timescale
| // Schedule the reconciliation job to run every 1 hour. | ||
| // | ||
| // The job checks whether retention has dropped chunks and advances the | ||
| // oldest ledger cursor if so. It is idempotent — a no-op when the cursor | ||
| // is already correct — and the query is microsecond-cheap (reads oldest | ||
| // chunk metadata + 1 row from ingest_store). Running on a fixed 1-hour | ||
| // interval keeps the cursor at most 1 hour stale after retention fires, | ||
| // with no coordination required with the retention job schedule. |
There was a problem hiding this comment.
I'm concerned about the semantic shift from having the oldest ledger cursor updated atomically to having it updated out-of-band in a cron job. Specifically, how does this shift impact systems that rely on the oldest ledger cursor?
My understanding is that our backfilling job reads the oldest ledger value to determine which ranges to process. What happens when this value is stale? For example, if the job is to backfill ledgers 1000 - 5000, because the wallet backend operator sees the oldest ledger at 5000, but its actually stale and the oldest ledger is really 10000, we'll create a gap in history between 5000 - 10000.
Is there a way to ensure the cursor is updated atomically with a chunk being dropped from the retention window? If not, should we have the job run more frequently?
There was a problem hiding this comment.
Hmm I think we can do the following together:
-
Not use the oldest ledger cursor when calculating backfill gaps but instead always check the actual min ledger value stored in the table: basically the same query that we use to reconcile the oldest ledger above. This will be agnostic of when the retention policy runs since we directly get the value from DB.
-
Reduce the frequency of reconciliation to update the oldest ledger: this value is currently shown in our grafana dashboard to help us keep track of current retention range. That would ensure it gets updated quickly
I did explore atomically doing the retention policy and updation together however Timescale doesnt allow us to chain jobs.
There was a problem hiding this comment.
for option #1, which table are we going to query? iiuc the retention jobs could happen at different times for each table. For this option to be safe, a job should use the oldest ledger across all of the tables it cares about.
One option that truly eliminates the potential gap would be to use a custom retention job instead of the built in one. This has the downside of losing all of the built in scheduling/lifecycle management/monitoring but would allow us to do it atomically.
There was a problem hiding this comment.
If we were to use a table's actual oldest ledger during backfill jobs, what else is the oldest ledger value actually used for? Maybe we should just remove the oldest ledger cursor?
There was a problem hiding this comment.
So I already updated the code to ensure backfill job doesnt use the cursor but actually gets the min ledger from the data: ab7985b
The only reason we would need the cursor is to know the current stored range and start another historical backfill. Other than that it is not used anywhere
Code reviewFound 1 issue:
wallet-backend/internal/integrationtests/infrastructure/helpers.go Lines 50 to 67 in aa3801f 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
This reverts commit c3f871f.
What
This PR converts the five indexer tables (
transactions,transactions_accounts,operations,operations_accounts,state_changes) from regular PostgreSQL tables to TimescaleDB hypertables with columnstore compression. It also:trustline_balances,native_balances,sac_balances) withfillfactor=80and aggressive autovacuum settings for HOT update optimizationreconcile_oldest_cursorscheduled job to keep theoldest_ingest_ledgercursor accurate after retention drops old chunksTrackRPCServiceHealthmethod (was only called from integration tests, never from production entrypoints)BatchInsertmethods, keeping onlyBatchCopyWhy
The indexer tables are append-heavy, time-ordered, and grow continuously — a workload pattern that maps directly to TimescaleDB's hypertable model. Converting to hypertables with columnstore compression gives us:
DELETEoperations, and the reconciliation job keeps cursors in syncledger_created_atavoids scanning irrelevant data, and sparse bloom indexes accelerate filtered lookupsThe balance tables remain regular PostgreSQL tables since they're UPSERT-heavy (not append-only), but get storage parameter tuning to maximize HOT (Heap-Only Tuple) updates where only non-indexed columns change.
Known limitations
Closes #519