diff --git a/fdb-extensions/src/main/java/com/apple/foundationdb/util/CloseException.java b/fdb-extensions/src/main/java/com/apple/foundationdb/util/CloseException.java new file mode 100644 index 0000000000..aa59565ea6 --- /dev/null +++ b/fdb-extensions/src/main/java/com/apple/foundationdb/util/CloseException.java @@ -0,0 +1,33 @@ +/* + * CloseException.java + * + * This source file is part of the FoundationDB open source project + * + * Copyright 2015-2025 Apple Inc. and the FoundationDB project authors + * + * Licensed 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 com.apple.foundationdb.util; + +/** + * Exception thrown when the {@link CloseableUtils#closeAll} method catches an exception. + * This exception will have the {@code cause} set to the first exception thrown during {@code closeAll} and any further + * exception thrown will be added as {@code Suppressed}. + */ +@SuppressWarnings("serial") +public class CloseException extends Exception { + public CloseException(final Throwable cause) { + super(cause); + } +} diff --git a/fdb-extensions/src/main/java/com/apple/foundationdb/util/CloseableUtils.java b/fdb-extensions/src/main/java/com/apple/foundationdb/util/CloseableUtils.java new file mode 100644 index 0000000000..4fb039408b --- /dev/null +++ b/fdb-extensions/src/main/java/com/apple/foundationdb/util/CloseableUtils.java @@ -0,0 +1,69 @@ +/* + * CloseableUtils.java + * + * This source file is part of the FoundationDB open source project + * + * Copyright 2015-2025 Apple Inc. and the FoundationDB project authors + * + * Licensed 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 com.apple.foundationdb.util; + +import com.apple.foundationdb.annotation.API; + +/** + * Utility methods to help interact with {@link AutoCloseable} classes. + **/ +public class CloseableUtils { + /** + * A utility to close multiple {@link AutoCloseable} objects, preserving all the caught exceptions. + * The method would attempt to close all closeables in order, even if some failed. + * Note that {@link CloseException} is used to wrap any exception thrown during the closing process. The reason for + * that is the compiler fails to compile a {@link AutoCloseable#close()} implementation that throws a generic + * {@link Exception} (due to {@link InterruptedException} issue) - We therefore have to catch and wrap all exceptions. + * @param closeables the given sequence of {@link AutoCloseable} + * @throws CloseException in case any exception was caught during the process. The first exception will be added + * as a {@code cause}. In case more than one exception was caught, it will be added as Suppressed. + */ + @API(API.Status.INTERNAL) + @SuppressWarnings("PMD.CloseResource") + public static void closeAll(AutoCloseable... closeables) throws CloseException { + CloseException accumulatedException = null; + for (AutoCloseable closeable: closeables) { + try { + closeable.close(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + if (accumulatedException == null) { + accumulatedException = new CloseException(e); + } else { + accumulatedException.addSuppressed(e); + } + } catch (Exception e) { + if (accumulatedException == null) { + accumulatedException = new CloseException(e); + } else { + accumulatedException.addSuppressed(e); + } + } + } + if (accumulatedException != null) { + throw accumulatedException; + } + } + + private CloseableUtils() { + // prevent constructor from being called + } +} diff --git a/fdb-extensions/src/test/java/com/apple/foundationdb/util/CloseableUtilsTest.java b/fdb-extensions/src/test/java/com/apple/foundationdb/util/CloseableUtilsTest.java new file mode 100644 index 0000000000..0c87d16ffb --- /dev/null +++ b/fdb-extensions/src/test/java/com/apple/foundationdb/util/CloseableUtilsTest.java @@ -0,0 +1,105 @@ +/* + * CloseableUtilsTest.java + * + * This source file is part of the FoundationDB open source project + * + * Copyright 2015-2025 Apple Inc. and the FoundationDB project authors + * + * Licensed 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 com.apple.foundationdb.util; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Tests for the {@link CloseableUtils} class. + */ +public class CloseableUtilsTest { + + @Test + void closeAllNoIssue() throws Exception { + SimpleCloseable c1 = new SimpleCloseable(false, null); + SimpleCloseable c2 = new SimpleCloseable(false, null); + SimpleCloseable c3 = new SimpleCloseable(false, null); + + CloseableUtils.closeAll(c1, c2, c3); + + Assertions.assertTrue(c1.isClosed()); + Assertions.assertTrue(c2.isClosed()); + Assertions.assertTrue(c3.isClosed()); + } + + @Test + void closeAllFailed() throws Exception { + SimpleCloseable c1 = new SimpleCloseable(true, "c1"); + SimpleCloseable c2 = new SimpleCloseable(true, "c2"); + SimpleCloseable c3 = new SimpleCloseable(true, "c3"); + + final CloseException exception = assertThrows(CloseException.class, () -> CloseableUtils.closeAll(c1, c2, c3)); + + Assertions.assertEquals("c1", exception.getCause().getMessage()); + final Throwable[] suppressed = exception.getSuppressed(); + Assertions.assertEquals(2, suppressed.length); + Assertions.assertEquals("c2", suppressed[0].getMessage()); + Assertions.assertEquals("c3", suppressed[1].getMessage()); + + Assertions.assertTrue(c1.isClosed()); + Assertions.assertTrue(c2.isClosed()); + Assertions.assertTrue(c3.isClosed()); + } + + @Test + void closeSomeFailed() throws Exception { + SimpleCloseable c1 = new SimpleCloseable(true, "c1"); + SimpleCloseable c2 = new SimpleCloseable(false, null); + SimpleCloseable c3 = new SimpleCloseable(true, "c3"); + + final CloseException exception = assertThrows(CloseException.class, () -> CloseableUtils.closeAll(c1, c2, c3)); + + Assertions.assertEquals("c1", exception.getCause().getMessage()); + final Throwable[] suppressed = exception.getSuppressed(); + Assertions.assertEquals(1, suppressed.length); + Assertions.assertEquals("c3", suppressed[0].getMessage()); + + Assertions.assertTrue(c1.isClosed()); + Assertions.assertTrue(c2.isClosed()); + Assertions.assertTrue(c3.isClosed()); + } + + private class SimpleCloseable implements AutoCloseable { + private final boolean fail; + private final String message; + private boolean closed = false; + + public SimpleCloseable(boolean fail, String message) { + this.fail = fail; + this.message = message; + } + + @Override + public void close() { + closed = true; + if (fail) { + throw new RuntimeException(message); + } + } + + public boolean isClosed() { + return closed; + } + } +} diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/foundationdb/recordrepair/RecordRepair.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/foundationdb/recordrepair/RecordRepair.java index 639d8e8f8a..7ca244d8c2 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/foundationdb/recordrepair/RecordRepair.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/foundationdb/recordrepair/RecordRepair.java @@ -30,6 +30,7 @@ import com.apple.foundationdb.record.provider.foundationdb.runners.throttled.CursorFactory; import com.apple.foundationdb.record.provider.foundationdb.runners.throttled.ThrottledRetryingIterator; import com.apple.foundationdb.tuple.Tuple; +import com.apple.foundationdb.util.CloseException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -114,7 +115,7 @@ public static Builder builder(@Nonnull FDBDatabase database, final FDBRecordStor } @Override - public void close() { + public void close() throws CloseException { throttledIterator.close(); } diff --git a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/foundationdb/runners/throttled/ThrottledRetryingIterator.java b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/foundationdb/runners/throttled/ThrottledRetryingIterator.java index 9333f30fe1..b47e2ba554 100644 --- a/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/foundationdb/runners/throttled/ThrottledRetryingIterator.java +++ b/fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/foundationdb/runners/throttled/ThrottledRetryingIterator.java @@ -33,6 +33,8 @@ import com.apple.foundationdb.record.provider.foundationdb.FDBRecordStore; import com.apple.foundationdb.record.provider.foundationdb.runners.FutureAutoClose; import com.apple.foundationdb.record.provider.foundationdb.runners.TransactionalRunner; +import com.apple.foundationdb.util.CloseException; +import com.apple.foundationdb.util.CloseableUtils; import com.google.common.annotations.VisibleForTesting; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -149,30 +151,13 @@ public CompletableFuture iterateAll(final FDBRecordStore.Builder storeBuil } @Override - public void close() { + public void close() throws CloseException { if (closed) { return; } closed = true; // Ensure we call both close() methods, capturing all exceptions - RuntimeException caught = null; - try { - futureManager.close(); - } catch (RuntimeException e) { - caught = e; - } - try { - transactionalRunner.close(); - } catch (RuntimeException e) { - if (caught != null) { - caught.addSuppressed(e); - } else { - caught = e; - } - } - if (caught != null) { - throw caught; - } + CloseableUtils.closeAll(futureManager, transactionalRunner); } /**