Skip to content

perf: rewrite primary keys with bounded shadow generations - #71

Open
HelgeSverre wants to merge 8 commits into
kwhorne:mainfrom
HelgeSverre:agent/storage-bulk-rewrite-performance
Open

perf: rewrite primary keys with bounded shadow generations#71
HelgeSverre wants to merge 8 commits into
kwhorne:mainfrom
HelgeSverre:agent/storage-bulk-rewrite-performance

Conversation

@HelgeSverre

Copy link
Copy Markdown
Contributor

Summary

  • replace the unbounded ALTER TABLE ... ADD PRIMARY KEY rewrite with bounded shadow-generation builds and an atomic catalog cutover
  • reclaim old generations asynchronously and resume interrupted cleanup during startup
  • retain durable generation watermarks across DROP and RENAME so deferred cleanup cannot delete a newly reused table keyspace
  • add reproducible latency/RSS benchmarks and an opt-in adversarial crash/recovery harness

This is stacked on #70 and should be merged or retargeted after that PR lands.

Why

The previous rewrite collected and decoded the whole table, then staged all data and secondary-index mutations in one transaction. Peak memory scaled with the full table and the transaction's validated point mutations dominated latency.

The new path builds an unreachable physical generation in bounded commits, validates the source table and catalog at cutover, atomically switches a small generation pointer, and reclaims the previous generation afterward. Explicit transactions and multi-operation ALTER statements retain the original atomic path.

Self-review also found a deferred-cleanup race: dropping and immediately recreating a rewritten table could reuse a generation while an old cleanup task was still deleting it. Persistent per-name generation watermarks and rename target watermarks prevent that reuse.

Performance

Matched native Apple Silicon release builds, full durability, one secondary index, five samples per cell. The baseline is c7cdebc, immediately before this stack.

Rows Before median / p95 After median / p95 Change Before RSS growth After RSS growth
20,000 203.01 / 206.95 ms 100.57 / 126.58 ms 50.5% faster 6.6 MiB 5.5 MiB
100,000 1,219.81 / 1,545.40 ms 525.75 / 736.08 ms 56.9% faster 44.8 MiB 24.3 MiB
500,000 6,287.03 / 6,809.33 ms 3,930.00 / 6,086.40 ms 37.5% faster 269.7 MiB 148.6 MiB

Native MySQL 8.0.33 on the same host:

Rows MySQL median / p95 ElyraSQL before / MySQL ElyraSQL after / MySQL
20,000 40.05 / 47.58 ms 5.07x 2.51x
100,000 127.17 / 135.26 ms 9.59x 4.13x
500,000 563.86 / 699.20 ms 11.15x 6.97x

MySQL remains substantially faster, particularly as the table grows. The benchmark documentation describes the methodology and deferred-cleanup tradeoff.

Crash and sustained-load testing

The five-minute adversarial campaign completed:

  • 327 forced process crashes
  • 237 additional kills during startup recovery
  • 90 SIGSTOP then SIGKILL cycles
  • 327 successful invariant-checked restarts
  • 1,272,209 indexed reads
  • 28,610 committed transfers
  • 7,184 atomic commit epochs
  • 48 completed shadow rewrites
  • 253.9 MiB peak RSS and 289.6 MiB peak database size

All final durability, account-balance, table aggregate, index-versus-scan, and atomicity invariants passed. A final post-fix 60-second campaign also passed 115 forced crashes and 91 recovery-time kills.

One earlier compound-failure run exposed a transient redb 2.6.3 startup panic (assertion failed: !self.needs_recovery) after repeatedly killing recovery. The preserved database opened successfully on the next clean launch and no persistent corruption was observed. The harness records retry/open failures, and the limitation is documented rather than hidden. Torn-sector and reordered/failed individual I/O testing will require a lower-level fault-injection interface.

Validation

  • cargo fmt --all -- --check
  • cargo clippy --all-targets -- -D warnings
  • cargo test --workspace
  • targeted drop/recreate/rename generation-watermark regression test
  • matched five-sample before/after/MySQL benchmark
  • five-minute and post-fix one-minute crash/recovery campaigns

@HelgeSverre
HelgeSverre marked this pull request as ready for review August 10, 2026 21:14
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