JingsongLi commented on code in PR #10845:
URL: https://github.com/apache/paimon/pull/10845#discussion_r4456632443


##########
paimon-core/src/main/java/org/apache/paimon/utils/FileSystemBranchManager.java:
##########
@@ -224,6 +240,261 @@ public void fastForward(String branchName) {
         }
     }
 
+    /**
+     * Fast-forwards by appending when the current branch is a prefix of the 
branch: its snapshots
+     * from the branch's earliest one (or its own earliest, if later) to its 
latest are the
+     * branch's, and so are its schemas. Nothing is deleted and files are 
written in ascending id
+     * order, so a failure leaves the current branch at one of the branch's 
snapshots and a retry
+     * continues from there.
+     *
+     * @return false if the current branch is not a prefix of the branch
+     */
+    private boolean tryAppendFromBranch(String branchName, long 
branchEarliestId, Lock lock)
+            throws IOException {
+        SnapshotManager branchSnapshotManager = 
snapshotManager.copyWithBranch(branchName);
+        SchemaManager branchSchemaManager = 
schemaManager.copyWithBranch(branchName);
+        // List rather than trust the LATEST hint: an interrupted replace by 
an older version copies
+        // the branch's hint before the snapshots it names.
+        Map<Long, Long> sizes = snapshotSizes(snapshotManager);
+        Map<Long, Long> branchSizes = snapshotSizes(branchSnapshotManager);
+        if (sizes.isEmpty() || branchSizes.isEmpty()) {
+            return false;
+        }
+        long latestId = Collections.max(sizes.keySet());
+        long branchLatestId = Collections.max(branchSizes.keySet());
+        if (latestId < branchEarliestId
+                || latestId > branchLatestId
+                || !samePrefix(
+                        sizes, branchSizes, branchSnapshotManager, 
branchEarliestId, latestId)
+                || !sameSchemas(branchSchemaManager)) {
+            return false;
+        }
+
+        long latestSchemaId = Collections.max(schemaManager.listAllIds());
+        for (long schemaId : sorted(branchSchemaManager.listAllIds())) {
+            if (schemaId > latestSchemaId) {
+                copyAtomically(
+                        lock,
+                        branchSchemaManager.toSchemaPath(schemaId),
+                        schemaManager.toSchemaPath(schemaId));
+            }
+        }
+        copySnapshots(branchSnapshotManager, latestId + 1, branchLatestId, 
lock);
+        copyBranchTags(branchName, lock);
+        commitHint(lock, HintFileUtils.LATEST, branchLatestId);
+        return true;
+    }
+
+    private Map<Long, Long> snapshotSizes(SnapshotManager manager) throws 
IOException {
+        Map<Long, Long> sizes = new HashMap<>();
+        for (FileStatus status :
+                listVersionedFileStatus(fileIO, manager.snapshotDirectory(), 
SNAPSHOT_PREFIX)
+                        .collect(Collectors.toList())) {
+            String version = 
status.getPath().getName().substring(SNAPSHOT_PREFIX.length());
+            try {
+                sizes.put(Long.parseLong(version), status.getLen());
+            } catch (NumberFormatException ignored) {
+                // not a snapshot file, for example a temporary file of a 
Python writer
+            }
+        }
+        return sizes;
+    }
+
+    /**
+     * Whether the current branch has every snapshot from {@code from} (or its 
earliest, if later)
+     * to {@code to}, each with the branch's content. Sizes are compared first 
because they come
+     * with the listing; equal sizes do not mean equal snapshots.
+     */
+    private boolean samePrefix(
+            Map<Long, Long> sizes,
+            Map<Long, Long> branchSizes,
+            SnapshotManager branchSnapshotManager,
+            long from,
+            long to)
+            throws IOException {
+        long start = Math.max(from, Collections.min(sizes.keySet()));
+        for (long id = start; id <= to; id++) {
+            Long size = sizes.get(id);
+            if (size == null || !size.equals(branchSizes.get(id))) {
+                return false;
+            }
+        }
+        for (long id = start; id <= to; id++) {
+            if (!sameContent(
+                    snapshotManager.snapshotPath(id),
+                    branchSnapshotManager.snapshotPath(id),
+                    Snapshot::fromJson)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /** Whether the current branch's schemas are the branch's schemas up to 
the current latest. */
+    private boolean sameSchemas(SchemaManager branchSchemaManager) throws 
IOException {
+        Set<Long> ids = new HashSet<>(schemaManager.listAllIds());
+        if (ids.isEmpty()) {
+            return false;
+        }
+        long latestId = Collections.max(ids);
+        Set<Long> branchIds =
+                branchSchemaManager.listAllIds().stream()
+                        .filter(id -> id <= latestId)
+                        .collect(Collectors.toSet());
+        if (!ids.equals(branchIds)) {
+            return false;
+        }
+        for (long id : ids) {
+            if (!sameContent(
+                    schemaManager.toSchemaPath(id),
+                    branchSchemaManager.toSchemaPath(id),
+                    TableSchema::fromJson)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Replaces the current branch's snapshots and tags from the branch's 
earliest snapshot onwards,
+     * and its schemas, with the branch's. Schemas are replaced file by file 
instead of deleted up
+     * front, so a failure never leaves the remaining snapshots without their 
schemas.
+     */
+    private void replaceFromBranch(String branchName, long earliestSnapshotId, 
Lock lock)
+            throws IOException {
+        SnapshotManager branchSnapshotManager = 
snapshotManager.copyWithBranch(branchName);
+        SchemaManager branchSchemaManager = 
schemaManager.copyWithBranch(branchName);
+        long earliestSchemaId = 
branchSnapshotManager.snapshot(earliestSnapshotId).schemaId();
+
+        // Delete tags first: a retry decides from snapshots and schemas alone 
whether to append,
+        // which keeps main's tags, so the tags must be gone before either 
changes.
+        List<Path> deletePaths =
+                new ArrayList<>(
+                        tagManager.tagPaths(
+                                path -> Tag.fromPath(fileIO, path).id() >= 
earliestSnapshotId));
+        deletePaths.addAll(snapshotManager.snapshotPaths(id -> id >= 
earliestSnapshotId));
+        lock.ensureValid();
+        snapshotManager.deleteLatestHint();
+        for (Path path : deletePaths) {
+            delete(lock, path);
+        }
+        // Main expired its own snapshots up to its EARLIEST hint; the 
branch's snapshots below
+        // that hint become main's. Move the hint before copying them, so that 
a retry after a
+        // failed copy, which appends the rest, finds it already moved.
+        Long earliestHint =
+                HintFileUtils.readHint(
+                        fileIO, HintFileUtils.EARLIEST, 
snapshotManager.snapshotDirectory());
+        if (earliestHint != null && earliestHint > earliestSnapshotId) {
+            commitHint(lock, HintFileUtils.EARLIEST, earliestSnapshotId);
+        }
+
+        List<Long> branchSchemaIds = sorted(branchSchemaManager.listAllIds());
+        for (long id : branchSchemaIds) {
+            replaceAtomically(
+                    lock, branchSchemaManager.toSchemaPath(id), 
schemaManager.toSchemaPath(id));
+        }
+        for (long id : schemaManager.listAllIds()) {
+            if (id >= earliestSchemaId && !branchSchemaIds.contains(id)) {
+                delete(lock, schemaManager.toSchemaPath(id));
+            }
+        }
+
+        // List rather than trust the branch's LATEST hint, as the append path 
does.
+        long branchLatestId = 
Collections.max(snapshotSizes(branchSnapshotManager).keySet());

Review Comment:
   [P1] Fix the source snapshot upper bound before copying schemas
   
   The replacement path copies the finite `branchSchemaIds` first, then lists 
the source's latest snapshot here. A source-branch writer can publish a new 
schema and commit between those steps; the held lock protects main and does not 
freeze that source branch. I reproduced this with the real divergent-main 
fixture and a one-shot FileIO callback during the first source-schema copy: the 
callback adds `new_col` on `b1` and commits a row through its normal writer. 
`fastForward("b1")` succeeds, but a newly loaded main table then fails to read 
with `schema/schema-2 (No such file or directory)`, because the new snapshot 
was included without schema 2. The old ordering copied snapshots before schemas 
and did not expose this window. Capturing a fixed source snapshot upper bound 
before copying its required schemas makes the same test pass; the test only 
requires the original four source rows and does not require inclusion of the 
concurrent row. Please use a coherent fixed source boundary instea
 d of extending it after schema copying.



-- 
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