This is an automated email from the ASF dual-hosted git repository.
JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git
The following commit(s) were added to refs/heads/master by this push:
new edd5d6d19f [core] Preserve failures during manifest rewrite cleanup
(#9270)
edd5d6d19f is described below
commit edd5d6d19fb54ceeaf210700b0533c2bc2a234e4
Author: QuakeWang <[email protected]>
AuthorDate: Thu Aug 20 10:17:56 2026 +0800
[core] Preserve failures during manifest rewrite cleanup (#9270)
---
.../apache/paimon/manifest/ManifestAvroWriter.java | 108 ++-
.../operation/ManifestEntryExternalSort.java | 63 +-
.../operation/ManifestEntryRunMergePlan.java | 130 ++--
.../paimon/operation/ManifestFileBlockMerger.java | 2 +-
.../paimon/operation/ManifestFileLegacyMerger.java | 22 +-
.../paimon/operation/ManifestFileMerger.java | 13 +-
.../operation/ManifestRewriteCleanupTest.java | 778 +++++++++++++++++++++
7 files changed, 992 insertions(+), 124 deletions(-)
diff --git
a/paimon-core/src/main/java/org/apache/paimon/manifest/ManifestAvroWriter.java
b/paimon-core/src/main/java/org/apache/paimon/manifest/ManifestAvroWriter.java
index 16c4b68ab2..e78f273e29 100644
---
a/paimon-core/src/main/java/org/apache/paimon/manifest/ManifestAvroWriter.java
+++
b/paimon-core/src/main/java/org/apache/paimon/manifest/ManifestAvroWriter.java
@@ -33,7 +33,7 @@ import org.apache.paimon.schema.SchemaManager;
import org.apache.paimon.stats.SimpleStats;
import org.apache.paimon.stats.SimpleStatsConverter;
import org.apache.paimon.types.RowType;
-import org.apache.paimon.utils.IOUtils;
+import org.apache.paimon.utils.ExceptionUtils;
import org.apache.paimon.utils.ObjectSerializer;
import org.apache.paimon.utils.PathFactory;
@@ -47,6 +47,8 @@ import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
+import static org.apache.paimon.utils.Preconditions.checkNotNull;
+
/**
* Avro writer for manifest entries.
*
@@ -97,14 +99,19 @@ public final class ManifestAvroWriter implements
AutoCloseable {
currentWriter().write(entry);
afterWrite(1, false);
} catch (IOException | RuntimeException | Error failure) {
- abort();
+ abort(failure);
throw failure;
}
}
public void write(Iterable<? extends ManifestEntry> entries) throws
IOException {
- for (ManifestEntry entry : entries) {
- write(entry);
+ try {
+ for (ManifestEntry entry : entries) {
+ write(entry);
+ }
+ } catch (IOException | RuntimeException | Error failure) {
+ abort(failure);
+ throw failure;
}
}
@@ -113,7 +120,7 @@ public final class ManifestAvroWriter implements
AutoCloseable {
currentWriter().writeEncoded(encodedRecord, metadata);
afterWrite(1, false);
} catch (IOException | RuntimeException | Error failure) {
- abort();
+ abort(failure);
throw failure;
}
}
@@ -124,7 +131,7 @@ public final class ManifestAvroWriter implements
AutoCloseable {
currentWriter().writeRow(row, metadata);
afterWrite(1, false);
} catch (IOException | RuntimeException | Error failure) {
- abort();
+ abort(failure);
throw failure;
}
}
@@ -148,7 +155,7 @@ public final class ManifestAvroWriter implements
AutoCloseable {
currentWriter().writeEncodedBlock(block, metadata);
afterWrite(metadataRecordCount, true);
} catch (IOException | RuntimeException | Error failure) {
- abort();
+ abort(failure);
throw failure;
}
}
@@ -180,7 +187,7 @@ public final class ManifestAvroWriter implements
AutoCloseable {
fileWriter.collectStats(metadata);
afterWrite(copiedRecords, true);
} catch (IOException | RuntimeException | Error failure) {
- abort();
+ abort(failure);
throw failure;
}
}
@@ -228,14 +235,44 @@ public final class ManifestAvroWriter implements
AutoCloseable {
}
public void abort() {
+ Throwable cleanupFailure = abortCollecting(null);
+ if (cleanupFailure != null) {
+ ExceptionUtils.rethrow(cleanupFailure);
+ }
+ }
+
+ /** Aborts this writer and adds cleanup failures as suppressed exceptions.
*/
+ public void abort(Throwable primaryFailure) {
+ abortCollecting(checkNotNull(primaryFailure));
+ }
+
+ private Throwable abortCollecting(@Nullable Throwable primaryFailure) {
if (currentWriter != null) {
- currentWriter.abort();
+ primaryFailure = currentWriter.abortCollecting(primaryFailure,
true);
}
for (Path path : completedPaths) {
- fileIO.deleteQuietly(path);
+ try {
+ fileIO.deleteQuietly(path);
+ } catch (Throwable cleanupFailure) {
+ primaryFailure =
ExceptionUtils.firstOrSuppressed(cleanupFailure, primaryFailure);
+ }
}
completedPaths.clear();
results.clear();
+ return primaryFailure;
+ }
+
+ private static Throwable closeCollecting(
+ @Nullable AutoCloseable closeable, @Nullable Throwable
primaryFailure) {
+ if (closeable == null) {
+ return primaryFailure;
+ }
+ try {
+ closeable.close();
+ } catch (Throwable cleanupFailure) {
+ primaryFailure = ExceptionUtils.firstOrSuppressed(cleanupFailure,
primaryFailure);
+ }
+ return primaryFailure;
}
@Override
@@ -246,7 +283,7 @@ public final class ManifestAvroWriter implements
AutoCloseable {
try {
closeCurrentWriter();
} catch (IOException | RuntimeException | Error failure) {
- abort();
+ abort(failure);
throw failure;
} finally {
closed = true;
@@ -365,6 +402,7 @@ public final class ManifestAvroWriter implements
AutoCloseable {
private boolean levelStatsKnown = true;
private @Nullable RowIdStats rowIdStats = new RowIdStats();
private boolean closed;
+ private boolean aborted;
private FileWriter(Path path) {
this.path = path;
@@ -378,19 +416,13 @@ public final class ManifestAvroWriter implements
AutoCloseable {
avroFileFormat.createBlockWriter(
out, ManifestEntry.MANIFEST_ROW_TYPE,
compression);
} catch (IOException failure) {
- IOUtils.closeQuietly(writer);
- IOUtils.closeQuietly(out);
- if (outputCreated) {
- fileIO.deleteQuietly(path);
- }
- throw new UncheckedIOException(
- "Failed to create manifest Avro writer for " + path,
failure);
+ UncheckedIOException primaryFailure =
+ new UncheckedIOException(
+ "Failed to create manifest Avro writer for " +
path, failure);
+ abortCollecting(primaryFailure, outputCreated);
+ throw primaryFailure;
} catch (RuntimeException | Error failure) {
- IOUtils.closeQuietly(writer);
- IOUtils.closeQuietly(out);
- if (outputCreated) {
- fileIO.deleteQuietly(path);
- }
+ abortCollecting(failure, outputCreated);
throw failure;
}
}
@@ -612,13 +644,31 @@ public final class ManifestAvroWriter implements
AutoCloseable {
}
}
- private void abort() {
- IOUtils.closeQuietly(writer);
+ private Throwable abortCollecting(@Nullable Throwable primaryFailure,
boolean deletePath) {
+ if (aborted) {
+ return primaryFailure;
+ }
+ aborted = true;
+ closed = true;
+ outputBytes = null;
+
+ AvroBlockWriter currentBlockWriter = writer;
writer = null;
- IOUtils.closeQuietly(out);
+ primaryFailure = closeCollecting(currentBlockWriter,
primaryFailure);
+
+ PositionOutputStream currentOut = out;
out = null;
- fileIO.deleteQuietly(path);
- closed = true;
+ primaryFailure = closeCollecting(currentOut, primaryFailure);
+
+ if (deletePath) {
+ try {
+ fileIO.deleteQuietly(path);
+ } catch (Throwable cleanupFailure) {
+ primaryFailure =
+ ExceptionUtils.firstOrSuppressed(cleanupFailure,
primaryFailure);
+ }
+ }
+ return primaryFailure;
}
private void close() throws IOException {
@@ -633,7 +683,7 @@ public final class ManifestAvroWriter implements
AutoCloseable {
out.close();
out = null;
} catch (IOException | RuntimeException | Error failure) {
- abort();
+ abortCollecting(failure, true);
throw failure;
} finally {
closed = true;
diff --git
a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryExternalSort.java
b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryExternalSort.java
index c261389093..00e8e38bcc 100644
---
a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryExternalSort.java
+++
b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryExternalSort.java
@@ -35,6 +35,7 @@ import org.apache.paimon.manifest.ProjectedManifestEntry;
import org.apache.paimon.options.MemorySize;
import org.apache.paimon.sort.BinaryExternalSortBuffer;
import org.apache.paimon.utils.CloseableIterator;
+import org.apache.paimon.utils.ExceptionUtils;
import org.apache.paimon.utils.MutableObjectIterator;
import org.apache.paimon.utils.Pair;
@@ -238,7 +239,8 @@ public class ManifestEntryExternalSort {
}
ManifestAvroWriter writer = manifestFile.createAvroWriter();
- Exception exception = null;
+ List<ManifestFileMeta> files = Collections.emptyList();
+ Throwable primaryFailure = null;
try {
MutableObjectIterator<BinaryRow> iterator =
sortBuffer.sortedIterator();
BinaryRow reuse = new
BinaryRow(sortKey.externalSortRowType().getFieldCount());
@@ -249,16 +251,19 @@ public class ManifestEntryExternalSort {
writer.write(entry.replace(sortKey.binaryManifestRow(row)));
}
entry.clear();
- } catch (Exception e) {
- exception = e;
+ writer.close();
+ files = writer.result();
+ } catch (Throwable failure) {
+ primaryFailure = failure;
} finally {
- if (exception != null) {
- writer.abort();
- throw exception;
+ if (primaryFailure != null) {
+ writer.abort(primaryFailure);
}
- writer.close();
}
- return writer.result();
+ if (primaryFailure != null) {
+ ExceptionUtils.rethrowException(primaryFailure);
+ }
+ return files;
}
private Pair<List<ManifestFileMeta>, List<ManifestFileMeta>>
writeMinorToManifest(
@@ -275,7 +280,9 @@ public class ManifestEntryExternalSort {
CompactFileIdentifierSet matchedEntries = new
CompactFileIdentifierSet();
CompactFileIdentifierSet emittedDeletes = new
CompactFileIdentifierSet();
ReusableIdentifier identifier = new ReusableIdentifier();
- Exception exception = null;
+ Pair<List<ManifestFileMeta>, List<ManifestFileMeta>> files =
+ Pair.of(Collections.emptyList(), Collections.emptyList());
+ Throwable primaryFailure = null;
try {
MutableObjectIterator<BinaryRow> iterator =
sortBuffer.sortedIterator();
BinaryRow reuse = new
BinaryRow(sortKey.externalSortRowType().getFieldCount());
@@ -302,19 +309,37 @@ public class ManifestEntryExternalSort {
newFilesForAbort.addAll(addWriter.result());
deleteWriter.close();
newFilesForAbort.addAll(deleteWriter.result());
- } catch (Exception e) {
- exception = e;
+ files = Pair.of(addWriter.result(), deleteWriter.result());
+ } catch (Throwable failure) {
+ primaryFailure = failure;
} finally {
- identifier.release();
- matchedEntries.release();
- emittedDeletes.release();
- if (exception != null) {
- addWriter.abort();
- deleteWriter.abort();
- throw exception;
+ try {
+ identifier.release();
+ } catch (Throwable cleanupFailure) {
+ primaryFailure =
+ ExceptionUtils.firstOrSuppressed(cleanupFailure,
primaryFailure);
+ }
+ try {
+ matchedEntries.release();
+ } catch (Throwable cleanupFailure) {
+ primaryFailure =
+ ExceptionUtils.firstOrSuppressed(cleanupFailure,
primaryFailure);
+ }
+ try {
+ emittedDeletes.release();
+ } catch (Throwable cleanupFailure) {
+ primaryFailure =
+ ExceptionUtils.firstOrSuppressed(cleanupFailure,
primaryFailure);
+ }
+ if (primaryFailure != null) {
+ addWriter.abort(primaryFailure);
+ deleteWriter.abort(primaryFailure);
}
}
- return Pair.of(addWriter.result(), deleteWriter.result());
+ if (primaryFailure != null) {
+ ExceptionUtils.rethrowException(primaryFailure);
+ }
+ return files;
}
@Override
diff --git
a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergePlan.java
b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergePlan.java
index 1b1d9562a0..618943df18 100644
---
a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergePlan.java
+++
b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestEntryRunMergePlan.java
@@ -38,6 +38,7 @@ import org.apache.paimon.manifest.ManifestFile;
import org.apache.paimon.manifest.ManifestFileMeta;
import org.apache.paimon.manifest.ProjectedManifestEntry;
import org.apache.paimon.utils.CloseableIterator;
+import org.apache.paimon.utils.ExceptionUtils;
import org.apache.paimon.utils.Pair;
import javax.annotation.Nullable;
@@ -71,7 +72,8 @@ final class ManifestEntryRunMergePlan {
List<ManifestFileMeta> mergeToManifest(
ManifestFile manifestFile, List<ManifestFileMeta>
newFilesForAbort) throws Exception {
List<Cursor> cursors = new ArrayList<>(sources.size());
- Exception failure = null;
+ List<ManifestFileMeta> files = Collections.emptyList();
+ Throwable primaryFailure = null;
try {
for (Source.Spec source : sources) {
Cursor cursor = source.open(manifestFile, deletes, minor,
partitions);
@@ -79,31 +81,31 @@ final class ManifestEntryRunMergePlan {
cursor.advance();
}
SelectionTree selectionTree = new SelectionTree(cursors);
- if (selectionTree.winner() < 0) {
- return Collections.emptyList();
- }
- List<ManifestFileMeta> files = writeSelected(selectionTree,
manifestFile);
- newFilesForAbort.addAll(files);
- return files;
- } catch (Exception e) {
- failure = e;
- throw e;
+ if (selectionTree.winner() >= 0) {
+ files = writeSelected(selectionTree, manifestFile);
+ newFilesForAbort.addAll(files);
+ }
+ } catch (Throwable failure) {
+ primaryFailure = failure;
} finally {
try {
closeCursors(cursors);
- } catch (Exception closeFailure) {
- if (failure == null) {
- throw closeFailure;
- }
- failure.addSuppressed(closeFailure);
+ } catch (Throwable cleanupFailure) {
+ primaryFailure =
ExceptionUtils.firstOrSuppressed(cleanupFailure, primaryFailure);
}
}
+ if (primaryFailure != null) {
+ ExceptionUtils.rethrowException(primaryFailure);
+ }
+ return files;
}
Pair<List<ManifestFileMeta>, List<ManifestFileMeta>> mergeMinorToManifest(
ManifestFile manifestFile, List<ManifestFileMeta>
newFilesForAbort) throws Exception {
List<Cursor> cursors = new ArrayList<>(sources.size());
- Exception failure = null;
+ Pair<List<ManifestFileMeta>, List<ManifestFileMeta>> files =
+ Pair.of(Collections.emptyList(), Collections.emptyList());
+ Throwable primaryFailure = null;
try {
for (Source.Spec source : sources) {
Cursor cursor = source.open(manifestFile, deletes, minor,
partitions);
@@ -111,33 +113,31 @@ final class ManifestEntryRunMergePlan {
cursor.advance();
}
SelectionTree selectionTree = new SelectionTree(cursors);
- if (selectionTree.winner() < 0) {
- return Pair.of(Collections.emptyList(),
Collections.emptyList());
- }
- Pair<List<ManifestFileMeta>, List<ManifestFileMeta>> files =
- writeMinorSelected(selectionTree, manifestFile, deletes);
- newFilesForAbort.addAll(files.getLeft());
- newFilesForAbort.addAll(files.getRight());
- return files;
- } catch (Exception e) {
- failure = e;
- throw e;
+ if (selectionTree.winner() >= 0) {
+ files = writeMinorSelected(selectionTree, manifestFile,
deletes);
+ newFilesForAbort.addAll(files.getLeft());
+ newFilesForAbort.addAll(files.getRight());
+ }
+ } catch (Throwable failure) {
+ primaryFailure = failure;
} finally {
try {
closeCursors(cursors);
- } catch (Exception closeFailure) {
- if (failure == null) {
- throw closeFailure;
- }
- failure.addSuppressed(closeFailure);
+ } catch (Throwable cleanupFailure) {
+ primaryFailure =
ExceptionUtils.firstOrSuppressed(cleanupFailure, primaryFailure);
}
}
+ if (primaryFailure != null) {
+ ExceptionUtils.rethrowException(primaryFailure);
+ }
+ return files;
}
static List<ManifestFileMeta> writeSelected(
SelectionTree selectionTree, ManifestFile manifestFile) throws
Exception {
ManifestAvroWriter writer = manifestFile.createAvroWriter();
- Exception failure = null;
+ List<ManifestFileMeta> files = Collections.emptyList();
+ Throwable primaryFailure = null;
try {
int winner;
while ((winner = selectionTree.winner()) >= 0) {
@@ -152,16 +152,19 @@ final class ManifestEntryRunMergePlan {
writeCurrent(writer, cursor);
selectionTree.update(winner, cursor.advance());
}
- } catch (Exception e) {
- failure = e;
+ writer.close();
+ files = writer.result();
+ } catch (Throwable failure) {
+ primaryFailure = failure;
} finally {
- if (failure != null) {
- writer.abort();
- throw failure;
+ if (primaryFailure != null) {
+ writer.abort(primaryFailure);
}
- writer.close();
}
- return writer.result();
+ if (primaryFailure != null) {
+ ExceptionUtils.rethrowException(primaryFailure);
+ }
+ return files;
}
private static Pair<List<ManifestFileMeta>, List<ManifestFileMeta>>
writeMinorSelected(
@@ -171,7 +174,9 @@ final class ManifestEntryRunMergePlan {
ManifestAvroWriter deleteWriter = manifestFile.createAvroWriter();
CompactFileIdentifierSet matchedEntries = new
CompactFileIdentifierSet();
CompactFileIdentifierSet emittedDeletes = new
CompactFileIdentifierSet();
- Exception failure = null;
+ Pair<List<ManifestFileMeta>, List<ManifestFileMeta>> files =
+ Pair.of(Collections.emptyList(), Collections.emptyList());
+ Throwable primaryFailure = null;
try {
int winner;
while ((winner = selectionTree.winner()) >= 0) {
@@ -203,18 +208,29 @@ final class ManifestEntryRunMergePlan {
}
addWriter.close();
deleteWriter.close();
- } catch (Exception e) {
- failure = e;
+ files = Pair.of(addWriter.result(), deleteWriter.result());
+ } catch (Throwable failure) {
+ primaryFailure = failure;
} finally {
- matchedEntries.release();
- emittedDeletes.release();
- if (failure != null) {
- addWriter.abort();
- deleteWriter.abort();
- throw failure;
+ try {
+ matchedEntries.release();
+ } catch (Throwable cleanupFailure) {
+ primaryFailure =
ExceptionUtils.firstOrSuppressed(cleanupFailure, primaryFailure);
+ }
+ try {
+ emittedDeletes.release();
+ } catch (Throwable cleanupFailure) {
+ primaryFailure =
ExceptionUtils.firstOrSuppressed(cleanupFailure, primaryFailure);
+ }
+ if (primaryFailure != null) {
+ addWriter.abort(primaryFailure);
+ deleteWriter.abort(primaryFailure);
}
}
- return Pair.of(addWriter.result(), deleteWriter.result());
+ if (primaryFailure != null) {
+ ExceptionUtils.rethrowException(primaryFailure);
+ }
+ return files;
}
private static void writeCurrent(ManifestAvroWriter writer, Cursor cursor)
throws Exception {
@@ -232,20 +248,16 @@ final class ManifestEntryRunMergePlan {
}
static void closeCursors(List<Cursor> cursors) throws Exception {
- Exception failure = null;
+ Throwable primaryFailure = null;
for (Cursor cursor : cursors) {
try {
cursor.close();
- } catch (Exception e) {
- if (failure == null) {
- failure = e;
- } else {
- failure.addSuppressed(e);
- }
+ } catch (Throwable cleanupFailure) {
+ primaryFailure =
ExceptionUtils.firstOrSuppressed(cleanupFailure, primaryFailure);
}
}
- if (failure != null) {
- throw failure;
+ if (primaryFailure != null) {
+ ExceptionUtils.rethrowException(primaryFailure);
}
}
diff --git
a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestFileBlockMerger.java
b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestFileBlockMerger.java
index baef36d807..05b6225f60 100644
---
a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestFileBlockMerger.java
+++
b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestFileBlockMerger.java
@@ -483,7 +483,7 @@ final class ManifestFileBlockMerger {
writer.close();
return writer.result();
} catch (Exception | Error failure) {
- writer.abort();
+ writer.abort(failure);
throw failure;
} finally {
reusableIdentifier.release();
diff --git
a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestFileLegacyMerger.java
b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestFileLegacyMerger.java
index 12c6332526..a016fb0b55 100644
---
a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestFileLegacyMerger.java
+++
b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestFileLegacyMerger.java
@@ -27,6 +27,7 @@ import org.apache.paimon.manifest.ManifestFile;
import org.apache.paimon.manifest.ManifestFileMeta;
import org.apache.paimon.partition.PartitionPredicate;
import org.apache.paimon.types.RowType;
+import org.apache.paimon.utils.ExceptionUtils;
import org.apache.paimon.utils.Filter;
import org.slf4j.Logger;
@@ -218,7 +219,6 @@ final class ManifestFileLegacyMerger {
singletonList(
readForFullCompaction(
file, manifestFile, mustChange,
deleteEntries));
- Exception exception = null;
try {
for (FullCompactionReadResult readResult :
sequentialBatchedExecute(reader, toBeMerged,
manifestReadParallelism)) {
@@ -228,20 +228,16 @@ final class ManifestFileLegacyMerger {
result.add(readResult.file);
}
}
- } catch (Exception e) {
- exception = e;
- } finally {
- if (exception != null) {
- writer.abort();
- throw exception;
- }
writer.close();
+ List<ManifestFileMeta> merged = writer.result();
+ result.addAll(merged);
+ newFilesForAbort.addAll(merged);
+ return Optional.of(result);
+ } catch (Throwable primaryFailure) {
+ writer.abort(primaryFailure);
+ ExceptionUtils.rethrowException(primaryFailure);
+ return Optional.empty();
}
-
- List<ManifestFileMeta> merged = writer.result();
- result.addAll(merged);
- newFilesForAbort.addAll(merged);
- return Optional.of(result);
}
private static FullCompactionReadResult readForFullCompaction(
diff --git
a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestFileMerger.java
b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestFileMerger.java
index a083967e6b..e3f8c7af76 100644
---
a/paimon-core/src/main/java/org/apache/paimon/operation/ManifestFileMerger.java
+++
b/paimon-core/src/main/java/org/apache/paimon/operation/ManifestFileMerger.java
@@ -24,6 +24,7 @@ import org.apache.paimon.manifest.ManifestEntry;
import org.apache.paimon.manifest.ManifestFile;
import org.apache.paimon.manifest.ManifestFileMeta;
import org.apache.paimon.types.RowType;
+import org.apache.paimon.utils.ExceptionUtils;
import javax.annotation.Nullable;
@@ -74,12 +75,18 @@ public class ManifestFileMerger {
}
return ManifestFileLegacyMerger.merge(
input, newFilesForAbort, manifestFile, partitionType,
options);
- } catch (Throwable e) {
+ } catch (Throwable primaryFailure) {
// exception occurs, clean up and rethrow
for (ManifestFileMeta manifest : newFilesForAbort) {
- manifestFile.delete(manifest.fileName());
+ try {
+ manifestFile.delete(manifest.fileName());
+ } catch (Throwable cleanupFailure) {
+ primaryFailure =
+ ExceptionUtils.firstOrSuppressed(cleanupFailure,
primaryFailure);
+ }
}
- throw new RuntimeException(e);
+ ExceptionUtils.rethrow(primaryFailure);
+ return null;
}
}
diff --git
a/paimon-core/src/test/java/org/apache/paimon/operation/ManifestRewriteCleanupTest.java
b/paimon-core/src/test/java/org/apache/paimon/operation/ManifestRewriteCleanupTest.java
new file mode 100644
index 0000000000..d54e29e91f
--- /dev/null
+++
b/paimon-core/src/test/java/org/apache/paimon/operation/ManifestRewriteCleanupTest.java
@@ -0,0 +1,778 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.paimon.operation;
+
+import org.apache.paimon.CoreOptions;
+import org.apache.paimon.data.BinaryRow;
+import org.apache.paimon.data.BinaryRowWriter;
+import org.apache.paimon.data.GenericRow;
+import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.data.Timestamp;
+import org.apache.paimon.fs.Path;
+import org.apache.paimon.io.DataFileMeta;
+import org.apache.paimon.manifest.CollectedDeletes;
+import org.apache.paimon.manifest.FileEntry.ReusableIdentifier;
+import org.apache.paimon.manifest.FileKind;
+import org.apache.paimon.manifest.FileSource;
+import org.apache.paimon.manifest.ManifestAvroReader;
+import org.apache.paimon.manifest.ManifestAvroWriter;
+import org.apache.paimon.manifest.ManifestAvroWriter.EncodedEntry;
+import org.apache.paimon.manifest.ManifestEntry;
+import org.apache.paimon.manifest.ManifestFile;
+import org.apache.paimon.manifest.ManifestFileMeta;
+import org.apache.paimon.manifest.ManifestFileMetaTestBase;
+import org.apache.paimon.manifest.ProjectedManifestEntry;
+import org.apache.paimon.options.Options;
+import org.apache.paimon.schema.SchemaManager;
+import org.apache.paimon.stats.StatsTestUtils;
+import org.apache.paimon.types.DataTypes;
+import org.apache.paimon.types.RowType;
+import org.apache.paimon.utils.FileStorePathFactory;
+import org.apache.paimon.utils.TraceableFileIO;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.mockito.AdditionalAnswers;
+import org.mockito.ArgumentMatchers;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.AbstractList;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.catchThrowable;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+
+/** Tests cleanup when manifest rewrites fail with {@link Error}. */
+class ManifestRewriteCleanupTest extends ManifestFileMetaTestBase {
+
+ private static final RowType PARTITION_TYPE = RowType.of(DataTypes.INT());
+
+ @TempDir java.nio.file.Path tempDir;
+
+ private FailingFileIO fileIO;
+ private ManifestFile manifestFile;
+ private Path manifestPath;
+
+ @BeforeEach
+ void beforeEach() {
+ fileIO = new FailingFileIO();
+ manifestFile = createManifestFile(tempDir.toString(), fileIO);
+ manifestPath = new Path(tempDir.toString(), "manifest");
+ }
+
+ @Test
+ void testRunMergeAbortsWriterAndPreservesError() throws Exception {
+ ManifestFileMeta input =
+ makeManifest(
+ rowIdEntry(FileKind.ADD, "file-0", 0),
+ rowIdEntry(FileKind.ADD, "file-1", 1));
+ int manifestCount = manifestFileCount();
+ AssertionError primaryFailure = new AssertionError("cursor failure");
+ IOException closeFailure = new IOException("cursor close failure");
+ List<ManifestFileMeta> newFilesForAbort = new ArrayList<>();
+ CollectedDeletes deletes = new CollectedDeletes(true);
+
+ Throwable thrown;
+ try {
+ ManifestEntryRunMergePlan plan =
+ runMergePlan(input, deletes, false, 1, primaryFailure,
closeFailure);
+ thrown = catchThrowable(() -> plan.mergeToManifest(manifestFile,
newFilesForAbort));
+ } finally {
+ deletes.release();
+ }
+
+
assertThat(thrown).isSameAs(primaryFailure).hasSuppressedException(closeFailure);
+ assertThat(newFilesForAbort).isEmpty();
+ assertNoManifestLeak(manifestCount);
+ }
+
+ @Test
+ void testRunMergeAbortsBothWritersWhenCleanupFails() throws Exception {
+ ManifestFileMeta input =
+ makeManifest(
+ rowIdEntry(FileKind.ADD, "add", 0),
+ rowIdEntry(FileKind.DELETE, "delete-0", 1),
+ rowIdEntry(FileKind.DELETE, "delete-1", 2));
+ int manifestCount = manifestFileCount();
+ AssertionError primaryFailure = new AssertionError("cursor failure");
+ List<ManifestFileMeta> newFilesForAbort = new ArrayList<>();
+ CollectedDeletes deletes = new CollectedDeletes(true);
+ fileIO.failDeletes();
+
+ Throwable thrown;
+ try {
+ ManifestEntryRunMergePlan plan =
+ runMergePlan(input, deletes, true, 2, primaryFailure,
null);
+ thrown =
+ catchThrowable(() ->
plan.mergeMinorToManifest(manifestFile, newFilesForAbort));
+ } finally {
+ deletes.release();
+ }
+
+ assertThat(thrown).isSameAs(primaryFailure);
+ assertThat(thrown.getSuppressed())
+ .extracting(Throwable::getMessage)
+ .containsExactly("delete failure 1", "delete failure 2");
+ assertThat(fileIO.deleteAttempts()).isEqualTo(2);
+ assertThat(newFilesForAbort).isEmpty();
+ assertNoManifestLeak(manifestCount);
+ }
+
+ @Test
+ void testExternalSortAbortsWriterAndPreservesError() throws Exception {
+ List<ManifestFileMeta> input =
+ Collections.singletonList(
+ makeManifest(
+ rowIdEntry(FileKind.ADD, "file-0", 0),
+ rowIdEntry(FileKind.ADD, "file-1", 1)));
+ int manifestCount = manifestFileCount();
+ AssertionError primaryFailure = new AssertionError("sort key failure");
+ List<ManifestFileMeta> newFilesForAbort = new ArrayList<>();
+ CollectedDeletes deletes = new CollectedDeletes(true);
+
+ Throwable thrown;
+ try {
+ thrown =
+ catchThrowable(
+ () ->
+
ManifestEntryExternalSort.sortAndWriteFullEntries(
+ input,
+ failingSortKey(input, 1,
primaryFailure),
+ externalSortConfig(),
+ manifestFile,
+ newFilesForAbort,
+ deletes,
+ 1));
+ } finally {
+ deletes.release();
+ }
+
+ assertThat(thrown).isSameAs(primaryFailure);
+ assertThat(newFilesForAbort).isEmpty();
+ assertNoManifestLeak(manifestCount);
+ }
+
+ @Test
+ void testExternalSortAbortsBothWritersWhenCleanupFails() throws Exception {
+ List<ManifestFileMeta> input =
+ Collections.singletonList(
+ makeManifest(
+ rowIdEntry(FileKind.ADD, "add", 0),
+ rowIdEntry(FileKind.DELETE, "delete-0", 1),
+ rowIdEntry(FileKind.DELETE, "delete-1", 2)));
+ int manifestCount = manifestFileCount();
+ AssertionError primaryFailure = new AssertionError("sort key failure");
+ List<ManifestFileMeta> newFilesForAbort = new ArrayList<>();
+ fileIO.failDeletes();
+
+ Throwable thrown =
+ catchThrowable(
+ () ->
+
ManifestEntryExternalSort.sortAndWriteMinorEntries(
+ input,
+ failingSortKey(input, 2,
primaryFailure),
+ externalSortConfig(),
+ manifestFile,
+ newFilesForAbort,
+ 1));
+
+ assertThat(thrown).isSameAs(primaryFailure);
+ assertThat(thrown.getSuppressed())
+ .extracting(Throwable::getMessage)
+ .containsExactly("delete failure 1", "delete failure 2");
+ assertThat(fileIO.deleteAttempts()).isEqualTo(2);
+ assertThat(newFilesForAbort).isEmpty();
+ assertNoManifestLeak(manifestCount);
+ }
+
+ @Test
+ void testLegacyMergeAbortsActiveWriterOnError() throws Exception {
+ List<ManifestFileMeta> input =
+ Arrays.asList(
+ makeManifest(makeEntry(true, "file-0")),
+ makeManifest(makeEntry(true, "file-1")));
+ int manifestCount = manifestFileCount();
+ AssertionError primaryFailure = new AssertionError("legacy writer
failure");
+ ManifestFile spyManifestFile = spy(manifestFile);
+ ManifestAvroWriter activeWriter = spy(manifestFile.createAvroWriter());
+ doReturn(activeWriter).when(spyManifestFile).createAvroWriter();
+ doAnswer(
+ invocation -> {
+ Iterable<? extends ManifestEntry> entries =
invocation.getArgument(0);
+ activeWriter.write(entries.iterator().next());
+ throw primaryFailure;
+ })
+ .when(activeWriter)
+ .write(ArgumentMatchers.<Iterable<? extends
ManifestEntry>>any());
+
+ Throwable thrown =
+ catchThrowable(
+ () ->
+ ManifestFileLegacyMerger.tryFullCompaction(
+ input,
+ new ArrayList<>(),
+ spyManifestFile,
+ Long.MAX_VALUE,
+ 1,
+ PARTITION_TYPE,
+ 1));
+
+ assertThat(thrown).isSameAs(primaryFailure);
+ assertNoManifestLeak(manifestCount);
+ }
+
+ @Test
+ void testBlockMergePreservesErrorWhenWriterCleanupFails() throws Exception
{
+ List<ManifestFileMeta> input =
+ Arrays.asList(
+ makeManifest(makeEntry(true, "file-0")),
+ makeManifest(makeEntry(true, "file-1")));
+ int manifestCount = manifestFileCount();
+ AssertionError primaryFailure = new AssertionError("block writer
failure");
+ ManifestFile spyManifestFile = spy(manifestFile);
+ ManifestAvroWriter activeWriter = spy(manifestFile.createAvroWriter());
+ doReturn(activeWriter).when(spyManifestFile).createAvroWriter();
+ doAnswer(
+ invocation -> {
+ activeWriter.write(makeEntry(true, "partial"));
+ throw primaryFailure;
+ })
+ .when(activeWriter)
+ .writeEncodedManifest(
+ ArgumentMatchers.any(ManifestAvroReader.class),
+ ArgumentMatchers.any(ManifestFileMeta.class));
+ fileIO.failDeletes();
+
+ Throwable thrown =
+ catchThrowable(
+ () ->
+ ManifestFileBlockMerger.tryFullCompaction(
+ input,
+ new ArrayList<>(),
+ spyManifestFile,
+ Long.MAX_VALUE,
+ 1,
+ PARTITION_TYPE,
+ 1));
+
+ assertThat(thrown).isSameAs(primaryFailure);
+ assertThat(thrown.getSuppressed())
+ .extracting(Throwable::getMessage)
+ .containsExactly("delete failure 1");
+ assertThat(fileIO.deleteAttempts()).isEqualTo(1);
+ assertNoManifestLeak(manifestCount);
+ }
+
+ @Test
+ void testWriterPreservesWriteErrorAndCleansAllRollingFiles() throws
Exception {
+ ManifestAvroWriter writer = createManifestFile(1).createAvroWriter();
+ ManifestEntry entry = rowIdEntry(FileKind.ADD, "file", 0);
+ // ManifestAvroWriter checks the rolling threshold every 1,000 records.
+ writer.write(Collections.nCopies(2000, entry));
+ assertThat(manifestFileCount()).isEqualTo(2);
+
+ AssertionError primaryFailure = new AssertionError("manifest write
failure");
+ AtomicInteger kindCalls = new AtomicInteger();
+ ManifestEntry failingEntry =
+ mock(ManifestEntry.class,
AdditionalAnswers.delegatesTo(entry));
+ doAnswer(
+ invocation -> {
+ if (kindCalls.incrementAndGet() == 2) {
+ throw primaryFailure;
+ }
+ return entry.kind();
+ })
+ .when(failingEntry)
+ .kind();
+ fileIO.failNextDeleteBeforeDeletion();
+
+ Throwable thrown = catchThrowable(() -> writer.write(failingEntry));
+
+ assertThat(thrown).isSameAs(primaryFailure);
+ assertThat(thrown.getSuppressed())
+ .extracting(Throwable::getMessage)
+ .containsExactly("delete failure 1");
+ assertThat(fileIO.deleteAttempts()).isEqualTo(3);
+ assertThat(manifestFileCount()).isEqualTo(1);
+ assertThat(
+ TraceableFileIO.openOutputStreams(
+ path ->
path.toString().startsWith(manifestPath.toString())))
+ .isEmpty();
+ }
+
+ @Test
+ void testCloseCursorsContinuesAfterError() {
+ AssertionError primaryFailure = new AssertionError("first close
failure");
+ IOException cleanupFailure = new IOException("second close failure");
+ CloseFailureCursor first = new CloseFailureCursor(primaryFailure);
+ CloseFailureCursor second = new CloseFailureCursor(cleanupFailure);
+ CloseFailureCursor third = new CloseFailureCursor(null);
+
+ Throwable thrown =
+ catchThrowable(
+ () ->
+ ManifestEntryRunMergePlan.closeCursors(
+ Arrays.asList(first, second, third)));
+
+
assertThat(thrown).isSameAs(primaryFailure).hasSuppressedException(cleanupFailure);
+ assertThat(first.closed).isTrue();
+ assertThat(second.closed).isTrue();
+ assertThat(third.closed).isTrue();
+ }
+
+ @Test
+ void testManifestFileMergerPreservesErrorWhenCleanupFails() throws
IOException {
+ List<ManifestFileMeta> input =
+ Arrays.asList(
+ makeManifest(makeEntry(true, "file-0")),
+ makeManifest(makeEntry(true, "file-1")),
+ makeManifest(makeEntry(true, "file-2")),
+ makeManifest(makeEntry(true, "file-3")));
+ int manifestCount = manifestFileCount();
+ long targetSize =
+
input.stream().mapToLong(ManifestFileMeta::fileSize).max().getAsLong() + 1;
+ AssertionError primaryFailure = new AssertionError("manifest merge
failure");
+ fileIO.failDeletes();
+
+ Options options = new Options();
+ options.set(CoreOptions.MANIFEST_TARGET_FILE_SIZE.key(), targetSize +
"B");
+ options.set(CoreOptions.MANIFEST_FULL_COMPACTION_FILE_SIZE.key(),
Long.MAX_VALUE + "B");
+ options.set(CoreOptions.MANIFEST_MERGE_MIN_COUNT, 2);
+ options.set(CoreOptions.MANIFEST_MERGE_OPTIMIZE_ENABLED, false);
+ options.set(CoreOptions.SCAN_MANIFEST_PARALLELISM, 1);
+
+ // The first iteration checks full compaction. Fail in the second
iteration after
+ // compactMinor registers one file.
+ List<ManifestFileMeta> failingInput = new
FailingSecondIterationList(input, primaryFailure);
+ Throwable thrown =
+ catchThrowable(
+ () ->
+ ManifestFileMerger.merge(
+ failingInput,
+ manifestFile,
+ PARTITION_TYPE,
+ new CoreOptions(options)));
+
+ assertThat(thrown).isSameAs(primaryFailure);
+ assertThat(thrown.getSuppressed())
+ .extracting(Throwable::getMessage)
+ .containsExactly("delete failure 1");
+ assertThat(fileIO.deleteAttempts()).isEqualTo(1);
+ assertNoManifestLeak(manifestCount);
+ }
+
+ private ManifestEntryRunMergePlan runMergePlan(
+ ManifestFileMeta input,
+ CollectedDeletes deletes,
+ boolean minor,
+ int successfulAdvancesBeforeFailure,
+ AssertionError failure,
+ IOException closeFailure) {
+ ManifestEntryRunMerge.SortPartitionDictionary partitions =
+ new ManifestEntryRunMerge.SortPartitionDictionary(
+ (left, right) -> Integer.compare(left.getInt(0),
right.getInt(0)));
+ ManifestEntryRunMergePlan.Source.Spec source =
+ (file, planDeletes, planMinor, planPartitions) ->
+ new FailingCursor(
+ new
ManifestEntryRunMergePlan.InMemoryManifestCursor(
+ file, input, planDeletes, planMinor,
planPartitions),
+ successfulAdvancesBeforeFailure,
+ failure,
+ closeFailure);
+ return new ManifestEntryRunMergePlan(
+ Collections.singletonList(source), partitions, deletes, minor);
+ }
+
+ private ManifestFileSorter.ManifestSortKey failingSortKey(
+ List<ManifestFileMeta> input, int successfulRowsBeforeFailure,
AssertionError failure) {
+ return new FailingManifestSortKey(
+ ManifestFileSorter.createSortKey(true, input, null,
PARTITION_TYPE),
+ successfulRowsBeforeFailure,
+ failure);
+ }
+
+ private ManifestEntryExternalSort.ExternalSortConfig externalSortConfig() {
+ Options options = new Options();
+ options.set(CoreOptions.SORT_SPILL_BUFFER_SIZE.key(), "1 mb");
+ return ManifestEntryExternalSort.ExternalSortConfig.from(new
CoreOptions(options), null);
+ }
+
+ private ManifestEntry rowIdEntry(FileKind kind, String fileName, long
firstRowId) {
+ BinaryRow partition = new BinaryRow(1);
+ BinaryRowWriter writer = new BinaryRowWriter(partition);
+ writer.writeInt(0, 0);
+ writer.complete();
+ return ManifestEntry.create(
+ kind,
+ partition,
+ 0,
+ 0,
+ DataFileMeta.create(
+ fileName,
+ 0,
+ 1,
+ partition,
+ partition,
+ StatsTestUtils.newEmptySimpleStats(),
+ StatsTestUtils.newEmptySimpleStats(),
+ 0,
+ 0,
+ 0,
+ 0,
+ Collections.emptyList(),
+ Timestamp.fromEpochMillis(200000),
+ 0L,
+ null,
+ FileSource.APPEND,
+ null,
+ null,
+ firstRowId,
+ Collections.singletonList("f0")));
+ }
+
+ private ManifestFile createManifestFile(long suggestedFileSize) {
+ Path tablePath = new Path(tempDir.toString());
+ return new ManifestFile.Factory(
+ fileIO,
+ new SchemaManager(fileIO, tablePath),
+ PARTITION_TYPE,
+ avro,
+ "zstd",
+ new FileStorePathFactory(
+ tablePath,
+ PARTITION_TYPE,
+ "default",
+ CoreOptions.FILE_FORMAT.defaultValue(),
+ CoreOptions.DATA_FILE_PREFIX.defaultValue(),
+
CoreOptions.CHANGELOG_FILE_PREFIX.defaultValue(),
+
CoreOptions.PARTITION_GENERATE_LEGACY_NAME.defaultValue(),
+
CoreOptions.FILE_SUFFIX_INCLUDE_COMPRESSION.defaultValue(),
+ CoreOptions.FILE_COMPRESSION.defaultValue(),
+ null,
+ null,
+ CoreOptions.ExternalPathStrategy.NONE,
+ null,
+ false,
+ null),
+ suggestedFileSize,
+ null)
+ .create();
+ }
+
+ private int manifestFileCount() throws IOException {
+ return fileIO.listStatus(manifestPath).length;
+ }
+
+ private void assertNoManifestLeak(int expectedManifestCount) throws
IOException {
+ assertThat(manifestFileCount()).isEqualTo(expectedManifestCount);
+ assertThat(
+ TraceableFileIO.openOutputStreams(
+ path ->
path.toString().startsWith(manifestPath.toString())))
+ .isEmpty();
+ }
+
+ @Override
+ protected ManifestFile getManifestFile() {
+ return manifestFile;
+ }
+
+ @Override
+ protected RowType getPartitionType() {
+ return PARTITION_TYPE;
+ }
+
+ private static class FailingCursor implements
ManifestEntryRunMergePlan.Cursor {
+
+ private final ManifestEntryRunMergePlan.Cursor delegate;
+ private final int successfulAdvancesBeforeFailure;
+ private final AssertionError failure;
+ private final IOException closeFailure;
+ private int successfulAdvances;
+
+ private FailingCursor(
+ ManifestEntryRunMergePlan.Cursor delegate,
+ int successfulAdvancesBeforeFailure,
+ AssertionError failure,
+ IOException closeFailure) {
+ this.delegate = delegate;
+ this.successfulAdvancesBeforeFailure =
successfulAdvancesBeforeFailure;
+ this.failure = failure;
+ this.closeFailure = closeFailure;
+ }
+
+ @Override
+ public boolean advance() throws Exception {
+ if (successfulAdvances >= successfulAdvancesBeforeFailure) {
+ throw failure;
+ }
+ boolean advanced = delegate.advance();
+ if (advanced) {
+ successfulAdvances++;
+ }
+ return advanced;
+ }
+
+ @Override
+ public boolean hasCurrent() {
+ return delegate.hasCurrent();
+ }
+
+ @Override
+ public ProjectedManifestEntry current() {
+ return delegate.current();
+ }
+
+ @Override
+ public EncodedEntry metadata() {
+ return delegate.metadata();
+ }
+
+ @Override
+ public ManifestEntryRunMerge.SortKey key() {
+ return delegate.key();
+ }
+
+ @Override
+ public ByteBuffer encodedRecord() {
+ return delegate.encodedRecord();
+ }
+
+ @Override
+ public ReusableIdentifier identifier() {
+ return delegate.identifier();
+ }
+
+ @Override
+ public void close() throws Exception {
+ delegate.close();
+ if (closeFailure != null) {
+ throw closeFailure;
+ }
+ }
+ }
+
+ private static class FailingManifestSortKey implements
ManifestFileSorter.ManifestSortKey {
+
+ private final ManifestFileSorter.ManifestSortKey delegate;
+ private final int successfulRowsBeforeFailure;
+ private final AssertionError failure;
+ private int successfulRows;
+
+ private FailingManifestSortKey(
+ ManifestFileSorter.ManifestSortKey delegate,
+ int successfulRowsBeforeFailure,
+ AssertionError failure) {
+ this.delegate = delegate;
+ this.successfulRowsBeforeFailure = successfulRowsBeforeFailure;
+ this.failure = failure;
+ }
+
+ @Override
+ public int compareMin(ManifestFileMeta left, ManifestFileMeta right) {
+ return delegate.compareMin(left, right);
+ }
+
+ @Override
+ public int compareMax(ManifestFileMeta left, ManifestFileMeta right) {
+ return delegate.compareMax(left, right);
+ }
+
+ @Override
+ public boolean isAfterMax(ManifestFileMeta file, ManifestFileMeta
maxFile) {
+ return delegate.isAfterMax(file, maxFile);
+ }
+
+ @Override
+ public RowType externalSortRowType() {
+ return delegate.externalSortRowType();
+ }
+
+ @Override
+ public int[] externalSortKeyFields() {
+ return delegate.externalSortKeyFields();
+ }
+
+ @Override
+ public void replaceExternalSortRow(
+ GenericRow row, ManifestEntry entry, InternalRow
binaryManifestRow) {
+ delegate.replaceExternalSortRow(row, entry, binaryManifestRow);
+ }
+
+ @Override
+ public InternalRow binaryManifestRow(BinaryRow row) {
+ if (successfulRows >= successfulRowsBeforeFailure) {
+ throw failure;
+ }
+ successfulRows++;
+ return delegate.binaryManifestRow(row);
+ }
+ }
+
+ private static class CloseFailureCursor implements
ManifestEntryRunMergePlan.Cursor {
+
+ private final Throwable failure;
+ private boolean closed;
+
+ private CloseFailureCursor(Throwable failure) {
+ this.failure = failure;
+ }
+
+ @Override
+ public boolean advance() {
+ return false;
+ }
+
+ @Override
+ public boolean hasCurrent() {
+ return false;
+ }
+
+ @Override
+ public ProjectedManifestEntry current() {
+ return null;
+ }
+
+ @Override
+ public EncodedEntry metadata() {
+ return null;
+ }
+
+ @Override
+ public ManifestEntryRunMerge.SortKey key() {
+ return null;
+ }
+
+ @Override
+ public ByteBuffer encodedRecord() {
+ return null;
+ }
+
+ @Override
+ public ReusableIdentifier identifier() {
+ return null;
+ }
+
+ @Override
+ public void close() throws Exception {
+ closed = true;
+ if (failure instanceof Error) {
+ throw (Error) failure;
+ }
+ if (failure != null) {
+ throw (Exception) failure;
+ }
+ }
+ }
+
+ private static class FailingFileIO extends TraceableFileIO {
+
+ private boolean trackDeletes;
+ private boolean failDeletesAfterDeletion;
+ private int failuresBeforeDeletion;
+ private int deleteAttempts;
+
+ private void failDeletes() {
+ trackDeletes = true;
+ failDeletesAfterDeletion = true;
+ }
+
+ private void failNextDeleteBeforeDeletion() {
+ trackDeletes = true;
+ failuresBeforeDeletion = 1;
+ }
+
+ private int deleteAttempts() {
+ return deleteAttempts;
+ }
+
+ @Override
+ public boolean delete(Path file, boolean recursive) throws IOException
{
+ if (trackDeletes) {
+ deleteAttempts++;
+ if (failuresBeforeDeletion > 0) {
+ failuresBeforeDeletion--;
+ throw new RuntimeException("delete failure " +
deleteAttempts);
+ }
+ }
+ boolean deleted = super.delete(file, recursive);
+ if (failDeletesAfterDeletion) {
+ throw new RuntimeException("delete failure " + deleteAttempts);
+ }
+ return deleted;
+ }
+ }
+
+ private static class FailingSecondIterationList extends
AbstractList<ManifestFileMeta> {
+
+ private final List<ManifestFileMeta> delegate;
+ private final AssertionError failure;
+ private int iterationCount;
+
+ private FailingSecondIterationList(
+ List<ManifestFileMeta> delegate, AssertionError failure) {
+ this.delegate = delegate;
+ this.failure = failure;
+ }
+
+ @Override
+ public ManifestFileMeta get(int index) {
+ return delegate.get(index);
+ }
+
+ @Override
+ public int size() {
+ return delegate.size();
+ }
+
+ @Override
+ public Iterator<ManifestFileMeta> iterator() {
+ Iterator<ManifestFileMeta> iterator = delegate.iterator();
+ int currentIteration = ++iterationCount;
+ return new Iterator<ManifestFileMeta>() {
+
+ private int returned;
+
+ @Override
+ public boolean hasNext() {
+ return iterator.hasNext();
+ }
+
+ @Override
+ public ManifestFileMeta next() {
+ if (currentIteration == 2 && returned == 2) {
+ throw failure;
+ }
+ returned++;
+ return iterator.next();
+ }
+ };
+ }
+ }
+}