Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,37 +12,25 @@
*/
public final class RetrievalErrorClassifier {

/** Owns the stable diagnostic label and vector-store retry policy for each failure category. */
/** Owns the vector-store retry policy for each failure category. */
private enum RetrievalErrorCategory {
NOT_FOUND("404 Not Found", false),
UNAUTHORIZED("401 Unauthorized", false),
FORBIDDEN("403 Forbidden", false),
RATE_LIMITED("429 Rate Limited", true),
CONNECTION_ERROR("Connection Error", true),
EMBEDDING_SERVICE_UNAVAILABLE("Embedding Service Unavailable", false),
UNKNOWN("Unknown Error", false);
NOT_FOUND(false),
UNAUTHORIZED(false),
FORBIDDEN(false),
RATE_LIMITED(true),
CONNECTION_ERROR(true),
EMBEDDING_SERVICE_UNAVAILABLE(false),
UNKNOWN(false);

private final String errorLabel;
private final boolean retryableVectorStoreFailure;

RetrievalErrorCategory(String errorLabel, boolean retryableVectorStoreFailure) {
this.errorLabel = errorLabel;
RetrievalErrorCategory(boolean retryableVectorStoreFailure) {
this.retryableVectorStoreFailure = retryableVectorStoreFailure;
}
}

private RetrievalErrorClassifier() {}

/**
* Determines a stable error category from exception types, messages, and causes.
*
* @param failure failure encountered during retrieval
* @return normalized error category label
*/
public static String determineErrorType(Throwable failure) {
return classify(failure).errorLabel;
}

private static RetrievalErrorCategory classify(Throwable failure) {
return classifyGrpcStatus(failure).orElseGet(() -> classifyNonGrpcFailure(failure));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ class RetrievalErrorClassifierTest {
void classifiesDirectTimeoutExceptionAsTransient() {
TimeoutException directTimeoutException = new TimeoutException("Future took too long");

assertEquals("Connection Error", RetrievalErrorClassifier.determineErrorType(directTimeoutException));
assertTrue(RetrievalErrorClassifier.isTransientVectorStoreError(directTimeoutException));
}

Expand All @@ -35,7 +34,6 @@ void classifiesWrappedTimeoutExceptionAsTransient() {
IllegalStateException qdrantTimeoutFailure = new IllegalStateException(
"Qdrant operation timed out after 5s", new TimeoutException("Future did not complete"));

assertEquals("Connection Error", RetrievalErrorClassifier.determineErrorType(qdrantTimeoutFailure));
assertTrue(RetrievalErrorClassifier.isTransientVectorStoreError(qdrantTimeoutFailure));
}

Expand All @@ -44,7 +42,6 @@ void classifiesNullMessageTimeoutExceptionAsTransient() {
TimeoutException nullMessageTimeoutException = new TimeoutException();

assertNull(nullMessageTimeoutException.getMessage());
assertEquals("Connection Error", RetrievalErrorClassifier.determineErrorType(nullMessageTimeoutException));
assertTrue(RetrievalErrorClassifier.isTransientVectorStoreError(nullMessageTimeoutException));
}

Expand All @@ -53,31 +50,27 @@ void doesNotClassifyTimeoutLookalikeTextAsTransient() {
IllegalStateException timeoutConfigurationFailure =
new IllegalStateException("Qdrant timeout configuration is invalid");

assertEquals("Unknown Error", RetrievalErrorClassifier.determineErrorType(timeoutConfigurationFailure));
assertFalse(RetrievalErrorClassifier.isTransientVectorStoreError(timeoutConfigurationFailure));
}

@Test
void classifiesGrpcDeadlineExceededAsTransient() {
RuntimeException grpcDeadlineFailure = Status.DEADLINE_EXCEEDED.asRuntimeException();

assertEquals("Connection Error", RetrievalErrorClassifier.determineErrorType(grpcDeadlineFailure));
assertTrue(RetrievalErrorClassifier.isTransientVectorStoreError(grpcDeadlineFailure));
}

@Test
void classifiesGrpcUnavailableAsTransient() {
RuntimeException grpcUnavailableFailure = Status.UNAVAILABLE.asRuntimeException();

assertEquals("Connection Error", RetrievalErrorClassifier.determineErrorType(grpcUnavailableFailure));
assertTrue(RetrievalErrorClassifier.isTransientVectorStoreError(grpcUnavailableFailure));
}

@Test
void doesNotRetryGrpcResourceExhaustedFailure() {
RuntimeException grpcResourceExhaustedFailure = Status.RESOURCE_EXHAUSTED.asRuntimeException();

assertEquals("Unknown Error", RetrievalErrorClassifier.determineErrorType(grpcResourceExhaustedFailure));
assertFalse(RetrievalErrorClassifier.isTransientVectorStoreError(grpcResourceExhaustedFailure));
}

Expand All @@ -87,7 +80,6 @@ void prioritizesGrpcUnavailableOverHttp429Description() {
.withDescription("HTTP 429 from upstream proxy")
.asRuntimeException();

assertEquals("Connection Error", RetrievalErrorClassifier.determineErrorType(grpcUnavailableFailure));
assertTrue(RetrievalErrorClassifier.isTransientVectorStoreError(grpcUnavailableFailure));
}

Expand All @@ -97,15 +89,13 @@ void keepsGrpcResourceExhaustedWithHttp429DescriptionNonRetryable() {
.withDescription("HTTP 429 quota exhausted")
.asRuntimeException();

assertEquals("Unknown Error", RetrievalErrorClassifier.determineErrorType(grpcResourceExhaustedFailure));
assertFalse(RetrievalErrorClassifier.isTransientVectorStoreError(grpcResourceExhaustedFailure));
}

@Test
void doesNotRetryGrpcInvalidArgumentFailure() {
RuntimeException grpcInvalidArgumentFailure = Status.INVALID_ARGUMENT.asRuntimeException();

assertEquals("Unknown Error", RetrievalErrorClassifier.determineErrorType(grpcInvalidArgumentFailure));
assertFalse(RetrievalErrorClassifier.isTransientVectorStoreError(grpcInvalidArgumentFailure));
}

Expand All @@ -114,7 +104,6 @@ void classifiesWrappedGrpcStatusExceptionAsTransient() {
IllegalStateException wrappedGrpcFailure =
new IllegalStateException("Qdrant operation failed", Status.UNAVAILABLE.asException());

assertEquals("Connection Error", RetrievalErrorClassifier.determineErrorType(wrappedGrpcFailure));
assertTrue(RetrievalErrorClassifier.isTransientVectorStoreError(wrappedGrpcFailure));
}

Expand Down