Repository navigation
Conversation
…fails A checkpoint that failed after a commit was raised from the statement that committed, so callers that retry failed writes applied them twice. Buffer pool pressure during the checkpoint, or another connection's open write transaction outlasting the checkpoint's wait, was enough. checkpointNoLock throws CheckpointException only with the checkpoint rolled back or not started, or with the WAL poisoned. commit() now drops that failure for the automatic checkpoint: the commit is in the WAL, and the next write commit retries. A poisoned WAL, any other exception, and the forced checkpoint after COPY FROM or CREATE INDEX on a large ART, whose data is not in the WAL, still fail the statement. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A COPY FROM or a CREATE INDEX on a large ART writes its data without the WAL and relies on the checkpoint it forces at commit. When that checkpoint failed, the error still said the committed data was durable and would be recovered on restart, but a crash before a later successful checkpoint loses it. The forced-path error now says the changes may be lost on restart unless a later CHECKPOINT succeeds. The tests check that such a CHECKPOINT persists the COPY's rows and the index. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| // is in the WAL and the next write commit retries. COPY FROM and CREATE INDEX on a large | ||
| // ART rely on their forced checkpoint instead of the WAL, so that failure still fails the | ||
| // statement. | ||
| if (shouldForceCheckpoint || wal.isPoisoned()) { |
There was a problem hiding this comment.
Low (non-blocking): when this guard is false the auto-checkpoint failure is swallowed with no log, so the statement succeeds silently and a persistently failing auto-checkpoint is invisible until WAL growth surfaces it. Consider a warn-level log here (reason + WAL size).
There was a problem hiding this comment.
The core has no logger (spdlog is test/benchmark only), so it's one stderr line with the WAL size and the reason, like the Warning: in ice_disk_utils.h. It prints once per failed checkpoint, so a persistent failure repeats it on each write commit. A poisoned WAL also reports its reason on the next write.
| } | ||
| } catch (const CheckpointException& e) { | ||
| // Unless the WAL is poisoned, a failed automatic checkpoint left nothing behind: the commit | ||
| // is in the WAL and the next write commit retries. COPY FROM and CREATE INDEX on a large |
There was a problem hiding this comment.
Nit: 'the next write commit retries' is only true when canAutoCheckpoint triggers again (threshold-gated; always-true only under the test's checkpoint_threshold=0). Suggest 'a later write commit retries'.
There was a problem hiding this comment.
Done in b9c44b4: "a rolled-back checkpoint is retried by a later write commit".
|
One open question from review (two inline nits posted separately): Question — cleanup-path contract: |
When a checkpoint failed and its rollback threw too, the free-page bookkeeping and the WAL rotation could stay half undone, and the engine kept accepting writes on that state. Checkpointer::rollback now releases the checkpoint lock files and poisons the WAL, as a failed cleanup already does, so the database refuses writes until restart. A beforeRollback hook, added after the existing virtual hooks, lets the test make the rollback fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A failed automatic checkpoint still failed the committed statement when it poisoned the WAL or when its cleanup failed, although the commit was in the WAL in both cases, so a client retrying after a restart applied the write twice. commit() now drops every failure of the automatic checkpoint and prints one warning to stderr with the WAL size and the reason. A rolled-back checkpoint is retried by a later write commit, and a poisoned WAL fails the next write, which really did not commit. The forced checkpoint after COPY FROM or CREATE INDEX on a large ART still fails the statement. WAL::isPoisoned() is removed again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
No, so the approach changed in two commits on top. Before. The first version dropped a failed automatic checkpoint only when it was rolled back, and still failed the statement when the WAL was poisoned or cleanup failed. The argument was that a poisoned WAL refuses writes, so a retry can't apply twice. That holds only until a restart: both tests reopen and find the row, so a client that restarts and retries applies it twice. Now. b9c44b4 drops every failure of the automatic checkpoint and prints one stderr warning with the WAL size and the reason. The commit is in the WAL in all of these cases. A poisoned WAL fails the next write instead, which really did not commit, so retrying it is correct. The forced checkpoint after New guard. Dropping everything would have made one case silent: a rollback that throws midway left the engine accepting writes on a half-undone checkpoint. 82135e1 makes Stress. A loop through the C API with failing checkpoints, kills and the same seeds on both builds: retries applied a write twice 372 times on main and never here. Committed writes went missing after a reopen in 33 of 300 rounds on main and 37 here (paired p = 0.54), all through #1304's pinned-checkpoint truncation; the repro is in my comment there. DuckDB. 2.0.0 keeps failing the statement. With a checkpoint after every commit and an I/O error during it, the INSERT raises |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d the WAL A COPY or large CREATE INDEX whose checkpoint failed reported that its changes are lost on restart unless a later CHECKPOINT succeeds. When that checkpoint failed after its record may have reached storage, or its rollback failed, the WAL is poisoned: no later CHECKPOINT can run before the restart, and recovery then keeps the changes only if the record reached storage. The error and docs/checkpoint_recovery.md now say so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This work was produced with the help of language models.
Fixes #1199.
Land with or after #1389: with this PR a checkpoint refused at
max_db_sizeno longer fails its statement, so without #1389 the next statement segfaults (#1386).In the issue's script,
COMMITraisedTransaction committed successfully, but the post-commit checkpoint failed. The committed data is durable and will be recovered on restart: Buffer manager exception: Unable to allocate memory! ...while another connection held the buffer pool full, and the retry stored a second relationship. A checkpoint that timed out waiting for another connection's open write transaction failed the write the same way.Fix. A failed automatic checkpoint no longer fails the statement that committed, since the commit is already in the WAL and the error only invited a retry that applied it twice.
commit()drops the failure and prints one stderr warning with the WAL size and the reason; the core has no logger. A rolled-back checkpoint is retried by a later write commit. A checkpoint that poisoned the WAL, past its record or in cleanup, now fails the next write instead, which really did not commit. The forced checkpoint afterCOPY FROMorCREATE INDEXon a large ART still fails the statement, since that data is not in the WAL.Failed rollback. A checkpoint whose rollback threw left the engine accepting writes on a half-undone checkpoint.
Checkpointer::rollbacknow poisons the WAL, as a failed cleanup already does. AbeforeRollbackhook, added after the existing virtual hooks, lets the test make it fail.Forced checkpoint message. It promised the data "is durable and will be recovered on restart". On 0.21.2, a
COPYof 1,000 rels whose checkpoint failed this way left none after a kill and reopen. The message now says the changes "may be lost on restart unless a later CHECKPOINT succeeds". If that checkpoint poisoned the WAL instead, after its record may have reached storage or in a failed rollback, no laterCHECKPOINTcan run, so the message says the database refuses writes until restart and that the changes survive it only if the checkpoint's record reached storage.Tests.
PostCommitCheckpointFailureTestcheckpoints after every commit. Five of its tests fail onmain: with the checkpoint failing, aCREATEand aCOMMITsucceed with the warning and survive a reopen; the next write's checkpoint empties the WAL; a checkpoint blocked by another open write transaction no longer fails the write; and a checkpoint that poisons the WAL, or fails in cleanup, leaves the write successful and fails the next one. Two more keep the forced path failing afterCOPYandCREATE INDEX, where a laterCHECKPOINTkeeps the rows and the index.ForcedCheckpointFailingAfterItsRecordRefusesWritesfails the forced checkpoint after its record: the error says writes are refused, not that a laterCHECKPOINThelps, and a reopen has the rows.FailedRollbackPoisonsWALhas a rollback fail: reads continue, the next write is refused, and a reopen recovers all 100 rows. Each test fails when its guard is removed. The issue's script now printsattempt 1: okwith one relationship. Full ctest: 3145/3145.