[core] Fall back to listing when the LATEST hint points to an expired snapshot - #10323
dev-donghwan wants to merge 2 commits into
Conversation
… snapshot findLatest trusted the LATEST hint whenever snapshot-(hint + 1) did not exist, without checking that the hinted snapshot itself still exists. When the hint stays behind across more commits than the retention window, expiration removes both the hinted snapshot and the next one, and findLatest keeps returning the expired id. Commits then fail before they can rewrite the hint, so the table never recovers by itself. Also require the hinted snapshot to exist, as findEarliest already does, and fall back to listing otherwise.
JingsongLi
left a comment
There was a problem hiding this comment.
-1 here will another io.
…issing Address review: do not add an exists check to findLatest, which would cost another IO on every lookup. Keep findLatest as it is, and list the snapshot directory only in latestSnapshotFromFileSystem after the hinted snapshot turns out to be missing and the hint still returns the same id. The commit path then finds the real latest snapshot and rewrites the hint, so the table recovers without extra IO on the normal path.
|
Thanks for the review, @JingsongLi! You're right, I overlooked that I've reverted the Could you take another look when you have time? |
|
Reviewed head [P1] Recover commit-user deduplication before allowing the commit to proceed (
This is reachable in Flink's batch committer restart path: I reproduced this with an append table ( Please make the commit-user lookup recover from the missing hint as well before concluding that a commit is new, and add replay coverage for both conflict-check modes. A failure-only fallback can preserve the normal-path I/O cost. There is also a remaining scope gap: Validation: |
|
Thanks for the detailed review, @JingsongLi. You're right about both points. I reproduced the duplicate commit on I think I took the wrong approach with this PR, so before pushing anything I'd like to ask for your view as the original author of the hint files. The current direction is to fix the read side. To continue with it, every caller that relies on the latest snapshot id would have to handle a hint that points to an expired snapshot.
Fixing each of these one by one would make the change much larger, and each fix could bring another side effect like these. So instead of the read side, I looked at the side that deletes snapshots. The stale state only appears when expiration deletes the snapshot the Proposal Snapshot expiration does not expire the snapshot the
If hint writes keep failing In our incident, hint writes failed because of a broken TaskManager, which Paimon cannot prevent. What Paimon can avoid is turning that into a permanent outage: the TaskManager problem lasted about 6 minutes, while the table stayed stuck for two days until I ran 30 commits with every Today the only trace of this is the generic I also tried letting expiration rewrite the Measured comparison
All cells come from running the same probe on the four variants, except the rollback procedures, which are from reading the code. 1. A table whose
2. A table that is already in the stale state
With B in place, A only runs on tables that are already stale. There it brings back commits but turns other failures into silently wrong results, and it does not fix the rest. So I'd go with B alone. Tests The tests produce the stale hint through real commits with Does this direction match how you see the hint files, and would you like the extra warning in the commit retry path as part of this PR? If so, I'll update the PR accordingly. |
Related: #10324 (merged) fixes one way the
LATESThint falls behind (a snapshot rename that throws but has succeeded).Purpose
A stale
LATESThint should be harmless, because the hint is only a cache. Today it can leave a table permanently unable to commit or read, even though all newer snapshot files are still there.The problem
HintFileUtils.findLatestreads the hintNand returns it ifsnapshot-(N + 1)does not exist. It never checks thatsnapshot-Nitself exists:A missing
N + 1can mean two things: it has not been created yet, or it was created and then expired.findLatestassumes the first. If the hint stops moving while commits and expiration go on, expiration eventually removes bothNandN + 1:findLatestsees thatsnapshot-11is missing and returns 10, which no longer exists, instead of 30. The directory listing is never reached.The table cannot recover on its own:
LATEST.latestSnapshot(), which gets 10 fromfindLatestand fails withSnapshot file .../snapshot-10 does not exist. It might have been expired by other jobs operating on this table. ....The retry added in feabf2c does not help, because the second
findLatestcall returns the same id. Callers that use onlylatestSnapshotId(), such as expiration and the streaming starting scanners, get the wrong id as well.Why this was safe before
findLatestandfindEarliestwere introduced together in FLINK-26778 (#56). Each hint only guards against the gap its own writer can leave:EARLIESTLATESTsnapshot-Nexistssnapshot-(N + 1)does not existThe review of #56 assumed that
LATEST"is only changed by the commit operation and that value should be precise". A failed hint write also failed the commit. Under that assumption, expiration could never reach the snapshotLATESTpoints to, so checking that it exists was unnecessary.That assumption no longer always holds. Since #5771 (1.3.0), a commit retry that finds its snapshot already written treats the commit as successful. This is the "Check if the commit has been completed" path in
FileStoreCommitImpl. It correctly avoids a duplicate commit, but it does not rewrite the hint. SoLATESTcan now stay behind across many successful commits. Once it falls behind by more than the retention window, the hinted snapshot is expired.Writing the hint on that path would help, but it cannot cover hint writes that keep failing. This PR therefore makes the table recover from a stale hint, whatever made it stale. #10324 separately fixes the case where a snapshot rename throws but has succeeded, which is how the hint fell behind for us.
How we hit this
We run Paimon 1.4.2 on Flink 2.2.1, with a Hive catalog and the warehouse on an S3-compatible object store. The table keeps
snapshot.num-retained.max=20with 30s checkpoints, which is about 6 minutes of snapshots.The trigger was a problem in our own setup. S3A was loaded from
/opt/flink/librather than as a Flink plugin. After a job restart closed the user classloader, S3A copies on a long-lived TaskManager kept succeeding on the server but failing on the client side. Every snapshot rename threw even though the snapshot file was written, and the retry path above reported success without writing the hint.LATESTstayed at the same id for 21 consecutive commits. We have since fixed this by moving S3A into the plugin directory.That trigger alone should have been temporary. The permanent outage came from
findLatest. About 6 minutes after the hint stopped moving, expiration removed the hinted snapshot. From then on, the writer failed inFileStoreCommitImpl.tryCommitand inFileSystemWriteRestore.restoreFiles, and a downstream streaming reader failed withOutOfRangeException. The job restarted about 6,000 times over two days. OverwritingLATESTby hand was the only way out.Any failure that keeps
LATESTfrom moving for longer than the retention window leads to the same state, for example repeated hint write failures, or a filesystem that keeps reporting errors after successful renames. With a smallsnapshot.num-retained.max, that window can be only a few minutes.Change
findLatestis unchanged, so the normal path does not get any extra IO.The fix is in the existing fallback of
SnapshotManager#latestSnapshotFromFileSystem. When reading the hinted snapshot fails withFileNotFoundExceptionandfindLateststill returns the same id, it now lists the snapshot directory to find the real latest snapshot and reads that one. If the listing finds no newer snapshot, it throws as before.NNis the latestNN(no extra IO)N + 1exists (hint slightly behind)findLatestlistsNandN + 1both expiredNfails, retry returnsN, throwsNfails, retry returnsN, list and read the real latestThe commit path (
FileStoreCommitImpl#tryCommit) and the writer restore (FileSystemWriteRestore#restoreFiles) both go through this method. They now find the real latest snapshot, the next commit succeeds and rewrites the hint, and the table recovers by itself. Callers that only uselatestSnapshotId()may still see the stale id until that commit rewrites the hint.Tests
SnapshotManagerTest#testLatestSnapshotWithExpiredLatestHint: the hint points to an expired snapshot while snapshots 5–10 exist.latestSnapshot()threw before this change and returns snapshot 10 after it.StaleLatestHintTest#testCommitAfterLatestHintExpired: drives the real commit and expire paths with aFileIOthat skips theLATESTwrite, then lets hint writes recover. Before this change, commits and reads keep failing after the recovery. After it, the next commit succeeds and refreshes the hint.SnapshotManagerTestcases pass, includingtestLatestSnapshotStillFailsWhenNoNewerSnapshotExists.org.apache.paimon.utils,org.apache.paimon.table.sourceandorg.apache.paimon.catalog, plus the expiration and commit tests inorg.apache.paimon.operation: 822 tests pass.paimon-core