Skip to content

Commit d258090

Browse files
authored
Lucene: preserve throwable types in accelerated vector formats (#2515)
## Summary Replace the broad RuntimeException wrappers in fieldsWriter and fieldsReader across the plain, scalar quantized, and binary quantized accelerated vector formats with Utils.handleThrowable. This preserves IOException, RuntimeException, and Error instances instead of discarding their type, cause, and stack information. It also documents and tests the shared Error propagation contract. Add focused helper and vector format regression coverage. The call site tests verify that IOException and RuntimeException instances are propagated unchanged, while the helper test verifies exact Error identity. ## Testing * TestUtilsThrowableHandling: 4 tests passed * TestAcceleratedHNSWVectorsFormatThrowableHandling: 3 tests passed * Spotless check passed * git diff check passed The call site tests were also run against the previous wrappers and correctly failed for IOException and RuntimeException identity. Closes #2474 Authors: - https://github.com/shaunakkapur Approvers: - Igor Motov (https://github.com/imotov) URL: #2515
1 parent 9c9d359 commit d258090

6 files changed

Lines changed: 165 additions & 8 deletions

File tree

‎java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Lucene99AcceleratedHNSWVectorsFormat.java‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ public KnnVectorsWriter fieldsWriter(SegmentWriteState state) throws IOException
9191
acceleratedHNSWParams.getNumMergeWorkers(),
9292
new TaskExecutor(acceleratedHNSWParams.getMergeExec()));
9393
} catch (Exception e) {
94-
throw new RuntimeException(e.getMessage());
94+
throw Utils.handleThrowable(e);
9595
}
9696
}
9797
}
@@ -105,7 +105,7 @@ public KnnVectorsReader fieldsReader(SegmentReadState state) throws IOException
105105
return LUCENE_PROVIDER.getLuceneHnswVectorsReaderInstance(
106106
state, FLAT_VECTORS_FORMAT.fieldsReader(state));
107107
} catch (Exception e) {
108-
throw new RuntimeException(e.getMessage());
108+
throw Utils.handleThrowable(e);
109109
}
110110
}
111111

‎java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat.java‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ public KnnVectorsWriter fieldsWriter(SegmentWriteState state) throws IOException
8989
acceleratedHNSWParams.getMaxConn(), acceleratedHNSWParams.getBeamWidth());
9090
return fallbackFormat.fieldsWriter(state);
9191
} catch (Exception e) {
92-
throw new RuntimeException(e.getMessage());
92+
throw Utils.handleThrowable(e);
9393
}
9494
}
9595
}
@@ -103,7 +103,7 @@ public KnnVectorsReader fieldsReader(SegmentReadState state) throws IOException
103103
return LUCENE99_PROVIDER.getLuceneHnswVectorsReaderInstance(
104104
state, FLAT_VECTORS_FORMAT.fieldsReader(state));
105105
} catch (Exception e) {
106-
throw new RuntimeException(e.getMessage());
106+
throw Utils.handleThrowable(e);
107107
}
108108
}
109109

‎java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/LuceneAcceleratedHNSWScalarQuantizedVectorsFormat.java‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ public KnnVectorsWriter fieldsWriter(SegmentWriteState state) throws IOException
8181
acceleratedHNSWParams.getBeamWidth(), acceleratedHNSWParams.getMaxConn());
8282
return fallbackFormat.fieldsWriter(state);
8383
} catch (Exception e) {
84-
throw new RuntimeException(e.getMessage());
84+
throw Utils.handleThrowable(e);
8585
}
8686
}
8787
}
@@ -95,7 +95,7 @@ public KnnVectorsReader fieldsReader(SegmentReadState state) throws IOException
9595
return LUCENE_PROVIDER.getLuceneHnswVectorsReaderInstance(
9696
state, FLAT_VECTORS_FORMAT.fieldsReader(state));
9797
} catch (Exception e) {
98-
throw new RuntimeException(e.getMessage());
98+
throw Utils.handleThrowable(e);
9999
}
100100
}
101101

‎java/cuvs-lucene/src/main/java/com/nvidia/cuvs/lucene/Utils.java‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,20 @@ public class Utils {
2828
static final Logger log = Logger.getLogger(Utils.class.getName());
2929

3030
/**
31-
* A utility method that throws specific types of throwable objects based on types.
31+
* A utility method that rethrows known throwable types without changing their identity.
32+
*
33+
* <p>In particular, {@link Error} instances must not be converted to a {@link
34+
* RuntimeException}; callers rely on errors retaining their original type and stack trace.
35+
*
36+
* <p>This method never returns normally; its return type exists solely so callers can write
37+
* {@code throw handleThrowable(t);}, letting the compiler verify that the enclosing statement
38+
* always completes abruptly.
3239
*
3340
* @param t the throwable object
41+
* @return never returns; always throws
3442
* @throws IOException
3543
*/
36-
static void handleThrowable(Throwable t) throws IOException {
44+
static RuntimeException handleThrowable(Throwable t) throws IOException {
3745
switch (t) {
3846
case IOException ioe -> throw ioe;
3947
case Error error -> throw error;
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package com.nvidia.cuvs.lucene;
6+
7+
import java.io.IOException;
8+
import java.util.Map;
9+
import org.apache.lucene.codecs.Codec;
10+
import org.apache.lucene.codecs.KnnVectorsFormat;
11+
import org.apache.lucene.index.FieldInfos;
12+
import org.apache.lucene.index.SegmentInfo;
13+
import org.apache.lucene.index.SegmentReadState;
14+
import org.apache.lucene.store.ByteBuffersDirectory;
15+
import org.apache.lucene.store.Directory;
16+
import org.apache.lucene.store.FilterDirectory;
17+
import org.apache.lucene.store.IOContext;
18+
import org.apache.lucene.store.IndexInput;
19+
import org.apache.lucene.tests.util.LuceneTestCase;
20+
import org.apache.lucene.util.StringHelper;
21+
import org.apache.lucene.util.Version;
22+
import org.junit.Test;
23+
24+
public class TestAcceleratedHNSWVectorsFormatThrowableHandling extends LuceneTestCase {
25+
26+
@Test
27+
public void testReadersRethrowIOExceptionUnchanged() throws Exception {
28+
assertReaderFormatsRethrowUnchanged(new IOException("reader I/O failure"));
29+
}
30+
31+
@Test
32+
public void testReadersRethrowRuntimeExceptionUnchanged() throws Exception {
33+
assertReaderFormatsRethrowUnchanged(new IllegalStateException("reader runtime failure"));
34+
}
35+
36+
@Test
37+
public void testReadersRethrowErrorUnchanged() throws Exception {
38+
assertReaderFormatsRethrowUnchanged(new AssertionError("reader error"));
39+
}
40+
41+
private void assertReaderFormatsRethrowUnchanged(Throwable failure) throws Exception {
42+
for (KnnVectorsFormat format : readerFormats()) {
43+
try (Directory directory = new ThrowingDirectory(failure)) {
44+
SegmentReadState state = newSegmentReadState(directory);
45+
Throwable thrown = assertThrows(failure.getClass(), () -> format.fieldsReader(state));
46+
assertSame(format.getName(), failure, thrown);
47+
}
48+
}
49+
}
50+
51+
private static KnnVectorsFormat[] readerFormats() {
52+
return new KnnVectorsFormat[] {
53+
new Lucene99AcceleratedHNSWVectorsFormat(),
54+
new LuceneAcceleratedHNSWScalarQuantizedVectorsFormat(),
55+
new LuceneAcceleratedHNSWBinaryQuantizedVectorsFormat()
56+
};
57+
}
58+
59+
private static SegmentReadState newSegmentReadState(Directory directory) {
60+
SegmentInfo segmentInfo =
61+
new SegmentInfo(
62+
directory,
63+
Version.LATEST,
64+
Version.LATEST,
65+
"_0",
66+
0,
67+
false,
68+
false,
69+
Codec.getDefault(),
70+
Map.of(),
71+
StringHelper.randomId(),
72+
Map.of(),
73+
null);
74+
return new SegmentReadState(directory, segmentInfo, FieldInfos.EMPTY, IOContext.DEFAULT);
75+
}
76+
77+
private static final class ThrowingDirectory extends FilterDirectory {
78+
private final Throwable failure;
79+
80+
private ThrowingDirectory(Throwable failure) {
81+
super(new ByteBuffersDirectory());
82+
this.failure = failure;
83+
}
84+
85+
@Override
86+
public IndexInput openInput(String name, IOContext context) throws IOException {
87+
if (failure instanceof IOException ioe) {
88+
throw ioe;
89+
}
90+
if (failure instanceof RuntimeException runtimeException) {
91+
throw runtimeException;
92+
}
93+
if (failure instanceof Error error) {
94+
throw error;
95+
}
96+
throw new AssertionError("unexpected test throwable", failure);
97+
}
98+
}
99+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package com.nvidia.cuvs.lucene;
6+
7+
import java.io.IOException;
8+
import org.apache.lucene.tests.util.LuceneTestCase;
9+
import org.junit.Test;
10+
11+
public class TestUtilsThrowableHandling extends LuceneTestCase {
12+
13+
@Test
14+
public void testHandleThrowableRethrowsIOExceptionUnchanged() {
15+
IOException exception = new IOException("I/O failure");
16+
17+
IOException thrown = assertThrows(IOException.class, () -> Utils.handleThrowable(exception));
18+
19+
assertSame(exception, thrown);
20+
}
21+
22+
@Test
23+
public void testHandleThrowableRethrowsRuntimeExceptionUnchanged() {
24+
RuntimeException exception = new IllegalStateException("runtime failure");
25+
26+
RuntimeException thrown =
27+
assertThrows(RuntimeException.class, () -> Utils.handleThrowable(exception));
28+
29+
assertSame(exception, thrown);
30+
}
31+
32+
@Test
33+
public void testHandleThrowableRethrowsErrorUnchanged() {
34+
Error error = new AssertionError("fatal failure");
35+
36+
Error thrown = assertThrows(Error.class, () -> Utils.handleThrowable(error));
37+
38+
assertSame(error, thrown);
39+
}
40+
41+
@Test
42+
public void testHandleThrowableWrapsCheckedExceptionWithCause() {
43+
Exception exception = new Exception("checked failure");
44+
45+
RuntimeException thrown =
46+
assertThrows(RuntimeException.class, () -> Utils.handleThrowable(exception));
47+
48+
assertSame(exception, thrown.getCause());
49+
}
50+
}

0 commit comments

Comments
 (0)