diff --git a/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsFormat.java b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsFormat.java index 61c9b41c3a..1b1bc3cbd2 100644 --- a/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsFormat.java +++ b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsFormat.java @@ -91,7 +91,7 @@ public KnnVectorsWriter fieldsWriter(SegmentWriteState state) throws IOException acceleratedHNSWParams.getNumMergeWorkers(), new TaskExecutor(acceleratedHNSWParams.getMergeExec())); } catch (Exception e) { - throw new RuntimeException(e.getMessage()); + throw Utils.handleThrowable(e); } } } @@ -105,7 +105,7 @@ public KnnVectorsReader fieldsReader(SegmentReadState state) throws IOException return LUCENE_PROVIDER.getLuceneHnswVectorsReaderInstance( state, FLAT_VECTORS_FORMAT.fieldsReader(state)); } catch (Exception e) { - throw new RuntimeException(e.getMessage()); + throw Utils.handleThrowable(e); } } diff --git a/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat.java b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat.java index d1810bc540..40a818683d 100644 --- a/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat.java +++ b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat.java @@ -89,7 +89,7 @@ public KnnVectorsWriter fieldsWriter(SegmentWriteState state) throws IOException acceleratedHNSWParams.getMaxConn(), acceleratedHNSWParams.getBeamWidth()); return fallbackFormat.fieldsWriter(state); } catch (Exception e) { - throw new RuntimeException(e.getMessage()); + throw Utils.handleThrowable(e); } } } @@ -103,7 +103,7 @@ public KnnVectorsReader fieldsReader(SegmentReadState state) throws IOException return LUCENE99_PROVIDER.getLuceneHnswVectorsReaderInstance( state, FLAT_VECTORS_FORMAT.fieldsReader(state)); } catch (Exception e) { - throw new RuntimeException(e.getMessage()); + throw Utils.handleThrowable(e); } } diff --git a/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsFormat.java b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsFormat.java index 8d599a54ef..ead6daeaad 100644 --- a/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsFormat.java +++ b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsFormat.java @@ -81,7 +81,7 @@ public KnnVectorsWriter fieldsWriter(SegmentWriteState state) throws IOException acceleratedHNSWParams.getBeamWidth(), acceleratedHNSWParams.getMaxConn()); return fallbackFormat.fieldsWriter(state); } catch (Exception e) { - throw new RuntimeException(e.getMessage()); + throw Utils.handleThrowable(e); } } } @@ -95,7 +95,7 @@ public KnnVectorsReader fieldsReader(SegmentReadState state) throws IOException return LUCENE_PROVIDER.getLuceneHnswVectorsReaderInstance( state, FLAT_VECTORS_FORMAT.fieldsReader(state)); } catch (Exception e) { - throw new RuntimeException(e.getMessage()); + throw Utils.handleThrowable(e); } } diff --git a/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Utils.java b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Utils.java index e4a20d2b4d..034f8aa5d8 100644 --- a/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Utils.java +++ b/java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Utils.java @@ -28,12 +28,20 @@ public class Utils { static final Logger log = Logger.getLogger(Utils.class.getName()); /** - * A utility method that throws specific types of throwable objects based on types. + * A utility method that rethrows known throwable types without changing their identity. + * + *
In particular, {@link Error} instances must not be converted to a {@link + * RuntimeException}; callers rely on errors retaining their original type and stack trace. + * + *
This method never returns normally; its return type exists solely so callers can write + * {@code throw handleThrowable(t);}, letting the compiler verify that the enclosing statement + * always completes abruptly. * * @param t the throwable object + * @return never returns; always throws * @throws IOException */ - static void handleThrowable(Throwable t) throws IOException { + static RuntimeException handleThrowable(Throwable t) throws IOException { switch (t) { case IOException ioe -> throw ioe; case Error error -> throw error; diff --git a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestAcceleratedHNSWVectorsFormatThrowableHandling.java b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestAcceleratedHNSWVectorsFormatThrowableHandling.java new file mode 100644 index 0000000000..97fb066bd5 --- /dev/null +++ b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestAcceleratedHNSWVectorsFormatThrowableHandling.java @@ -0,0 +1,99 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuvs.lucene; + +import java.io.IOException; +import java.util.Map; +import org.apache.lucene.codecs.Codec; +import org.apache.lucene.codecs.KnnVectorsFormat; +import org.apache.lucene.index.FieldInfos; +import org.apache.lucene.index.SegmentInfo; +import org.apache.lucene.index.SegmentReadState; +import org.apache.lucene.store.ByteBuffersDirectory; +import org.apache.lucene.store.Directory; +import org.apache.lucene.store.FilterDirectory; +import org.apache.lucene.store.IOContext; +import org.apache.lucene.store.IndexInput; +import org.apache.lucene.tests.util.LuceneTestCase; +import org.apache.lucene.util.StringHelper; +import org.apache.lucene.util.Version; +import org.junit.Test; + +public class TestAcceleratedHNSWVectorsFormatThrowableHandling extends LuceneTestCase { + + @Test + public void testReadersRethrowIOExceptionUnchanged() throws Exception { + assertReaderFormatsRethrowUnchanged(new IOException("reader I/O failure")); + } + + @Test + public void testReadersRethrowRuntimeExceptionUnchanged() throws Exception { + assertReaderFormatsRethrowUnchanged(new IllegalStateException("reader runtime failure")); + } + + @Test + public void testReadersRethrowErrorUnchanged() throws Exception { + assertReaderFormatsRethrowUnchanged(new AssertionError("reader error")); + } + + private void assertReaderFormatsRethrowUnchanged(Throwable failure) throws Exception { + for (KnnVectorsFormat format : readerFormats()) { + try (Directory directory = new ThrowingDirectory(failure)) { + SegmentReadState state = newSegmentReadState(directory); + Throwable thrown = assertThrows(failure.getClass(), () -> format.fieldsReader(state)); + assertSame(format.getName(), failure, thrown); + } + } + } + + private static KnnVectorsFormat[] readerFormats() { + return new KnnVectorsFormat[] { + new Lucene99AcceleratedHNSWVectorsFormat(), + new LuceneAcceleratedHNSWScalarQuantizedVectorsFormat(), + new LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat() + }; + } + + private static SegmentReadState newSegmentReadState(Directory directory) { + SegmentInfo segmentInfo = + new SegmentInfo( + directory, + Version.LATEST, + Version.LATEST, + "_0", + 0, + false, + false, + Codec.getDefault(), + Map.of(), + StringHelper.randomId(), + Map.of(), + null); + return new SegmentReadState(directory, segmentInfo, FieldInfos.EMPTY, IOContext.DEFAULT); + } + + private static final class ThrowingDirectory extends FilterDirectory { + private final Throwable failure; + + private ThrowingDirectory(Throwable failure) { + super(new ByteBuffersDirectory()); + this.failure = failure; + } + + @Override + public IndexInput openInput(String name, IOContext context) throws IOException { + if (failure instanceof IOException ioe) { + throw ioe; + } + if (failure instanceof RuntimeException runtimeException) { + throw runtimeException; + } + if (failure instanceof Error error) { + throw error; + } + throw new AssertionError("unexpected test throwable", failure); + } + } +} diff --git a/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestUtilsThrowableHandling.java b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestUtilsThrowableHandling.java new file mode 100644 index 0000000000..6aead1bd8d --- /dev/null +++ b/java/cuvs-lucene/src/test/java/com/nvidia/cuvs/lucene/TestUtilsThrowableHandling.java @@ -0,0 +1,50 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.nvidia.cuvs.lucene; + +import java.io.IOException; +import org.apache.lucene.tests.util.LuceneTestCase; +import org.junit.Test; + +public class TestUtilsThrowableHandling extends LuceneTestCase { + + @Test + public void testHandleThrowableRethrowsIOExceptionUnchanged() { + IOException exception = new IOException("I/O failure"); + + IOException thrown = assertThrows(IOException.class, () -> Utils.handleThrowable(exception)); + + assertSame(exception, thrown); + } + + @Test + public void testHandleThrowableRethrowsRuntimeExceptionUnchanged() { + RuntimeException exception = new IllegalStateException("runtime failure"); + + RuntimeException thrown = + assertThrows(RuntimeException.class, () -> Utils.handleThrowable(exception)); + + assertSame(exception, thrown); + } + + @Test + public void testHandleThrowableRethrowsErrorUnchanged() { + Error error = new AssertionError("fatal failure"); + + Error thrown = assertThrows(Error.class, () -> Utils.handleThrowable(error)); + + assertSame(error, thrown); + } + + @Test + public void testHandleThrowableWrapsCheckedExceptionWithCause() { + Exception exception = new Exception("checked failure"); + + RuntimeException thrown = + assertThrows(RuntimeException.class, () -> Utils.handleThrowable(exception)); + + assertSame(exception, thrown.getCause()); + } +}