dev-donghwan opened a new pull request, #10323:
URL: https://github.com/apache/paimon/pull/10323

   ### Purpose
   
   A stale `LATEST` hint 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.findLatest` reads the hint `N` and returns it if `snapshot-(N 
+ 1)` does not exist. It never checks that `snapshot-N` itself exists:
   
   ```java
   Long snapshotId = readHint(fileIO, LATEST, dir);
   if (snapshotId != null && snapshotId > 0) {
       long nextSnapshot = snapshotId + 1;
       // it is the latest only there is no next one
       if (!fileIO.exists(file.apply(nextSnapshot))) {
           return snapshotId;
       }
   }
   return findByListFiles(fileIO, Math::max, dir, prefix);
   ```
   
   A missing `N + 1` can mean two things: it has not been created yet, or it 
was created and then expired. `findLatest` assumes the first. If the hint stops 
moving while commits and expiration go on, expiration eventually removes both 
`N` and `N + 1`:
   
   ```
   snapshot-13 ... snapshot-30    (10, 11 and 12 expired)
   LATEST = 10
   ```
   
   `findLatest` sees that `snapshot-11` is missing and returns 10, which no 
longer exists, instead of 30. The directory listing is never reached.
   
   The table cannot recover on its own:
   
   1. Only the commit path writes `LATEST`.
   2. A commit first calls `latestSnapshot()`, which gets 10 from `findLatest` 
and fails with `Snapshot file .../snapshot-10 does not exist. It might have 
been expired by other jobs operating on this table. ...`.
   3. The commit never gets far enough to rewrite the hint, so every later 
commit reads the same stale hint.
   
   The retry added in feabf2c6d does not help, because the second `findLatest` 
call returns the same id. Callers that use only `latestSnapshotId()`, such as 
expiration and the streaming starting scanners, get the wrong id as well.
   
   #### Why this was safe before
   
   `findLatest` and `findEarliest` were introduced together in FLINK-26778 
(apache/flink-table-store#56). Each hint only guards against the gap its own 
writer can leave:
   
   | | `EARLIEST` | `LATEST` |
   |---|---|---|
   | Written by | expiration | commit |
   | Order | delete snapshots, then write hint | create snapshot, then write 
hint |
   | Gap in between | hint points to a deleted snapshot | a newer snapshot 
exists |
   | Check | `snapshot-N` exists | `snapshot-(N + 1)` does not exist |
   
   The 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 snapshot 
`LATEST` points 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. So 
`LATEST` can 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 `findLatest` recover from a stale 
hint, whatever made it stale.
   
   #### 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=20` 
with 30s checkpoints, which is about 6 minutes of snapshots.
   
   The trigger was a problem in our own setup. S3A was loaded from 
`/opt/flink/lib` rather 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. `LATEST` stayed 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 in 
`FileStoreCommitImpl.tryCommit` and in `FileSystemWriteRestore.restoreFiles`, 
and a downstream streaming reader failed with `OutOfRangeException`. The job 
restarted about 6,000 times over two days. Overwriting `LATEST` by hand was the 
only way out.
   
   Any failure that keeps `LATEST` from 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 small 
`snapshot.num-retained.max`, that window can be only a few minutes.
   
   #### Change
   
   `findLatest` now trusts the hint only if `snapshot-(N + 1)` does not exist 
and `snapshot-N` does exist. Otherwise it lists the snapshot directory, as it 
already does when `N + 1` exists:
   
   | State of hint `N` | Before | After |
   |---|---|---|
   | `N` is the latest | `N` | `N` |
   | `N + 1` exists (hint slightly behind) | list | list |
   | `N` and `N + 1` both expired | `N` (missing file) | list, finds the real 
latest |
   
   The `N + 1` check stays, since it is what catches a hint that is only 
slightly behind. The new check is the one `findEarliest` already does, because 
both hints can now be invalidated by expiration. Once `findLatest` returns the 
real latest snapshot, the next commit succeeds and rewrites the hint, so the 
table recovers by itself. `ChangelogManager` uses the same `findLatest` and 
gets the same fix.
   
   The cost is one extra `exists` call when the hint is valid. The `N + 1` 
check runs first, so a hint that is already behind costs nothing extra. No 
listing is added while the hint is valid.
   
   ### Tests
   
   - [x] `SnapshotManagerTest#testLatestSnapshotWithExpiredLatestHint`: the 
hint points to an expired snapshot while snapshots 5–10 exist. 
`latestSnapshotId()` returned 2 before this change and returns 10 after it.
   - [x] `StaleLatestHintTest#testCommitAfterLatestHintExpired`: drives the 
real commit and expire paths with a `FileIO` that skips the `LATEST` write, 
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.
   - [x] All tests under `org.apache.paimon.utils` and 
`org.apache.paimon.table.source`, plus the snapshot expiration tests in 
`org.apache.paimon.operation`: 621 tests pass.
   - [x] Spotless and Checkstyle for `paimon-core`
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to