From c66ed360738e925bcabcfd11c834c21d2e3d41dd Mon Sep 17 00:00:00 2001 From: Christian Tzolov Date: Fri, 22 Mar 2024 14:50:00 +0100 Subject: [PATCH] Allow Retry on choosen status codes - Add a new autoconfig property spring.ai.retry.on-http-codes that list status codes (scuh as 429) for which the retry should be attemptd. - Updated the docs. Resolves #433 --- .../ai/anthropic/AnthropicChatClientIT.java | 2 +- .../modules/ROOT/pages/api/chat/anthropic-chat.adoc | 3 ++- .../modules/ROOT/pages/api/chat/mistralai-chat.adoc | 1 + .../modules/ROOT/pages/api/chat/openai-chat.adoc | 1 + .../pages/api/embeddings/mistralai-embeddings.adoc | 1 + .../pages/api/embeddings/openai-embeddings.adoc | 1 + .../modules/ROOT/pages/api/image/openai-image.adoc | 1 + .../retry/SpringAiRetryAutoConfiguration.java | 13 +++++++------ .../retry/SpringAiRetryProperties.java | 13 +++++++++++++ .../retry/SpringAiRetryPropertiesTests.java | 8 +++++--- 10 files changed, 33 insertions(+), 11 deletions(-) diff --git a/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/AnthropicChatClientIT.java b/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/AnthropicChatClientIT.java index 4fe481ea4..8e4223241 100644 --- a/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/AnthropicChatClientIT.java +++ b/models/spring-ai-anthropic/src/test/java/org/springframework/ai/anthropic/AnthropicChatClientIT.java @@ -50,7 +50,7 @@ import org.springframework.util.MimeTypeUtils; import static org.assertj.core.api.Assertions.assertThat; -@SpringBootTest(classes = AnthropicTestConfiguration.class) +@SpringBootTest(classes = AnthropicTestConfiguration.class, properties = "spring.ai.retry.on-http-codes=429") @EnabledIfEnvironmentVariable(named = "ANTHROPIC_API_KEY", matches = ".+") class AnthropicChatClientIT { diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/anthropic-chat.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/anthropic-chat.adoc index edca8ea97..95340321f 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/anthropic-chat.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/anthropic-chat.adoc @@ -67,7 +67,8 @@ The prefix `spring.ai.retry` is used as the property prefix that lets you config | spring.ai.retry.backoff.multiplier | Backoff interval multiplier. | 5 | spring.ai.retry.backoff.max-interval | Maximum backoff duration. | 3 min. | spring.ai.retry.on-client-errors | If false, throw a NonTransientAiException, and do not attempt retry for `4xx` client error codes | false -| spring.ai.retry.exclude-on-http-codes | List of HTTP status codes that should not trigger a retry (e.g. to throw NonTransientAiException). | empty +| spring.ai.retry.exclude-on-http-codes | List of HTTP status codes that should NOT trigger a retry (e.g. to throw NonTransientAiException). | empty +| spring.ai.retry.on-http-codes | List of HTTP status codes that should trigger a retry (e.g. to throw TransientAiException). | empty |==== NOTE: currently the retry policies are not applicable for the streaming API. diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/mistralai-chat.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/mistralai-chat.adoc index 5395ae05b..b2a7cb2aa 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/mistralai-chat.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/mistralai-chat.adoc @@ -63,6 +63,7 @@ The prefix `spring.ai.retry` is used as the property prefix that lets you config | spring.ai.retry.backoff.max-interval | Maximum backoff duration. | 3 min. | spring.ai.retry.on-client-errors | If false, throw a NonTransientAiException, and do not attempt retry for `4xx` client error codes | false | spring.ai.retry.exclude-on-http-codes | List of HTTP status codes that should not trigger a retry (e.g. to throw NonTransientAiException). | empty +| spring.ai.retry.on-http-codes | List of HTTP status codes that should trigger a retry (e.g. to throw TransientAiException). | empty |==== ==== Connection Properties diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/openai-chat.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/openai-chat.adoc index 0f7ad12d5..cdbe9061b 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/openai-chat.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/chat/openai-chat.adoc @@ -63,6 +63,7 @@ The prefix `spring.ai.retry` is used as the property prefix that lets you config | spring.ai.retry.backoff.max-interval | Maximum backoff duration. | 3 min. | spring.ai.retry.on-client-errors | If false, throw a NonTransientAiException, and do not attempt retry for `4xx` client error codes | false | spring.ai.retry.exclude-on-http-codes | List of HTTP status codes that should not trigger a retry (e.g. to throw NonTransientAiException). | empty +| spring.ai.retry.on-http-codes | List of HTTP status codes that should trigger a retry (e.g. to throw TransientAiException). | empty |==== ==== Connection Properties diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/mistralai-embeddings.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/mistralai-embeddings.adoc index b049c030f..30e91ec73 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/mistralai-embeddings.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/mistralai-embeddings.adoc @@ -63,6 +63,7 @@ The prefix `spring.ai.retry` is used as the property prefix that lets you config | spring.ai.retry.backoff.max-interval | Maximum backoff duration. | 3 min. | spring.ai.retry.on-client-errors | If false, throw a NonTransientAiException, and do not attempt retry for `4xx` client error codes | false | spring.ai.retry.exclude-on-http-codes | List of HTTP status codes that should not trigger a retry (e.g. to throw NonTransientAiException). | empty +| spring.ai.retry.on-http-codes | List of HTTP status codes that should trigger a retry (e.g. to throw TransientAiException). | empty |==== ==== Connection Properties diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/openai-embeddings.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/openai-embeddings.adoc index 92d55a9d6..1933669b3 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/openai-embeddings.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/embeddings/openai-embeddings.adoc @@ -64,6 +64,7 @@ The prefix `spring.ai.retry` is used as the property prefix that lets you config | spring.ai.retry.backoff.max-interval | Maximum backoff duration. | 3 min. | spring.ai.retry.on-client-errors | If false, throw a NonTransientAiException, and do not attempt retry for `4xx` client error codes | false | spring.ai.retry.exclude-on-http-codes | List of HTTP status codes that should not trigger a retry (e.g. to throw NonTransientAiException). | empty +| spring.ai.retry.on-http-codes | List of HTTP status codes that should trigger a retry (e.g. to throw TransientAiException). | empty |==== ==== Connection Properties diff --git a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/image/openai-image.adoc b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/image/openai-image.adoc index 0fcef6cd7..ff544bd85 100644 --- a/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/image/openai-image.adoc +++ b/spring-ai-docs/src/main/antora/modules/ROOT/pages/api/image/openai-image.adoc @@ -89,6 +89,7 @@ The prefix `spring.ai.retry` is used as the property prefix that lets you config | spring.ai.retry.backoff.max-interval | Maximum backoff duration. | 3 min. | spring.ai.retry.on-client-errors | If false, throw a NonTransientAiException, and do not attempt retry for `4xx` client error codes | false | spring.ai.retry.exclude-on-http-codes | List of HTTP status codes that should not trigger a retry (e.g. to throw NonTransientAiException). | empty +| spring.ai.retry.on-http-codes | List of HTTP status codes that should trigger a retry (e.g. to throw TransientAiException). | empty |==== diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/retry/SpringAiRetryAutoConfiguration.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/retry/SpringAiRetryAutoConfiguration.java index df0b43d23..00c32ed67 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/retry/SpringAiRetryAutoConfiguration.java +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/retry/SpringAiRetryAutoConfiguration.java @@ -82,18 +82,19 @@ public class SpringAiRetryAutoConfiguration { if (response.getStatusCode().isError()) { String error = StreamUtils.copyToString(response.getBody(), StandardCharsets.UTF_8); String message = String.format("%s - %s", response.getStatusCode().value(), error); - /** - * Thrown on 4xx client errors, such as 401 - Incorrect API key - * provided, 401 - You must be a member of an organization to use the - * API, 429 - Rate limit reached for requests, 429 - You exceeded your - * current quota , please check your plan and billing details. - */ + + // Explicitly configured transient codes + if (properties.getOnHttpCodes().contains(response.getStatusCode().value())) { + throw new TransientAiException(message); + } + // onClientErrors - If true, do not throw a NonTransientAiException, // and do not attempt retry for 4xx client error codes, false by // default. if (!properties.isOnClientErrors() && response.getStatusCode().is4xxClientError()) { throw new NonTransientAiException(message); } + // Explicitly configured non-transient codes if (!CollectionUtils.isEmpty(properties.getExcludeOnHttpCodes()) && properties.getExcludeOnHttpCodes().contains(response.getStatusCode().value())) { diff --git a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/retry/SpringAiRetryProperties.java b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/retry/SpringAiRetryProperties.java index 1f70659d4..8b04b81e2 100644 --- a/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/retry/SpringAiRetryProperties.java +++ b/spring-ai-spring-boot-autoconfigure/src/main/java/org/springframework/ai/autoconfigure/retry/SpringAiRetryProperties.java @@ -54,6 +54,11 @@ public class SpringAiRetryProperties { */ private List excludeOnHttpCodes = new ArrayList<>(); + /** + * List of HTTP status codes that should trigger a retry. + */ + private List onHttpCodes = new ArrayList<>(); + /** * Exponential Backoff properties. */ @@ -128,4 +133,12 @@ public class SpringAiRetryProperties { this.onClientErrors = onClientErrors; } + public List getOnHttpCodes() { + return this.onHttpCodes; + } + + public void setOnHttpCodes(List onHttpCodes) { + this.onHttpCodes = onHttpCodes; + } + } diff --git a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/retry/SpringAiRetryPropertiesTests.java b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/retry/SpringAiRetryPropertiesTests.java index eba635f59..c663dfb3a 100644 --- a/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/retry/SpringAiRetryPropertiesTests.java +++ b/spring-ai-spring-boot-autoconfigure/src/test/java/org/springframework/ai/autoconfigure/retry/SpringAiRetryPropertiesTests.java @@ -37,10 +37,10 @@ public class SpringAiRetryPropertiesTests { var retryProperties = context.getBean(SpringAiRetryProperties.class); assertThat(retryProperties.getMaxAttempts()).isEqualTo(10); - assertThat(retryProperties.isOnClientErrors()).isFalse(); // do not retry - // on 4xx - // errors + // do not retry on 4xx errors + assertThat(retryProperties.isOnClientErrors()).isFalse(); assertThat(retryProperties.getExcludeOnHttpCodes()).isEmpty(); + assertThat(retryProperties.getOnHttpCodes()).isEmpty(); assertThat(retryProperties.getBackoff().getInitialInterval().toMillis()).isEqualTo(2000); assertThat(retryProperties.getBackoff().getMultiplier()).isEqualTo(5); assertThat(retryProperties.getBackoff().getMaxInterval().toMillis()).isEqualTo(3 * 60000); @@ -55,6 +55,7 @@ public class SpringAiRetryPropertiesTests { "spring.ai.retry.max-attempts=100", "spring.ai.retry.on-client-errors=false", "spring.ai.retry.exclude-on-http-codes=404,500", + "spring.ai.retry.on-http-codes=429", "spring.ai.retry.backoff.initial-interval=1000", "spring.ai.retry.backoff.multiplier=2", "spring.ai.retry.backoff.max-interval=60000" ) @@ -66,6 +67,7 @@ public class SpringAiRetryPropertiesTests { assertThat(retryProperties.getMaxAttempts()).isEqualTo(100); assertThat(retryProperties.isOnClientErrors()).isFalse(); assertThat(retryProperties.getExcludeOnHttpCodes()).containsExactly(404, 500); + assertThat(retryProperties.getOnHttpCodes()).containsExactly(429); assertThat(retryProperties.getBackoff().getInitialInterval().toMillis()).isEqualTo(1000); assertThat(retryProperties.getBackoff().getMultiplier()).isEqualTo(2); assertThat(retryProperties.getBackoff().getMaxInterval().toMillis()).isEqualTo(60000);