Skip to content

perf(engine): append logged feed events during recovery without decoding them - #100

Merged
venkat1701 merged 1 commit into
mainfrom
perf/faster-recovery
Oct 14, 2026
Merged

venkat1701 merged 1 commit into
mainfrom
perf/faster-recovery

Conversation

@venkat1701

Copy link
Copy Markdown
Collaborator

Opening a crashed store replays the WAL written since the last checkpoint. Each replayed commit's Feed record was decoded into a CommitEvent and passed to ChangeFeed.append. After a process crash the feed file usually already holds that generation, so the decoded event was thrown away. Otherwise it was encoded again into the same bytes.

The WAL payload is exactly the FeedCodec encoding the feed stores. Recovery now passes it to ChangeFeed.appendEncoded, which reads the generation from the start of the payload (FeedCodec.generationOf) and writes the bytes unchanged unless the feed already has that generation. Recovery.recover no longer takes a FeedCodec. No file format, default or setting changes.

Allocation, main against this branch

Crashed copies of benchmark stores (ingest killed with SIGKILL after half the nodes were acknowledged, async durability), opened in warmed-up JVMs, two JVMs of 12 opens per variant, main and branch alternating. The first two opens of each JVM are dropped, which leaves 20 per variant. Allocation is the opening thread's allocated bytes, median per open:

Store Replayed Whole open, main Whole open, branch Replay alone, main Replay alone, branch
scale 1, copy a 28 commits, 4.46 MB of WAL 115,934 KiB 93,696 KiB 89,071 KiB 66,837 KiB
scale 1, copy b 28 commits, 4.46 MB of WAL 115,524 KiB 94,865 KiB 89,071 KiB 68,464 KiB
scale 4, copy a 39 commits 264,927 KiB 234,154 KiB 189,982 KiB 160,571 KiB

So about 20 MiB less per scale-1 open (18 to 19%) and 30 MiB less at scale 4 (12%). Every open recovered the same node count on both sides (26,000 and 101,000). I'm not quoting times: other builds were running and the startup checkpoint's page writes varied by an order of magnitude between runs.

Tests

FeedRecoveryTest.recoveryDecodesEachLoggedValueOnce registers a slot whose codec counts decodes, commits 20 values, halts and reopens. Recovery decodes each value once on this branch (20) and twice on main (40). recoveryRestoresFeedEventsTheFeedLost deletes the feed files after a crash and checks that recovery writes back the same events and the data is intact.

Where the rest of a scale-1 crash open goes

This doesn't close the gap to HyperGraphDB on the recover workload at scale 1, and it isn't meant to. With HyperGraphDB measured in the same window, the crashed scale-1 store opened in a median of 253 ms for HStore and 162 ms for HyperGraphDB (15 opens each), with 116,012 KiB and 80,486 KiB allocated. The difference is how much work each store left for recovery:

  • The HStore crash child never checkpointed. A 4.46 MB WAL is far below checkpoint_wal_mb (256) and the write buffer never got past half of write_buffer_mb (64), so the catalog still holds the empty initial image. The open replays all 28 commits (the whole 26,000-node load, 72 ms median), then the startup checkpoint writes every page of the store (74 ms median) and does about 35 ms of fsyncs (catalog 16.9, page sync 5.3, reference journal 5.3, WAL 3.8, applying the journal 4.0).
  • JE checkpoints every 20 MB of log by default. DbPrintLog -S on a crashed HyperGraphDB copy shows two checkpoints in its 24 MB log and 749,476 bytes after the last one, with at most 9,690 records to replay. Its redo phase took 58 ms median and its own recovery checkpoint 20.5 ms.

At scale 4 the HStore child does checkpoint before the kill, so the copy above replays 39 commits instead of the whole load. I didn't break down HyperGraphDB's side at scale 4.

…ing them

Recovery decoded every Feed record in the replayed part of the WAL into a
CommitEvent, and ChangeFeed.append then either dropped it (the feed already
held that generation, which is the usual case after a process crash) or
encoded it again. The WAL payload is already the FeedCodec bytes the feed
stores, so recovery now hands it to ChangeFeed.appendEncoded, which reads
the generation from the start of the payload and writes the bytes as they
are.

At scale 1 of the benchmark this was about a quarter of the bytes
allocated while replaying the log. Recovery.recover no longer needs the
FeedCodec.
@venkat1701
venkat1701 merged commit b338f09 into main Oct 14, 2026
@venkat1701
venkat1701 deleted the perf/faster-recovery branch October 14, 2026 13:09
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