Repository navigation
feat(engine): size the checkpoint threshold from the store and set it with CHECKPOINT AT - #102
Merged
Merged
Conversation
EngineOptions.defaults() no longer fixes the background checkpoint threshold at 256 MiB. With checkpointWalBytes set to AUTOMATIC_CHECKPOINTS (0, the new default), the maintenance thread checkpoints once the WAL or the pages written since the last checkpoint pass a quarter of the live bytes in the data segments, held between 2 MiB and 256 MiB. A small store now checkpoints after a few MiB of log instead of replaying everything since it was created. An explicit withCheckpointWalBytes value still wins. withAutomaticCheckpoints() goes back to the rule. StorageEngine.checkpointThreshold() reports the value in use and where it came from.
StorageEngine.checkpointAt(OptionalLong) stores a threshold in a new engine slot, settings (slot 8, schema 15), through a system transaction on main, the same way interned symbols are written. It is logged and checkpointed like any other commit, so it survives a restart and a crash, and it is read back on open before the maintenance thread starts. An empty value removes it and the engine goes back to the configured value or the automatic rule. A stored value wins over EngineOptions.checkpointWalBytes, which wins over the rule. It takes effect on the next maintenance pass. The slot has no root until a threshold is stored, so stores that never set one look the same on disk as before. An older build cannot open a store that has one, because it does not know slot 8.
…eckpoint threshold The maintenance thread only looked at the WAL and page volume every maintenanceInterval (500 ms by default), so a store that passed its checkpoint threshold could keep committing for up to half a second before a checkpoint started. A crash in that window replayed all of it. After each commit, TransactionManager now runs an afterCommit hook. StorageEngine compares the WAL bytes appended and the page bytes written with two marks it computes at the end of every maintenance pass, from the threshold then in force. When a commit passes either mark it unparks the maintenance thread, which decides and runs the checkpoint as before. The check is two counter reads and two volatile reads. A flag set by the first commit to pass a mark and cleared at the end of the next maintenance pass keeps later commits from waking the thread again while that checkpoint is pending or running.
CHECKPOINT AT <size> stores a threshold in the database through StorageEngine.checkpointAt, and CHECKPOINT AT AUTO removes it. Sizes take a unit, in any case and with or without a space: B, KB, MB, GB or TB (KiB, MiB, GiB and TiB are accepted too). Units are powers of 1024, the same as checkpoint_wal_mb. A size without a unit, a size that is not a whole number of bytes, and zero or negative sizes are syntax errors. The statement needs the ADMIN role, like CHECKPOINT and COMPACT, and log_statement = ddl logs it. STATS shows the threshold in use and whether it is automatic, stored in the database or set by configuration.
…reshold comes from checkpoint_wal_mb now defaults to auto, which leaves the threshold to the engine's rule. A number is still read as megabytes and must be between 1 and the same upper limit as the other megabyte settings; any other value is rejected at startup with a message that names the setting. A value stored with CHECKPOINT AT still wins over either. GET /api/stats reports the threshold in use as checkpointWalBytes and adds checkpointSource (automatic, database or configuration). The dashboard shows both under "Checkpoint after".
configuration.md gives checkpoint_wal_mb's new default and the order in which the threshold is chosen. maintenance.md has the rule, a table of thresholds by store size, why it is a quarter of the live data with a 2 MiB floor, and how a commit that passes the threshold wakes the maintenance thread. The HQL reference documents CHECKPOINT AT, the two new STATS rows, and that older builds cannot open a database that has run it. The slot tables list the settings slot, and the Studio, logging, security, recovery and architecture pages follow the new names.
venkat1701
force-pushed
the
feat/dynamic-checkpoints
branch
from
October 14, 2026 13:39
344dde3 to
f89b1cf
Compare
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.
The background checkpoint used to wait for 256 MiB of WAL (or 256 MiB of pages written) since the last one. A small store never got there. In the scale-1 crash test the child wrote a 4.46 MB WAL and was killed before any checkpoint, so opening the crashed store replayed every commit from an empty catalog.
The threshold is now worked out from the size of the store by default, and it can be set from HQL.
The automatic rule
With nothing set, the threshold is a quarter of the live bytes in the data segments, held between 2 MiB and 256 MiB (
CheckpointThreshold.automatic):Why a quarter. Recovery replays every commit since the last checkpoint and then writes the pages those commits changed, so the log it has to redo should stay small next to the store. A checkpoint writes the pages changed since the previous one, which is at most the whole store. Checkpointing once the log reaches a quarter of the live data keeps replay to a quarter of the store's size, and keeps checkpoint page writes to at most about four bytes per byte of log even if every checkpoint rewrites the whole store. A store of 1 GiB or more keeps the old 256 MiB.
Why a 2 MiB floor. Without one, a nearly empty store would pay the fixed cost of a checkpoint (catalog image, reference journal, several fsyncs) every few hundred kilobytes of log. I also tried 4 MiB. The scale-1 crash child writes 4.46 MB of WAL in total, so with 4 MiB it passes the floor only in its last commit or two and is killed before that checkpoint is published: in 10 runs it replayed exactly as much as main. 2 MiB is low enough for that case.
The rule is evaluated on every maintenance pass, so the threshold grows with the store. The write buffer trigger (
write_buffer_mb / 2) and the reference count trigger are unchanged.Waking on commit
The maintenance thread used to look at the WAL only every
maintenanceInterval(500 ms). Now a commit that passes the threshold wakes it. At the end of each maintenance pass,StorageEngine.armWaketurns the threshold into two marks, one onwal.bytesAppended()and one onpages.bytesWritten().TransactionManagerruns a newafterCommithook, andStorageEngine.wakeIfDuecompares the two counters with the marks: two atomic reads and two volatile reads per commit. When either mark is passed it unparks the maintenance thread, which decides and runs the checkpoint as before. The commit never runs the checkpoint itself. The first commit past a mark sets a flag, and the next maintenance pass clears it, so commits that follow while that checkpoint is pending or running don't wake the thread again. This is its own commit.CHECKPOINT AT
CHECKPOINT AT 64MB; CHECKPOINT AT 1 gb; CHECKPOINT AT 512KB; CHECKPOINT AT AUTO;Units are B, KB, MB, GB, TB (KiB, MiB, GiB, TiB also accepted), any case, space optional, powers of 1024 like
checkpoint_wal_mb. A size with no unit, a fraction of a byte, zero or a negative size is a syntax error. The statement needs ADMIN, the same check asCHECKPOINTandCOMPACT(Executor.administrative), andlog_statement = ddllogs it.The value is kept in the database, not in a file. It goes into a new engine slot,
settings(slot 8, tree schema 15, key 1), through a system transaction on main, the same wayDictionary.internwrites symbols. So it is in the WAL, in the next checkpoint and in replay like any other commit.StorageEnginereads it on open before the maintenance thread starts, andcheckpointAtupdates the in-memory value right after the commit, so a change applies without a restart.CHECKPOINT AT AUTOdeletes the key.Order of precedence: a value stored with
CHECKPOINT AT, then an explicitcheckpoint_wal_mb(orEngineOptions.withCheckpointWalBytes), then the rule.Configuration and API
checkpoint_wal_mbdefaults toauto. A number is still megabytes and is now range-checked like the other megabyte settings (1 to 16,777,216). Anything else fails at startup with a message naming the setting.EngineOptions.defaults()now usesAUTOMATIC_CHECKPOINTS(0).withCheckpointWalBytes(n)still fixes the threshold, andwithAutomaticCheckpoints()goes back to the rule. Negative values are rejected.withCheckpointWalBytes(0)changes meaning. It used to checkpoint on every maintenance pass that saw any new WAL. It now means automatic.StorageEngine.checkpointThreshold()returns the bytes in use and the source (AUTOMATIC,DATABASE,CONFIGURATION).StorageEngine.checkpointAt(OptionalLong)stores or clears the value.STATSgainscheckpoint after wal bytesandcheckpoint threshold.GET /api/statsreports the threshold in use ascheckpointWalBytesand addscheckpointSource. The dashboard's "Checkpoint after" row shows the size followed by "automatic", "set by CHECKPOINT AT" or "set by checkpoint_wal_mb".Compatibility
Older builds cannot open a database that has run
CHECKPOINT AT <size>. They fail withno slot registered with id 8, whether the value has been checkpointed or is still only in the WAL; I checked both against main's engine jar. Nothing has been released yet, so this was accepted. The HQL reference says so where the statement is described.There is no storage format change and
FORMATstays at 8. The new slot has no root until a value is stored, so a database that never ranCHECKPOINT AThas nothing new on disk, and main opens it.Measurements
A/B, main (
0c847f7) against this branch, same probe on both sides. Byte counts only; the laptop was running other builds, so no timing claims.Crash: the benchmark's crash child (
Comparison crash --store hstore --scale 1 --durability async --history 64) killed with SIGKILL after 25,000 acknowledged nodes, then the crashed directory opened with the benchmark's options. "WAL replayed" is the LSN of the first checkpoint after open minus the checkpoint LSN found in the crashed catalog. 10 runs each:All 10 branch runs replayed less than main: 49% of the bytes and 13 of 28 commits. Before the wake-up, with the same floor, only 4 of 10 runs did, because a checkpoint waited for the next 500 ms pass and the kill often came first. With the wake-up the checkpoint starts at the commit that passes 2 MiB, so every run publishes the same checkpoint.
Ingest cost:
HStoreStore.ingestNodestheningestEdgesat scale 1, batches of 1,000, async durability, then one checkpoint, as in the benchmark's ingest workloads. Bytes arewalBytes + dataBytesWrittenfromEngineStats. Medians of 5 runs each (range in brackets):The extra checkpoints cost about 10.9 MB of page writes at scale 1, 21% more than main writes during ingest. That is the price of the small floor on a small store. The threshold grows with the store, so at 1 GiB of live data and above checkpoints come at the old 256 MiB. With the wake-up, checkpoints start at the same points in every run, which is why the branch's page bytes don't vary.
Tests
CheckpointThresholdTest(engine):halt();AUTOclears it;WAL_SYNC,CHECKPOINT_PAGES,CHECKPOINT_CATALOGandCHECKPOINT_JOURNAL;Long.MAX_VALUE.QueryLanguageTest:CHECKPOINT ATwith64MB,64 mb,1GB,512KB,1.5 GiB,4096 b,AUTO, and ten malformed forms. The value survives closing and reopening the database and shows inSTATS.SecurityTest: a WRITER gets the ADMIN error and the threshold stays automatic.ServerTest:checkpoint_wal_mbunset,AUTO,64and512, and the bad values0,-1, too large andsometimes.StudioTest:/api/statsshowsautomatic, thendatabaseand 64 MiB afterCHECKPOINT AT 64MB.Each was run once against the code with its part of the change reverted, and each failed:
afterCommithook removedCheckpointAtleft out ofadministrativeSetting,ServerConfigandStudio./mvnw -q installpasses on every commit: 242 tests, 13 of them new.