From 76bc0dcd3dee316bc215421a1ebb73c19bbdace1 Mon Sep 17 00:00:00 2001 From: Kirill Kurdyukov Date: Mon, 15 Jun 2026 12:48:31 +0300 Subject: [PATCH 1/2] spring-ydb-retry: rename maxRetries to maxAttempts and bump to 0.10.0 Align the retry setting with Spring Retry semantics: maxAttempts now counts the total number of attempts including the initial execution, and the ydb.transaction.retry.max-retries property becomes max-attempts. @YdbTransactional override attributes use 0 (instead of -1) to inherit the global configuration value; negative values are rejected. Adds a version badge to the module README. Co-authored-by: Cursor --- spring-ydb-retry/CHANGELOG.md | 5 ++ spring-ydb-retry/README.md | 19 ++++--- spring-ydb-retry/pom.xml | 2 +- spring-ydb-retry/slo/README.md | 2 +- .../playground/chaos-aggressive/compose.yaml | 2 +- .../slo/playground/chaos/compose.yaml | 2 +- spring-ydb-retry/slo/pom.xml | 2 +- spring-ydb-retry/slo/src/README.md | 4 +- .../java/tech/ydb/slo/SloResultWriter.java | 2 +- .../src/main/resources/application.properties | 2 +- .../java/tech/ydb/retry/YdbRetryPolicy.java | 5 +- .../tech/ydb/retry/YdbRetryPolicyConfig.java | 54 ++++++++----------- .../tech/ydb/retry/YdbRetryProperties.java | 12 ++--- .../java/tech/ydb/retry/YdbTransactional.java | 31 ++++++----- .../ydb/retry/InterceptorTestSupport.java | 32 +++++------ .../NestedYdbTransactionalRetryTest.java | 8 +-- .../RetryStartsFreshTransactionTest.java | 6 +-- .../SqlExceptionStatusExtractionTest.java | 10 ++-- .../TransactionPropagationRetryTest.java | 8 +-- .../retry/TransactionalDefaultRetryTest.java | 24 ++++----- .../ydb/retry/YdbRetryPolicyConfigTest.java | 36 ++++++------- .../YdbTransactionInterceptorFactoryTest.java | 2 +- ...bTransactionInterceptorInvocationTest.java | 2 +- ...YdbTransactionInterceptorReplacerTest.java | 2 +- .../YdbTransactionalConfigOverrideTest.java | 54 +++++++++---------- .../integration/HappyPathIntegrationTest.java | 2 +- .../integration/MaxRetriesExhaustedTest.java | 4 +- .../NonRetryableCommitIntegrationTest.java | 2 +- .../retry/integration/app/UserService.java | 6 +-- .../resources/application-enabled.properties | 2 +- 30 files changed, 174 insertions(+), 170 deletions(-) diff --git a/spring-ydb-retry/CHANGELOG.md b/spring-ydb-retry/CHANGELOG.md index c30204f5..0928abcc 100644 --- a/spring-ydb-retry/CHANGELOG.md +++ b/spring-ydb-retry/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.10.0 ## + +- Renamed the `maxRetries` retry setting to `maxAttempts`, which now counts the total number of attempts including the initial execution (aligned with Spring Retry semantics). The `ydb.transaction.retry.max-retries` property is renamed to `ydb.transaction.retry.max-attempts`. +- `@YdbTransactional` override attributes now use `0` (instead of `-1`) to inherit the global configuration value; negative values are rejected. + ## 0.9.0 ## - First version of the plugin diff --git a/spring-ydb-retry/README.md b/spring-ydb-retry/README.md index e2fc576d..65eb5ab3 100644 --- a/spring-ydb-retry/README.md +++ b/spring-ydb-retry/README.md @@ -1,5 +1,9 @@ # Spring YDB Retry +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/ydb-platform/ydb-java-dialects/blob/main/LICENSE.md) +[![Maven metadata URL](https://img.shields.io/maven-metadata/v?metadataUrl=https%3A%2F%2Frepo1.maven.org%2Fmaven2%2Ftech%2Fydb%2Fspring-ydb-retry%2Fmaven-metadata.xml)](https://mvnrepository.com/artifact/tech.ydb/spring-ydb-retry) +[![CI](https://img.shields.io/github/actions/workflow/status/ydb-platform/ydb-java-dialects/ci-spring-ydb-retry.yaml?branch=main&label=CI)](https://github.com/ydb-platform/ydb-java-dialects/actions/workflows/ci-spring-ydb-retry.yaml) + ## Overview This project is a Spring Boot auto-configuration module that provides automatic retry @@ -8,7 +12,7 @@ for transactional operations with [YDB](https://ydb.tech). ### Features - Automatic retry of failed `@Transactional` methods on YDB retryable status codes -- `@YdbTransactional` annotation with per-method retry settings (maxRetries, backoff, idempotency) +- `@YdbTransactional` annotation with per-method retry settings (maxAttempts, backoff, idempotency) - Dual backoff strategy (fast/slow) with jitter tailored to YDB error semantics - Idempotent mode for extended retry coverage on non-deterministic status codes - Fully configurable via `application.properties` @@ -27,6 +31,7 @@ for transactional operations with [YDB](https://ydb.tech). For Maven, add the following dependency to your pom.xml: ```xml + tech.ydb spring-ydb-retry @@ -54,9 +59,10 @@ Use `@YdbTransactional` as a drop-in replacement for `@Transactional` with addit retry parameters: ```java -@YdbTransactional(maxRetries = 5, idempotent = true) + +@YdbTransactional(maxAttempts = 5, idempotent = true) public void save(User user) { - // retried up to 5 times on YDB retryable errors + // executed up to 5 times in total (initial attempt + up to 4 retries) on YDB retryable errors } ``` @@ -67,14 +73,11 @@ Configure retry behavior in `application.properties`: ```properties # Enable/disable retry (default: true) ydb.transaction.retry.enabled=true - -# Maximum retry attempts (default: 10) -ydb.transaction.retry.max-retries=10 - +# Maximum total attempts, counting the initial execution (default: 10) +ydb.transaction.retry.max-attempts=10 # Backoff settings for slow errors ydb.transaction.retry.slow-backoff-base-ms=50 ydb.transaction.retry.slow-cap-backoff-ms=5000 - # Backoff settings for fast errors ydb.transaction.retry.fast-backoff-base-ms=5 ydb.transaction.retry.fast-cap-backoff-ms=500 diff --git a/spring-ydb-retry/pom.xml b/spring-ydb-retry/pom.xml index f1dd0568..d039b2d5 100644 --- a/spring-ydb-retry/pom.xml +++ b/spring-ydb-retry/pom.xml @@ -7,7 +7,7 @@ tech.ydb spring-ydb-retry - 0.9.0 + 0.10.0 jar Spring YDB Retry diff --git a/spring-ydb-retry/slo/README.md b/spring-ydb-retry/slo/README.md index 3a49a295..0e51cec2 100644 --- a/spring-ydb-retry/slo/README.md +++ b/spring-ydb-retry/slo/README.md @@ -104,7 +104,7 @@ Environment variables for the app containers: | `SERVER_PORT` | 8080 | HTTP port | | `SPRING_DATASOURCE_URL` | - | YDB JDBC URL | | `YDB_TRANSACTION_RETRY_ENABLED` | true | Enable/disable retry | -| `YDB_TRANSACTION_RETRY_MAX_RETRIES` | 10 | Max retry attempts | +| `YDB_TRANSACTION_RETRY_MAX_ATTEMPTS` | 10 | Max total attempts (incl. initial execution) | | `SLO_RUN_ID` | auto | Shared run identifier used for result folder name | | `SLO_RESULTS_DIR` | `/app/results` in Docker | Root directory for saved run results | | `REF` | unknown | Label for metrics (with-retry / no-retry) | diff --git a/spring-ydb-retry/slo/playground/chaos-aggressive/compose.yaml b/spring-ydb-retry/slo/playground/chaos-aggressive/compose.yaml index abd3c649..ed90f408 100644 --- a/spring-ydb-retry/slo/playground/chaos-aggressive/compose.yaml +++ b/spring-ydb-retry/slo/playground/chaos-aggressive/compose.yaml @@ -290,7 +290,7 @@ services: SPRING_DATASOURCE_URL: jdbc:ydb:grpc://static-0:2135/Root/testdb SPRING_DATASOURCE_DRIVER_CLASS_NAME: tech.ydb.jdbc.YdbDriver YDB_TRANSACTION_RETRY_ENABLED: "true" - YDB_TRANSACTION_RETRY_MAX_RETRIES: "10" + YDB_TRANSACTION_RETRY_MAX_ATTEMPTS: "10" REF: with-retry SLO_RUN_ID: ${SLO_RUN_ID:-} SLO_RESULTS_DIR: /app/results diff --git a/spring-ydb-retry/slo/playground/chaos/compose.yaml b/spring-ydb-retry/slo/playground/chaos/compose.yaml index 61cf3dfb..e2977f8a 100644 --- a/spring-ydb-retry/slo/playground/chaos/compose.yaml +++ b/spring-ydb-retry/slo/playground/chaos/compose.yaml @@ -282,7 +282,7 @@ services: SPRING_DATASOURCE_URL: jdbc:ydb:grpc://static-0:2135/Root/testdb SPRING_DATASOURCE_DRIVER_CLASS_NAME: tech.ydb.jdbc.YdbDriver YDB_TRANSACTION_RETRY_ENABLED: "true" - YDB_TRANSACTION_RETRY_MAX_RETRIES: "10" + YDB_TRANSACTION_RETRY_MAX_ATTEMPTS: "10" REF: with-retry SLO_RUN_ID: ${SLO_RUN_ID:-} SLO_RESULTS_DIR: /app/results diff --git a/spring-ydb-retry/slo/pom.xml b/spring-ydb-retry/slo/pom.xml index 6f9825c6..5e80ef2d 100644 --- a/spring-ydb-retry/slo/pom.xml +++ b/spring-ydb-retry/slo/pom.xml @@ -57,7 +57,7 @@ tech.ydb spring-ydb-retry - 0.9.0 + 0.10.0 io.opentelemetry diff --git a/spring-ydb-retry/slo/src/README.md b/spring-ydb-retry/slo/src/README.md index 7f7bca6b..bc3640ab 100644 --- a/spring-ydb-retry/slo/src/README.md +++ b/spring-ydb-retry/slo/src/README.md @@ -23,7 +23,7 @@ java -jar target/ydb-slo-workload-1.0.0-SNAPSHOT-exec.jar \ --server.port=8081 \ --spring.datasource.url=jdbc:ydb:grpc://localhost:2136/Root/testdb \ --ydb.transaction.retry.enabled=true \ - --ydb.transaction.retry.max-retries=10 \ + --ydb.transaction.retry.max-attempts=10 \ --slo.ref=with-retry # Without retry @@ -76,7 +76,7 @@ All parameters are set via environment variables (or Spring Boot command-line ar | `SERVER_PORT` | `8080` | HTTP port (Actuator endpoints) | | `SPRING_DATASOURCE_URL` | `jdbc:ydb:grpc://localhost:2136/Root/testdb` | YDB JDBC URL | | `YDB_TRANSACTION_RETRY_ENABLED` | `true` | Enable/disable retry | -| `YDB_TRANSACTION_RETRY_MAX_RETRIES` | `10` | Max retry attempts | +| `YDB_TRANSACTION_RETRY_MAX_ATTEMPTS` | `10` | Max total attempts (incl. initial execution) | | `SLO_RUN_ID` | auto | Shared run identifier used for the result folder name | | `SLO_RESULTS_DIR` | `results` | Root directory where per-run result folders are stored | diff --git a/spring-ydb-retry/slo/src/main/java/tech/ydb/slo/SloResultWriter.java b/spring-ydb-retry/slo/src/main/java/tech/ydb/slo/SloResultWriter.java index 58027767..aa0ccb1a 100644 --- a/spring-ydb-retry/slo/src/main/java/tech/ydb/slo/SloResultWriter.java +++ b/spring-ydb-retry/slo/src/main/java/tech/ydb/slo/SloResultWriter.java @@ -120,7 +120,7 @@ private String buildRunSummaryText( builder.append("runTimeSeconds: ").append(config.getRunTimeSeconds()).append('\n'); builder.append('\n'); builder.append("retryEnabled: ").append(retryProperties.isEnabled()).append('\n'); - builder.append("retryMaxRetries: ").append(retryProperties.getMaxRetries()).append('\n'); + builder.append("retryMaxAttempts: ").append(retryProperties.getMaxAttempts()).append('\n'); builder.append("retrySlowBackoffBaseMs: ") .append(retryProperties.getSlowBackoffBaseMs()) .append('\n'); diff --git a/spring-ydb-retry/slo/src/main/resources/application.properties b/spring-ydb-retry/slo/src/main/resources/application.properties index 34fdbddb..a2c37853 100644 --- a/spring-ydb-retry/slo/src/main/resources/application.properties +++ b/spring-ydb-retry/slo/src/main/resources/application.properties @@ -5,7 +5,7 @@ spring.datasource.driver-class-name=tech.ydb.jdbc.YdbDriver spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration ydb.transaction.retry.enabled=${YDB_TRANSACTION_RETRY_ENABLED:true} -ydb.transaction.retry.max-retries=${YDB_TRANSACTION_RETRY_MAX_RETRIES:10} +ydb.transaction.retry.max-attempts=${YDB_TRANSACTION_RETRY_MAX_ATTEMPTS:10} slo.read-rps=${SLO_READ_RPS:100} slo.write-rps=${SLO_WRITE_RPS:100} diff --git a/spring-ydb-retry/src/main/java/tech/ydb/retry/YdbRetryPolicy.java b/spring-ydb-retry/src/main/java/tech/ydb/retry/YdbRetryPolicy.java index dcd7c505..07f4c7b0 100644 --- a/spring-ydb-retry/src/main/java/tech/ydb/retry/YdbRetryPolicy.java +++ b/spring-ydb-retry/src/main/java/tech/ydb/retry/YdbRetryPolicy.java @@ -75,7 +75,10 @@ public static int extractVendorCode(Throwable error) { */ public static OptionalLong getNextRetryDelayMs( int vendorCode, int attempt, YdbRetryPolicyConfig config, boolean idempotent) { - if (attempt >= config.getMaxRetries()) { + // {@code attempt} is the zero-based index of the attempt that has just failed, so the next + // attempt is allowed only while we stay within the total {@code maxAttempts} budget + // (which counts the initial execution). + if (attempt + 1 >= config.getMaxAttempts()) { return OptionalLong.empty(); } if (vendorCode == 0) { diff --git a/spring-ydb-retry/src/main/java/tech/ydb/retry/YdbRetryPolicyConfig.java b/spring-ydb-retry/src/main/java/tech/ydb/retry/YdbRetryPolicyConfig.java index ac9a3a50..2b7f1a11 100644 --- a/spring-ydb-retry/src/main/java/tech/ydb/retry/YdbRetryPolicyConfig.java +++ b/spring-ydb-retry/src/main/java/tech/ydb/retry/YdbRetryPolicyConfig.java @@ -14,14 +14,14 @@ */ public final class YdbRetryPolicyConfig { public static final boolean DEFAULT_ENABLED = true; - public static final int DEFAULT_MAX_RETRIES = 10; + public static final int DEFAULT_MAX_ATTEMPTS = 10; public static final int DEFAULT_SLOW_BACKOFF_BASE_MS = 50; public static final int DEFAULT_FAST_BACKOFF_BASE_MS = 5; public static final int DEFAULT_SLOW_CAP_BACKOFF_MS = 5_000; public static final int DEFAULT_FAST_CAP_BACKOFF_MS = 500; private final boolean enabled; - private final int maxRetries; + private final int maxAttempts; private final int slowBackoffBaseMs; private final int fastBackoffBaseMs; private final int slowCapBackoffMs; @@ -32,7 +32,7 @@ public final class YdbRetryPolicyConfig { public YdbRetryPolicyConfig() { this( DEFAULT_ENABLED, - DEFAULT_MAX_RETRIES, + DEFAULT_MAX_ATTEMPTS, DEFAULT_SLOW_BACKOFF_BASE_MS, DEFAULT_FAST_BACKOFF_BASE_MS, DEFAULT_SLOW_CAP_BACKOFF_MS, @@ -41,13 +41,13 @@ public YdbRetryPolicyConfig() { public YdbRetryPolicyConfig( boolean enabled, - int maxRetries, + int maxAttempts, int slowBackoffBaseMs, int fastBackoffBaseMs, int slowCapBackoffMs, int fastCapBackoffMs) { - if (maxRetries < 1) { - throw new IllegalArgumentException("maxRetries must be >= 1"); + if (maxAttempts < 0) { + throw new IllegalArgumentException("maxAttempts must be >= 0"); } if (slowBackoffBaseMs < 0 || fastBackoffBaseMs < 0 @@ -56,7 +56,7 @@ public YdbRetryPolicyConfig( throw new IllegalArgumentException("backoff values must be >= 0"); } this.enabled = enabled; - this.maxRetries = maxRetries; + this.maxAttempts = maxAttempts; this.slowBackoffBaseMs = slowBackoffBaseMs; this.fastBackoffBaseMs = fastBackoffBaseMs; this.slowCapBackoffMs = slowCapBackoffMs; @@ -69,8 +69,8 @@ public boolean isEnabled() { return enabled; } - public int getMaxRetries() { - return maxRetries; + public int getMaxAttempts() { + return maxAttempts; } public int getSlowBackoffBaseMs() { @@ -103,36 +103,26 @@ public YdbRetryPolicyConfig merge(@Nullable YdbTransactional transactionPolicy) } return new YdbRetryPolicyConfig( enabled && transactionPolicy.enabled(), - mergeMaxRetries(transactionPolicy.maxRetries(), maxRetries), - mergeNonNegativeInt( + mergeOverride("maxAttempts", transactionPolicy.maxAttempts(), maxAttempts), + mergeOverride( "slowBackoffBaseMs", transactionPolicy.slowBackoffBaseMs(), slowBackoffBaseMs), - mergeNonNegativeInt( + mergeOverride( "fastBackoffBaseMs", transactionPolicy.fastBackoffBaseMs(), fastBackoffBaseMs), - mergeNonNegativeInt( + mergeOverride( "slowCapBackoffMs", transactionPolicy.slowCapBackoffMs(), slowCapBackoffMs), - mergeNonNegativeInt( + mergeOverride( "fastCapBackoffMs", transactionPolicy.fastCapBackoffMs(), fastCapBackoffMs)); } - private static int mergeMaxRetries(int candidate, int fallback) { - return switch (candidate) { - case -1 -> fallback; - case 0 -> throw new IllegalArgumentException( - "maxRetries must not be 0; use enabled = false to disable retry"); - default -> { - if (candidate < -1) { - throw new IllegalArgumentException("maxRetries must be -1 or >= 1"); - } - yield candidate; - } - }; - } - - private static int mergeNonNegativeInt(String name, int candidate, int fallback) { - if (candidate < -1) { - throw new IllegalArgumentException(String.format("%s is invalid", name)); + /** + * Resolves a per-method override against the global value: {@code 0} inherits the global value, + * a positive value overrides it, and a negative value is rejected. + */ + private static int mergeOverride(String name, int candidate, int fallback) { + if (candidate < 0) { + throw new IllegalArgumentException(name + " must be >= 0"); } - return candidate == -1 ? fallback : candidate; + return candidate == 0 ? fallback : candidate; } /** diff --git a/spring-ydb-retry/src/main/java/tech/ydb/retry/YdbRetryProperties.java b/spring-ydb-retry/src/main/java/tech/ydb/retry/YdbRetryProperties.java index 6e8033ce..543fb6a3 100644 --- a/spring-ydb-retry/src/main/java/tech/ydb/retry/YdbRetryProperties.java +++ b/spring-ydb-retry/src/main/java/tech/ydb/retry/YdbRetryProperties.java @@ -6,7 +6,7 @@ public class YdbRetryProperties { private boolean enabled = YdbRetryPolicyConfig.DEFAULT_ENABLED; - private int maxRetries = YdbRetryPolicyConfig.DEFAULT_MAX_RETRIES; + private int maxAttempts = YdbRetryPolicyConfig.DEFAULT_MAX_ATTEMPTS; private int slowBackoffBaseMs = YdbRetryPolicyConfig.DEFAULT_SLOW_BACKOFF_BASE_MS; private int fastBackoffBaseMs = YdbRetryPolicyConfig.DEFAULT_FAST_BACKOFF_BASE_MS; private int slowCapBackoffMs = YdbRetryPolicyConfig.DEFAULT_SLOW_CAP_BACKOFF_MS; @@ -20,12 +20,12 @@ public void setEnabled(boolean enabled) { this.enabled = enabled; } - public int getMaxRetries() { - return maxRetries; + public int getMaxAttempts() { + return maxAttempts; } - public void setMaxRetries(int maxRetries) { - this.maxRetries = maxRetries; + public void setMaxAttempts(int maxAttempts) { + this.maxAttempts = maxAttempts; } public int getSlowBackoffBaseMs() { @@ -63,7 +63,7 @@ public void setFastCapBackoffMs(int fastCapBackoffMs) { public YdbRetryPolicyConfig toConfig() { return new YdbRetryPolicyConfig( enabled, - maxRetries, + maxAttempts, slowBackoffBaseMs, fastBackoffBaseMs, slowCapBackoffMs, diff --git a/spring-ydb-retry/src/main/java/tech/ydb/retry/YdbTransactional.java b/spring-ydb-retry/src/main/java/tech/ydb/retry/YdbTransactional.java index e53c4690..48d3b563 100644 --- a/spring-ydb-retry/src/main/java/tech/ydb/retry/YdbTransactional.java +++ b/spring-ydb-retry/src/main/java/tech/ydb/retry/YdbTransactional.java @@ -70,36 +70,41 @@ boolean enabled() default true; /** - * Specifies the maximum number of retry attempts after the initial failed execution. - * The annotated method may be executed up to {@code maxRetries + 1} times in total. - * Use {@code -1} to inherit the value from the global retry configuration. - * A value of {@code 0} is not supported; use {@code enabled() = false} to disable retry. + * Specifies the maximum total number of attempts, counting the initial execution. + * For example, {@code maxAttempts = 3} allows the initial attempt plus up to two retries, + * while {@code maxAttempts = 1} executes the method exactly once without retries. + * Use {@code 0} to inherit the value from the global retry configuration. + * Negative values are not allowed. */ - int maxRetries() default -1; + int maxAttempts() default 0; /** * Overrides the base delay in milliseconds for the slow backoff strategy. - * Use {@code -1} to inherit the value from the global retry configuration. + * Use {@code 0} to inherit the value from the global retry configuration. + * Negative values are not allowed. */ - int slowBackoffBaseMs() default -1; + int slowBackoffBaseMs() default 0; /** * Overrides the base delay in milliseconds for the fast backoff strategy. - * Use {@code -1} to inherit the value from the global retry configuration. + * Use {@code 0} to inherit the value from the global retry configuration. + * Negative values are not allowed. */ - int fastBackoffBaseMs() default -1; + int fastBackoffBaseMs() default 0; /** * Overrides the maximum delay in milliseconds for the slow backoff strategy. - * Use {@code -1} to inherit the value from the global retry configuration. + * Use {@code 0} to inherit the value from the global retry configuration. + * Negative values are not allowed. */ - int slowCapBackoffMs() default -1; + int slowCapBackoffMs() default 0; /** * Overrides the maximum delay in milliseconds for the fast backoff strategy. - * Use {@code -1} to inherit the value from the global retry configuration. + * Use {@code 0} to inherit the value from the global retry configuration. + * Negative values are not allowed. */ - int fastCapBackoffMs() default -1; + int fastCapBackoffMs() default 0; /** * Marks the transactional method as idempotent for YDB retries. diff --git a/spring-ydb-retry/src/test/java/tech/ydb/retry/InterceptorTestSupport.java b/spring-ydb-retry/src/test/java/tech/ydb/retry/InterceptorTestSupport.java index a8b68ba7..4ffc03d1 100644 --- a/spring-ydb-retry/src/test/java/tech/ydb/retry/InterceptorTestSupport.java +++ b/spring-ydb-retry/src/test/java/tech/ydb/retry/InterceptorTestSupport.java @@ -22,15 +22,15 @@ void cleanupTransactionContext() { } static TestableInterceptor interceptorWithConfig( - boolean enabled, int maxRetries, int slowBase, int fastBase, int slowCap, int fastCap) { + boolean enabled, int maxAttempts, int slowBase, int fastBase, int slowCap, int fastCap) { return interceptorWithSleeper( - enabled, maxRetries, slowBase, fastBase, slowCap, fastCap, delay -> { + enabled, maxAttempts, slowBase, fastBase, slowCap, fastCap, delay -> { }); } static TestableInterceptor interceptorWithSleeper( boolean enabled, - int maxRetries, + int maxAttempts, int slowBase, int fastBase, int slowCap, @@ -38,7 +38,7 @@ static TestableInterceptor interceptorWithSleeper( BackoffSleeper sleeper) { TestableInterceptor interceptor = new TestableInterceptor( - new YdbRetryPolicyConfig(enabled, maxRetries, slowBase, fastBase, slowCap, fastCap), + new YdbRetryPolicyConfig(enabled, maxAttempts, slowBase, fastBase, slowCap, fastCap), sleeper); interceptor.setTransactionAttributeSource(new AnnotationTransactionAttributeSource()); return interceptor; @@ -123,27 +123,27 @@ public String regularTx() { } static class YdbTransactionalTestService { - @YdbTransactional(maxRetries = 2) + @YdbTransactional(maxAttempts = 3) public String ydbCustomRetry() { return "ok"; } - @YdbTransactional(maxRetries = 5) + @YdbTransactional(maxAttempts = 6) public String ydbRequiredRetry() { return "ok"; } - @YdbTransactional(maxRetries = 2, propagation = Propagation.REQUIRES_NEW) + @YdbTransactional(maxAttempts = 3, propagation = Propagation.REQUIRES_NEW) public String ydbRequiresNewRetry() { return "ok"; } - @YdbTransactional(maxRetries = 3, propagation = Propagation.NESTED) + @YdbTransactional(maxAttempts = 4, propagation = Propagation.NESTED) public String ydbNestedRetry() { return "ok"; } - @YdbTransactional(maxRetries = 3, propagation = Propagation.NOT_SUPPORTED) + @YdbTransactional(maxAttempts = 4, propagation = Propagation.NOT_SUPPORTED) public String ydbNotSupportedRetry() { return "ok"; } @@ -174,7 +174,7 @@ public String ydbTimeoutString() { } @YdbTransactional( - maxRetries = 100, + maxAttempts = 100, slowBackoffBaseMs = 200, fastBackoffBaseMs = 10, slowCapBackoffMs = 10000, @@ -183,22 +183,22 @@ public String ydbNewTransactionSettings() { return "ok"; } - @YdbTransactional(maxRetries = -2) - public String ydbNegativeMaxRetries() { + @YdbTransactional(maxAttempts = -2) + public String ydbNegativeMaxAttempts() { return "ok"; } - @YdbTransactional(maxRetries = 0) - public String ydbZeroMaxRetries() { + @YdbTransactional(maxAttempts = 0) + public String ydbZeroMaxAttempts() { return "ok"; } - @YdbTransactional(maxRetries = 5, idempotent = true) + @YdbTransactional(maxAttempts = 6, idempotent = true) public String ydbIdempotentRetry() { return "ok"; } - @YdbTransactional(maxRetries = 3) + @YdbTransactional(maxAttempts = 4) public String ydbNonIdempotentRetry() { return "ok"; } diff --git a/spring-ydb-retry/src/test/java/tech/ydb/retry/NestedYdbTransactionalRetryTest.java b/spring-ydb-retry/src/test/java/tech/ydb/retry/NestedYdbTransactionalRetryTest.java index d51fedf1..fe734ed1 100644 --- a/spring-ydb-retry/src/test/java/tech/ydb/retry/NestedYdbTransactionalRetryTest.java +++ b/spring-ydb-retry/src/test/java/tech/ydb/retry/NestedYdbTransactionalRetryTest.java @@ -98,13 +98,13 @@ void setSelf(NestedService self) { this.self = self; } - @YdbTransactional(maxRetries = 5) + @YdbTransactional(maxAttempts = 6) public void outer() { outerCount.incrementAndGet(); self.inner(); } - @YdbTransactional(maxRetries = 5) + @YdbTransactional(maxAttempts = 6) public void inner() { innerCount.incrementAndGet(); if (innerCount.get() == 1) { @@ -115,13 +115,13 @@ public void inner() { } } - @YdbTransactional(maxRetries = 5) + @YdbTransactional(maxAttempts = 6) public void outerWithFailingInner() { outerCount.incrementAndGet(); self.failingInner(); } - @YdbTransactional(maxRetries = 5) + @YdbTransactional(maxAttempts = 6) public void failingInner() { innerCount.incrementAndGet(); throw new ConfigurableInnerException(); diff --git a/spring-ydb-retry/src/test/java/tech/ydb/retry/RetryStartsFreshTransactionTest.java b/spring-ydb-retry/src/test/java/tech/ydb/retry/RetryStartsFreshTransactionTest.java index 7d23fd9a..dcc53f02 100644 --- a/spring-ydb-retry/src/test/java/tech/ydb/retry/RetryStartsFreshTransactionTest.java +++ b/spring-ydb-retry/src/test/java/tech/ydb/retry/RetryStartsFreshTransactionTest.java @@ -60,7 +60,7 @@ void shouldRollbackEveryAttemptAndPropagateWhenMaxRetriesExhausted() { assertThrows(ConfigurableStatusException.class, () -> interceptor.invoke(invocation)); - assertEquals(3, txManager.beginCount(), "max-retries + 1 attempts must each begin a tx"); + assertEquals(3, txManager.beginCount(), "every attempt up to maxAttempts must begin a tx"); assertEquals(3, txManager.rollbackCount(), "all three attempts must roll back"); assertEquals(0, txManager.commitCount(), "nothing must be committed"); } @@ -96,9 +96,9 @@ void shouldBeginAndCommitOnceForHappyPath() throws Throwable { } private static YdbTransactionInterceptor newInterceptor( - PlatformTransactionManager txManager, int maxRetries) { + PlatformTransactionManager txManager, int maxAttempts) { YdbTransactionInterceptor interceptor = new YdbTransactionInterceptor( - new YdbRetryPolicyConfig(true, maxRetries, 0, 0, 0, 0), delay -> { + new YdbRetryPolicyConfig(true, maxAttempts, 0, 0, 0, 0), delay -> { }); interceptor.setTransactionAttributeSource(new AnnotationTransactionAttributeSource()); interceptor.setTransactionManager(txManager); diff --git a/spring-ydb-retry/src/test/java/tech/ydb/retry/SqlExceptionStatusExtractionTest.java b/spring-ydb-retry/src/test/java/tech/ydb/retry/SqlExceptionStatusExtractionTest.java index e687cae7..30d174f1 100644 --- a/spring-ydb-retry/src/test/java/tech/ydb/retry/SqlExceptionStatusExtractionTest.java +++ b/spring-ydb-retry/src/test/java/tech/ydb/retry/SqlExceptionStatusExtractionTest.java @@ -19,7 +19,7 @@ class SqlExceptionStatusExtractionTest extends InterceptorTestSupport { @Test void shouldRetryWhenSqlExceptionDirectlyCarriesRetryableStatus() throws Throwable { - TestableInterceptor interceptor = interceptorWithConfig(true, 3, 0, 0, 0, 0); + TestableInterceptor interceptor = interceptorWithConfig(true, 4, 0, 0, 0, 0); interceptor.enqueueOutcome(plainSqlException(BAD_SESSION.getCode()), "ok"); Object result = interceptor.invoke(invocationFor("regularTx")); @@ -30,7 +30,7 @@ void shouldRetryWhenSqlExceptionDirectlyCarriesRetryableStatus() throws Throwabl @Test void shouldRetryWhenSqlExceptionIsBuriedInSpringDataAccessWrapper() throws Throwable { - TestableInterceptor interceptor = interceptorWithConfig(true, 3, 0, 0, 0, 0); + TestableInterceptor interceptor = interceptorWithConfig(true, 4, 0, 0, 0, 0); DataIntegrityViolationException wrapper = new DataIntegrityViolationException( "wrapped", plainSqlException(ABORTED.getCode())); interceptor.enqueueOutcome(wrapper, "ok"); @@ -43,7 +43,7 @@ void shouldRetryWhenSqlExceptionIsBuriedInSpringDataAccessWrapper() throws Throw @Test void shouldNotRetryWhenSqlExceptionHasNonRetryableStatus() { - TestableInterceptor interceptor = interceptorWithConfig(true, 5, 0, 0, 0, 0); + TestableInterceptor interceptor = interceptorWithConfig(true, 6, 0, 0, 0, 0); interceptor.enqueueOutcome(plainSqlException(SCHEME_ERROR.getCode())); SQLException thrown = @@ -55,7 +55,7 @@ void shouldNotRetryWhenSqlExceptionHasNonRetryableStatus() { @Test void shouldNotRetryWhenSqlExceptionHasZeroVendorCode() { - TestableInterceptor interceptor = interceptorWithConfig(true, 5, 0, 0, 0, 0); + TestableInterceptor interceptor = interceptorWithConfig(true, 6, 0, 0, 0, 0); interceptor.enqueueOutcome(new SQLException("non-ydb driver", null, 0)); assertThrows(SQLException.class, () -> interceptor.invoke(invocationFor("regularTx"))); @@ -64,7 +64,7 @@ void shouldNotRetryWhenSqlExceptionHasZeroVendorCode() { @Test void shouldNotRetryWhenSqlExceptionHasUnknownVendorCode() { - TestableInterceptor interceptor = interceptorWithConfig(true, 5, 0, 0, 0, 0); + TestableInterceptor interceptor = interceptorWithConfig(true, 6, 0, 0, 0, 0); interceptor.enqueueOutcome(new SQLException("some-other-driver", null, 12345)); assertThrows(SQLException.class, () -> interceptor.invoke(invocationFor("regularTx"))); diff --git a/spring-ydb-retry/src/test/java/tech/ydb/retry/TransactionPropagationRetryTest.java b/spring-ydb-retry/src/test/java/tech/ydb/retry/TransactionPropagationRetryTest.java index 0029778c..edda4abb 100644 --- a/spring-ydb-retry/src/test/java/tech/ydb/retry/TransactionPropagationRetryTest.java +++ b/spring-ydb-retry/src/test/java/tech/ydb/retry/TransactionPropagationRetryTest.java @@ -14,7 +14,7 @@ class TransactionPropagationRetryTest extends InterceptorTestSupport { void shouldDisableRetryWhenParticipatingInOuterTransaction() { TransactionSynchronizationManager.setActualTransactionActive(true); - TestableInterceptor interceptor = interceptorWithConfig(true, 1, 0, 0, 0, 0); + TestableInterceptor interceptor = interceptorWithConfig(true, 2, 0, 0, 0, 0); interceptor.enqueueOutcome(new IllegalStateException("no retry expected")); assertThrows( @@ -26,7 +26,7 @@ void shouldDisableRetryWhenParticipatingInOuterTransaction() { void shouldRetryWithRequiresNewInsideOuterTransaction() throws Throwable { TransactionSynchronizationManager.setActualTransactionActive(true); - TestableInterceptor interceptor = interceptorWithConfig(true, 1, 0, 0, 0, 0); + TestableInterceptor interceptor = interceptorWithConfig(true, 2, 0, 0, 0, 0); interceptor.enqueueOutcome(new ConfigurableStatusException(BAD_SESSION), "ok"); Object result = interceptor.invoke(invocationFor("ydbRequiresNewRetry")); @@ -39,7 +39,7 @@ void shouldRetryWithRequiresNewInsideOuterTransaction() throws Throwable { void shouldDisableRetryWithNestedPropagationInsideOuterTransaction() { TransactionSynchronizationManager.setActualTransactionActive(true); - TestableInterceptor interceptor = interceptorWithConfig(true, 1, 0, 0, 0, 0); + TestableInterceptor interceptor = interceptorWithConfig(true, 2, 0, 0, 0, 0); interceptor.enqueueOutcome(new ConfigurableStatusException(ABORTED)); assertThrows( @@ -52,7 +52,7 @@ void shouldDisableRetryWithNestedPropagationInsideOuterTransaction() { void shouldRetryWithNotSupportedPropagationInsideOuterTransaction() throws Throwable { TransactionSynchronizationManager.setActualTransactionActive(true); - TestableInterceptor interceptor = interceptorWithConfig(true, 1, 0, 0, 0, 0); + TestableInterceptor interceptor = interceptorWithConfig(true, 2, 0, 0, 0, 0); interceptor.enqueueOutcome(new ConfigurableStatusException(BAD_SESSION), "ok"); Object result = interceptor.invoke(invocationFor("ydbNotSupportedRetry")); diff --git a/spring-ydb-retry/src/test/java/tech/ydb/retry/TransactionalDefaultRetryTest.java b/spring-ydb-retry/src/test/java/tech/ydb/retry/TransactionalDefaultRetryTest.java index 58b10dca..eb0242e7 100644 --- a/spring-ydb-retry/src/test/java/tech/ydb/retry/TransactionalDefaultRetryTest.java +++ b/spring-ydb-retry/src/test/java/tech/ydb/retry/TransactionalDefaultRetryTest.java @@ -18,7 +18,7 @@ class TransactionalDefaultRetryTest extends InterceptorTestSupport { @Test void shouldRetryWithDefaultConfigUntilSuccess() throws Throwable { - TestableInterceptor interceptor = interceptorWithConfig(true, 3, 0, 0, 0, 0); + TestableInterceptor interceptor = interceptorWithConfig(true, 4, 0, 0, 0, 0); interceptor.enqueueOutcome(new ConfigurableStatusException(BAD_SESSION), "ok"); Object result = interceptor.invoke(invocationFor("regularTx")); @@ -30,7 +30,7 @@ void shouldRetryWithDefaultConfigUntilSuccess() throws Throwable { @Test void shouldExhaustDefaultMaxRetriesAndPropagateLastException() { - TestableInterceptor interceptor = interceptorWithConfig(true, 2, 0, 0, 0, 0); + TestableInterceptor interceptor = interceptorWithConfig(true, 3, 0, 0, 0, 0); interceptor.enqueueOutcome( new ConfigurableStatusException(BAD_SESSION), new ConfigurableStatusException(ABORTED), @@ -45,7 +45,7 @@ void shouldExhaustDefaultMaxRetriesAndPropagateLastException() { @Test void shouldPropagateNonRetryableExceptionImmediately() { - TestableInterceptor interceptor = interceptorWithConfig(true, 5, 0, 0, 0, 0); + TestableInterceptor interceptor = interceptorWithConfig(true, 6, 0, 0, 0, 0); interceptor.enqueueOutcome(new ConfigurableStatusException(UNAUTHORIZED)); ConfigurableStatusException exception = @@ -60,7 +60,7 @@ void shouldPropagateNonRetryableExceptionImmediately() { @Test void shouldNotRetryNonYdbRuntimeException() { - TestableInterceptor interceptor = interceptorWithConfig(true, 5, 0, 0, 0, 0); + TestableInterceptor interceptor = interceptorWithConfig(true, 6, 0, 0, 0, 0); interceptor.enqueueOutcome(new IllegalStateException("not ydb")); IllegalStateException exception = @@ -74,7 +74,7 @@ void shouldNotRetryNonYdbRuntimeException() { @Test void shouldImmediatelyPropagateJavaError() { - TestableInterceptor interceptor = interceptorWithConfig(true, 5, 0, 0, 0, 0); + TestableInterceptor interceptor = interceptorWithConfig(true, 6, 0, 0, 0, 0); interceptor.enqueueOutcome(new OutOfMemoryError("test oom")); assertThrows(OutOfMemoryError.class, () -> interceptor.invoke(invocationFor("regularTx"))); @@ -83,7 +83,7 @@ void shouldImmediatelyPropagateJavaError() { @Test void shouldRetryWhenYdbStatusExtractedFromExceptionChain() throws Throwable { - TestableInterceptor interceptor = interceptorWithConfig(true, 3, 0, 0, 0, 0); + TestableInterceptor interceptor = interceptorWithConfig(true, 4, 0, 0, 0, 0); interceptor.enqueueOutcome( new RuntimeException("wrapped", new ConfigurableStatusException(BAD_SESSION)), "ok"); @@ -96,7 +96,7 @@ void shouldRetryWhenYdbStatusExtractedFromExceptionChain() throws Throwable { @Test void shouldCallSleeperWithBackoffDelay() throws Throwable { List delays = new ArrayList<>(); - TestableInterceptor interceptor = interceptorWithSleeper(true, 5, 0, 0, 0, 0, delays::add); + TestableInterceptor interceptor = interceptorWithSleeper(true, 6, 0, 0, 0, 0, delays::add); interceptor.enqueueOutcome( new ConfigurableStatusException(ABORTED), new ConfigurableStatusException(ABORTED), "ok"); @@ -114,7 +114,7 @@ void shouldCallSleeperWithBackoffDelay() throws Throwable { void shouldUseZeroDelayForBadSession() throws Throwable { List delays = new ArrayList<>(); TestableInterceptor interceptor = - interceptorWithSleeper(true, 5, 100, 50, 1000, 500, delays::add); + interceptorWithSleeper(true, 6, 100, 50, 1000, 500, delays::add); interceptor.enqueueOutcome(new ConfigurableStatusException(BAD_SESSION), "ok"); Object result = interceptor.invoke(invocationFor("regularTx")); @@ -131,7 +131,7 @@ void shouldHandleInterruptedSleep() { TestableInterceptor interceptor = interceptorWithSleeper( true, - 3, + 4, 0, 0, 0, @@ -157,7 +157,7 @@ void shouldHandleInterruptedSleep() { @Test void shouldNotRetryClientInternalErrorForTransactionalMethod() { - TestableInterceptor interceptor = interceptorWithConfig(true, 3, 0, 0, 0, 0); + TestableInterceptor interceptor = interceptorWithConfig(true, 4, 0, 0, 0, 0); interceptor.enqueueOutcome(new ConfigurableStatusException(CLIENT_INTERNAL_ERROR), "ok"); ConfigurableStatusException exception = @@ -171,7 +171,7 @@ void shouldNotRetryClientInternalErrorForTransactionalMethod() { @Test void shouldNotRetryTimeoutForTransactionalMethodWhenDefaultConfigNotIdempotent() { - TestableInterceptor interceptor = interceptorWithConfig(true, 3, 0, 0, 0, 0); + TestableInterceptor interceptor = interceptorWithConfig(true, 4, 0, 0, 0, 0); interceptor.enqueueOutcome(new ConfigurableStatusException(TIMEOUT)); ConfigurableStatusException exception = @@ -185,7 +185,7 @@ void shouldNotRetryTimeoutForTransactionalMethodWhenDefaultConfigNotIdempotent() @Test void shouldNotRetryWhenDisabledInConfig() { - TestableInterceptor interceptor = interceptorWithConfig(false, 3, 0, 0, 0, 0); + TestableInterceptor interceptor = interceptorWithConfig(false, 4, 0, 0, 0, 0); interceptor.enqueueOutcome(new ConfigurableStatusException(BAD_SESSION), "ok"); ConfigurableStatusException exception = diff --git a/spring-ydb-retry/src/test/java/tech/ydb/retry/YdbRetryPolicyConfigTest.java b/spring-ydb-retry/src/test/java/tech/ydb/retry/YdbRetryPolicyConfigTest.java index 4c30a3a5..a6df6bdd 100644 --- a/spring-ydb-retry/src/test/java/tech/ydb/retry/YdbRetryPolicyConfigTest.java +++ b/spring-ydb-retry/src/test/java/tech/ydb/retry/YdbRetryPolicyConfigTest.java @@ -11,7 +11,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static tech.ydb.retry.YdbRetryPolicyConfig.DEFAULT_FAST_BACKOFF_BASE_MS; import static tech.ydb.retry.YdbRetryPolicyConfig.DEFAULT_FAST_CAP_BACKOFF_MS; -import static tech.ydb.retry.YdbRetryPolicyConfig.DEFAULT_MAX_RETRIES; +import static tech.ydb.retry.YdbRetryPolicyConfig.DEFAULT_MAX_ATTEMPTS; import static tech.ydb.retry.YdbRetryPolicyConfig.DEFAULT_SLOW_BACKOFF_BASE_MS; import static tech.ydb.retry.YdbRetryPolicyConfig.DEFAULT_SLOW_CAP_BACKOFF_MS; @@ -21,7 +21,7 @@ class YdbRetryPolicyConfigTest extends InterceptorTestSupport { void defaultConstructorShouldSetDefaultValues() { YdbRetryPolicyConfig config = new YdbRetryPolicyConfig(); - assertEquals(DEFAULT_MAX_RETRIES, config.getMaxRetries()); + assertEquals(DEFAULT_MAX_ATTEMPTS, config.getMaxAttempts()); assertEquals(DEFAULT_SLOW_BACKOFF_BASE_MS, config.getSlowBackoffBaseMs()); assertEquals(DEFAULT_FAST_BACKOFF_BASE_MS, config.getFastBackoffBaseMs()); assertEquals(DEFAULT_SLOW_CAP_BACKOFF_MS, config.getSlowCapBackoffMs()); @@ -32,7 +32,7 @@ void defaultConstructorShouldSetDefaultValues() { void customConstructorShouldSetValues() { YdbRetryPolicyConfig config = new YdbRetryPolicyConfig(true, 5, 100, 20, 2000, 300); - assertEquals(5, config.getMaxRetries()); + assertEquals(5, config.getMaxAttempts()); assertEquals(100, config.getSlowBackoffBaseMs()); assertEquals(20, config.getFastBackoffBaseMs()); assertEquals(2000, config.getSlowCapBackoffMs()); @@ -40,13 +40,13 @@ void customConstructorShouldSetValues() { } @Test - void shouldThrowWhenMaxRetriesIsZero() { - assertThrows( - IllegalArgumentException.class, () -> new YdbRetryPolicyConfig(true, 0, 0, 0, 0, 0)); + void shouldAcceptZeroMaxAttempts() { + YdbRetryPolicyConfig config = new YdbRetryPolicyConfig(true, 0, 0, 0, 0, 0); + assertEquals(0, config.getMaxAttempts()); } @Test - void shouldThrowWhenMaxRetriesIsNegative() { + void shouldThrowWhenMaxAttemptsIsNegative() { assertThrows( IllegalArgumentException.class, () -> new YdbRetryPolicyConfig(true, -1, 0, 0, 0, 0)); } @@ -91,7 +91,7 @@ void mergeWithDefaultAnnotationShouldKeepConfigValues() throws NoSuchMethodExcep YdbRetryPolicyConfig merged = original.merge(annotation); - assertEquals(5, merged.getMaxRetries()); + assertEquals(5, merged.getMaxAttempts()); assertEquals(100, merged.getSlowBackoffBaseMs()); assertEquals(20, merged.getFastBackoffBaseMs()); assertEquals(2000, merged.getSlowCapBackoffMs()); @@ -108,7 +108,7 @@ void mergeWithCustomAnnotationShouldOverride() throws NoSuchMethodException { YdbRetryPolicyConfig merged = original.merge(annotation); - assertEquals(100, merged.getMaxRetries()); + assertEquals(100, merged.getMaxAttempts()); assertEquals(200, merged.getSlowBackoffBaseMs()); assertEquals(10, merged.getFastBackoffBaseMs()); assertEquals(10000, merged.getSlowCapBackoffMs()); @@ -125,8 +125,8 @@ void mergeWithPartialOverrideShouldOnlyChangeSpecifiedValues() throws NoSuchMeth YdbRetryPolicyConfig merged = original.merge(annotation); - // only maxRetries should change - assertEquals(2, merged.getMaxRetries()); + // only maxAttempts should change + assertEquals(3, merged.getMaxAttempts()); assertEquals(100, merged.getSlowBackoffBaseMs()); assertEquals(20, merged.getFastBackoffBaseMs()); assertEquals(2000, merged.getSlowCapBackoffMs()); @@ -134,10 +134,10 @@ void mergeWithPartialOverrideShouldOnlyChangeSpecifiedValues() throws NoSuchMeth } @Test - void shouldThrowWhenYdbTransactionalMaxRetriesIsNegative() throws NoSuchMethodException { + void shouldThrowWhenYdbTransactionalMaxAttemptsIsNegative() throws NoSuchMethodException { YdbRetryPolicyConfig original = new YdbRetryPolicyConfig(true, 5, 100, 20, 2000, 300); - Method method = YdbTransactionalTestService.class.getMethod("ydbNegativeMaxRetries"); + Method method = YdbTransactionalTestService.class.getMethod("ydbNegativeMaxAttempts"); YdbTransactional annotation = AnnotatedElementUtils.findMergedAnnotation(method, YdbTransactional.class); @@ -145,18 +145,16 @@ void shouldThrowWhenYdbTransactionalMaxRetriesIsNegative() throws NoSuchMethodEx } @Test - void shouldRejectZeroMaxRetriesAtAnnotationMergeTime() throws NoSuchMethodException { + void shouldInheritGlobalMaxAttemptsWhenAnnotationIsZero() throws NoSuchMethodException { YdbRetryPolicyConfig original = new YdbRetryPolicyConfig(true, 5, 100, 20, 2000, 300); - Method method = YdbTransactionalTestService.class.getMethod("ydbZeroMaxRetries"); + Method method = YdbTransactionalTestService.class.getMethod("ydbZeroMaxAttempts"); YdbTransactional annotation = AnnotatedElementUtils.findMergedAnnotation(method, YdbTransactional.class); - IllegalArgumentException exception = - assertThrows(IllegalArgumentException.class, () -> original.merge(annotation)); + YdbRetryPolicyConfig merged = original.merge(annotation); - assertEquals( - "maxRetries must not be 0; use enabled = false to disable retry", exception.getMessage()); + assertEquals(5, merged.getMaxAttempts()); } @Test diff --git a/spring-ydb-retry/src/test/java/tech/ydb/retry/YdbTransactionInterceptorFactoryTest.java b/spring-ydb-retry/src/test/java/tech/ydb/retry/YdbTransactionInterceptorFactoryTest.java index be28d530..41b0d36f 100644 --- a/spring-ydb-retry/src/test/java/tech/ydb/retry/YdbTransactionInterceptorFactoryTest.java +++ b/spring-ydb-retry/src/test/java/tech/ydb/retry/YdbTransactionInterceptorFactoryTest.java @@ -33,7 +33,7 @@ void getObjectShouldReturnYdbTransactionInterceptor() { void getObjectShouldUseRetryPropertiesConfig() { YdbRetryProperties properties = new YdbRetryProperties(); properties.setEnabled(false); - properties.setMaxRetries(3); + properties.setMaxAttempts(3); YdbTransactionInterceptorFactory factory = new YdbTransactionInterceptorFactory(); factory.setRetryProperties(properties); factory.setTransactionAttributeSource(new AnnotationTransactionAttributeSource()); diff --git a/spring-ydb-retry/src/test/java/tech/ydb/retry/YdbTransactionInterceptorInvocationTest.java b/spring-ydb-retry/src/test/java/tech/ydb/retry/YdbTransactionInterceptorInvocationTest.java index c67a6b1d..b3d8786d 100644 --- a/spring-ydb-retry/src/test/java/tech/ydb/retry/YdbTransactionInterceptorInvocationTest.java +++ b/spring-ydb-retry/src/test/java/tech/ydb/retry/YdbTransactionInterceptorInvocationTest.java @@ -13,7 +13,7 @@ class YdbTransactionInterceptorInvocationTest extends InterceptorTestSupport { @Test void shouldCloneProxyMethodInvocationForEachRetryAttempt() throws Throwable { - TestableInterceptor interceptor = interceptorWithConfig(true, 3, 0, 0, 0, 0); + TestableInterceptor interceptor = interceptorWithConfig(true, 4, 0, 0, 0, 0); Method method = methodOf("ydbCustomRetry"); Object target = new YdbTransactionalTestService(); diff --git a/spring-ydb-retry/src/test/java/tech/ydb/retry/YdbTransactionInterceptorReplacerTest.java b/spring-ydb-retry/src/test/java/tech/ydb/retry/YdbTransactionInterceptorReplacerTest.java index 1db1a70f..267fcec9 100644 --- a/spring-ydb-retry/src/test/java/tech/ydb/retry/YdbTransactionInterceptorReplacerTest.java +++ b/spring-ydb-retry/src/test/java/tech/ydb/retry/YdbTransactionInterceptorReplacerTest.java @@ -99,7 +99,7 @@ void shouldRegisterInterceptorWithCorrectProperties() { PlatformTransactionManager txManager = Mockito.mock(PlatformTransactionManager.class); YdbRetryProperties properties = new YdbRetryProperties(); properties.setEnabled(false); - properties.setMaxRetries(3); + properties.setMaxAttempts(3); TransactionAttributeSource tas = new AnnotationTransactionAttributeSource(); beanFactory.registerSingleton("transactionManager", txManager); diff --git a/spring-ydb-retry/src/test/java/tech/ydb/retry/YdbTransactionalConfigOverrideTest.java b/spring-ydb-retry/src/test/java/tech/ydb/retry/YdbTransactionalConfigOverrideTest.java index ac79d9ce..268ecf2e 100644 --- a/spring-ydb-retry/src/test/java/tech/ydb/retry/YdbTransactionalConfigOverrideTest.java +++ b/spring-ydb-retry/src/test/java/tech/ydb/retry/YdbTransactionalConfigOverrideTest.java @@ -23,7 +23,7 @@ class YdbTransactionalConfigOverrideTest extends InterceptorTestSupport { @Test void shouldOverrideMaxRetriesFromAnnotation() throws Throwable { - TestableInterceptor interceptor = interceptorWithConfig(true, 1, 0, 0, 0, 0); + TestableInterceptor interceptor = interceptorWithConfig(true, 2, 0, 0, 0, 0); interceptor.enqueueOutcome(new ConfigurableStatusException(ABORTED), "ok"); Object result = interceptor.invoke(invocationFor("ydbCustomRetry")); @@ -35,7 +35,7 @@ void shouldOverrideMaxRetriesFromAnnotation() throws Throwable { @Test void shouldUseConfigMaxRetriesWhenAnnotationNotSet() throws Throwable { - TestableInterceptor interceptor = interceptorWithConfig(true, 2, 0, 0, 0, 0); + TestableInterceptor interceptor = interceptorWithConfig(true, 3, 0, 0, 0, 0); interceptor.enqueueOutcome(new ConfigurableStatusException(BAD_SESSION), "ok"); Object result = interceptor.invoke(invocationFor("defaultRetry")); @@ -47,7 +47,7 @@ void shouldUseConfigMaxRetriesWhenAnnotationNotSet() throws Throwable { @Test void shouldExhaustAnnotatedMaxRetriesAndPropagate() { - TestableInterceptor interceptor = interceptorWithConfig(true, 1, 0, 0, 0, 0); + TestableInterceptor interceptor = interceptorWithConfig(true, 2, 0, 0, 0, 0); interceptor.enqueueOutcome( new ConfigurableStatusException(SESSION_BUSY), new ConfigurableStatusException(OVERLOADED), @@ -65,7 +65,7 @@ void shouldExhaustAnnotatedMaxRetriesAndPropagate() { @Test void shouldUseAnnotatedMaxRetriesWhenLowerThanConfig() { - TestableInterceptor interceptor = interceptorWithConfig(true, 1, 0, 0, 0, 0); + TestableInterceptor interceptor = interceptorWithConfig(true, 2, 0, 0, 0, 0); interceptor.enqueueOutcome( new ConfigurableStatusException(OVERLOADED), new ConfigurableStatusException(BAD_SESSION), @@ -83,7 +83,7 @@ void shouldUseAnnotatedMaxRetriesWhenLowerThanConfig() { @Test void shouldUseAnnotatedMaxRetriesWhenHigherThanConfig() throws Throwable { - TestableInterceptor interceptor = interceptorWithConfig(true, 1, 0, 0, 0, 0); + TestableInterceptor interceptor = interceptorWithConfig(true, 2, 0, 0, 0, 0); interceptor.enqueueOutcome( new ConfigurableStatusException(BAD_SESSION), new ConfigurableStatusException(SESSION_BUSY), @@ -99,7 +99,7 @@ void shouldUseAnnotatedMaxRetriesWhenHigherThanConfig() throws Throwable { @Test void shouldRetryDifferentStatusCodesAcrossRetries() throws Throwable { - TestableInterceptor interceptor = interceptorWithConfig(true, 1, 0, 0, 0, 0); + TestableInterceptor interceptor = interceptorWithConfig(true, 2, 0, 0, 0, 0); interceptor.enqueueOutcome( new ConfigurableStatusException(ABORTED), new ConfigurableStatusException(BAD_SESSION), @@ -113,7 +113,7 @@ void shouldRetryDifferentStatusCodesAcrossRetries() throws Throwable { @Test void shouldNotRetryClientCancelledWhenNotIdempotent() { - TestableInterceptor interceptor = interceptorWithConfig(true, 5, 0, 0, 0, 0); + TestableInterceptor interceptor = interceptorWithConfig(true, 6, 0, 0, 0, 0); interceptor.enqueueOutcome(new ConfigurableStatusException(CLIENT_CANCELLED), "ok"); ConfigurableStatusException exception = @@ -127,7 +127,7 @@ void shouldNotRetryClientCancelledWhenNotIdempotent() { @Test void shouldNotRetryClientCancelledEvenWhenIdempotent() { - TestableInterceptor interceptor = interceptorWithConfig(true, 5, 0, 0, 0, 0); + TestableInterceptor interceptor = interceptorWithConfig(true, 6, 0, 0, 0, 0); interceptor.enqueueOutcome(new ConfigurableStatusException(CLIENT_CANCELLED), "ok"); ConfigurableStatusException exception = @@ -141,7 +141,7 @@ void shouldNotRetryClientCancelledEvenWhenIdempotent() { @Test void shouldNotRetryClientInternalErrorEvenWhenIdempotent() { - TestableInterceptor interceptor = interceptorWithConfig(true, 5, 0, 0, 0, 0); + TestableInterceptor interceptor = interceptorWithConfig(true, 6, 0, 0, 0, 0); interceptor.enqueueOutcome(new ConfigurableStatusException(CLIENT_INTERNAL_ERROR), "ok"); ConfigurableStatusException exception = @@ -155,7 +155,7 @@ void shouldNotRetryClientInternalErrorEvenWhenIdempotent() { @Test void shouldUseInterfaceMethodYdbTransactionalOverrides() throws Throwable { - TestableInterceptor interceptor = interceptorWithConfig(true, 1, 0, 0, 0, 0); + TestableInterceptor interceptor = interceptorWithConfig(true, 2, 0, 0, 0, 0); interceptor.enqueueOutcome(new ConfigurableStatusException(TRANSPORT_UNAVAILABLE), "ok"); Object result = interceptor.invoke(invocationFor( @@ -168,7 +168,7 @@ void shouldUseInterfaceMethodYdbTransactionalOverrides() throws Throwable { @Test void shouldNotRetryTransportUnavailableWhenNotIdempotent() { - TestableInterceptor interceptor = interceptorWithConfig(true, 5, 0, 0, 0, 0); + TestableInterceptor interceptor = interceptorWithConfig(true, 6, 0, 0, 0, 0); interceptor.enqueueOutcome(new ConfigurableStatusException(TRANSPORT_UNAVAILABLE), "ok"); ConfigurableStatusException exception = @@ -182,7 +182,7 @@ void shouldNotRetryTransportUnavailableWhenNotIdempotent() { @Test void shouldRetryTransportUnavailableWhenIdempotent() throws Throwable { - TestableInterceptor interceptor = interceptorWithConfig(true, 5, 0, 0, 0, 0); + TestableInterceptor interceptor = interceptorWithConfig(true, 6, 0, 0, 0, 0); interceptor.enqueueOutcome(new ConfigurableStatusException(TRANSPORT_UNAVAILABLE), "ok"); Object result = interceptor.invoke(invocationFor("ydbIdempotentRetry")); @@ -193,7 +193,7 @@ void shouldRetryTransportUnavailableWhenIdempotent() throws Throwable { @Test void shouldRetryClientResourceExhaustedWhenNotIdempotent() throws Throwable { - TestableInterceptor interceptor = interceptorWithConfig(true, 5, 0, 0, 0, 0); + TestableInterceptor interceptor = interceptorWithConfig(true, 6, 0, 0, 0, 0); interceptor.enqueueOutcome(new ConfigurableStatusException(CLIENT_RESOURCE_EXHAUSTED), "ok"); Object result = interceptor.invoke(invocationFor("ydbNonIdempotentRetry")); @@ -204,7 +204,7 @@ void shouldRetryClientResourceExhaustedWhenNotIdempotent() throws Throwable { @Test void shouldRetryClientResourceExhaustedWhenIdempotent() throws Throwable { - TestableInterceptor interceptor = interceptorWithConfig(true, 5, 0, 0, 0, 0); + TestableInterceptor interceptor = interceptorWithConfig(true, 6, 0, 0, 0, 0); interceptor.enqueueOutcome(new ConfigurableStatusException(CLIENT_RESOURCE_EXHAUSTED), "ok"); Object result = interceptor.invoke(invocationFor("ydbIdempotentRetry")); @@ -215,7 +215,7 @@ void shouldRetryClientResourceExhaustedWhenIdempotent() throws Throwable { @Test void shouldNotRetryTimeoutWhenIdempotent() { - TestableInterceptor interceptor = interceptorWithConfig(true, 5, 0, 0, 0, 0); + TestableInterceptor interceptor = interceptorWithConfig(true, 6, 0, 0, 0, 0); interceptor.enqueueOutcome(new ConfigurableStatusException(TIMEOUT)); ConfigurableStatusException exception = @@ -229,7 +229,7 @@ void shouldNotRetryTimeoutWhenIdempotent() { @Test void shouldNotRetrySessionExpiredWhenNotIdempotent() { - TestableInterceptor interceptor = interceptorWithConfig(true, 5, 0, 0, 0, 0); + TestableInterceptor interceptor = interceptorWithConfig(true, 6, 0, 0, 0, 0); interceptor.enqueueOutcome(new ConfigurableStatusException(SESSION_EXPIRED)); ConfigurableStatusException exception = @@ -243,7 +243,7 @@ void shouldNotRetrySessionExpiredWhenNotIdempotent() { @Test void shouldRetryAlwaysRetryableCodesWhenIdempotent() throws Throwable { - TestableInterceptor interceptor = interceptorWithConfig(true, 5, 0, 0, 0, 0); + TestableInterceptor interceptor = interceptorWithConfig(true, 6, 0, 0, 0, 0); interceptor.enqueueOutcome(new ConfigurableStatusException(ABORTED), "ok"); Object result = interceptor.invoke(invocationFor("ydbIdempotentRetry")); @@ -254,7 +254,7 @@ void shouldRetryAlwaysRetryableCodesWhenIdempotent() throws Throwable { @Test void shouldRetryMixedStatusCodesWhenIdempotent() throws Throwable { - TestableInterceptor interceptor = interceptorWithConfig(true, 5, 0, 0, 0, 0); + TestableInterceptor interceptor = interceptorWithConfig(true, 6, 0, 0, 0, 0); interceptor.enqueueOutcome( new ConfigurableStatusException(ABORTED), new ConfigurableStatusException(UNDETERMINED), @@ -269,7 +269,7 @@ void shouldRetryMixedStatusCodesWhenIdempotent() throws Throwable { @Test void shouldRetrySessionExpiredWithZeroDelayWhenIdempotent() throws Throwable { - TestableInterceptor interceptor = interceptorWithConfig(true, 5, 0, 0, 0, 0); + TestableInterceptor interceptor = interceptorWithConfig(true, 6, 0, 0, 0, 0); interceptor.enqueueOutcome(new ConfigurableStatusException(SESSION_EXPIRED), "ok"); Object result = interceptor.invoke(invocationFor("ydbIdempotentRetry")); @@ -280,7 +280,7 @@ void shouldRetrySessionExpiredWithZeroDelayWhenIdempotent() throws Throwable { @Test void shouldStopAtIdempotentOnlyCodeWhenNotIdempotent() { - TestableInterceptor interceptor = interceptorWithConfig(true, 5, 0, 0, 0, 0); + TestableInterceptor interceptor = interceptorWithConfig(true, 6, 0, 0, 0, 0); interceptor.enqueueOutcome( new ConfigurableStatusException(BAD_SESSION), new ConfigurableStatusException(TIMEOUT)); @@ -297,7 +297,7 @@ void shouldStopAtIdempotentOnlyCodeWhenNotIdempotent() { void shouldNotReachDelayCalculatorForTimeoutWhenIdempotent() { List delays = new ArrayList<>(); TestableInterceptor interceptor = - interceptorWithSleeper(true, 5, 100, 50, 1000, 500, delays::add); + interceptorWithSleeper(true, 6, 100, 50, 1000, 500, delays::add); interceptor.enqueueOutcome(new ConfigurableStatusException(TIMEOUT)); assertThrows( @@ -312,7 +312,7 @@ void shouldNotReachDelayCalculatorForTimeoutWhenIdempotent() { void shouldUseZeroDelayForSessionExpiredWhenIdempotent() throws Throwable { List delays = new ArrayList<>(); TestableInterceptor interceptor = - interceptorWithSleeper(true, 5, 100, 50, 1000, 500, delays::add); + interceptorWithSleeper(true, 6, 100, 50, 1000, 500, delays::add); interceptor.enqueueOutcome(new ConfigurableStatusException(SESSION_EXPIRED), "ok"); Object result = interceptor.invoke(invocationFor("ydbIdempotentRetry")); @@ -326,7 +326,7 @@ void shouldUseZeroDelayForSessionExpiredWhenIdempotent() throws Throwable { void shouldUseFastBackoffForUndeterminedWhenIdempotent() throws Throwable { List delays = new ArrayList<>(); TestableInterceptor interceptor = - interceptorWithSleeper(true, 5, 100, 50, 1000, 500, delays::add); + interceptorWithSleeper(true, 6, 100, 50, 1000, 500, delays::add); interceptor.enqueueOutcome(new ConfigurableStatusException(UNDETERMINED), "ok"); interceptor.invoke(invocationFor("ydbIdempotentRetry")); @@ -338,7 +338,7 @@ void shouldUseFastBackoffForUndeterminedWhenIdempotent() throws Throwable { @Test void shouldDelayFirstOverloadedRetryUsingZeroBasedRetryIndex() throws Throwable { List delays = new ArrayList<>(); - TestableInterceptor interceptor = interceptorWithSleeper(true, 5, 1, 1, 1, 1, delays::add); + TestableInterceptor interceptor = interceptorWithSleeper(true, 6, 1, 1, 1, 1, delays::add); interceptor.enqueueOutcome(new ConfigurableStatusException(OVERLOADED), "ok"); Object result = interceptor.invoke(invocationFor("ydbCustomRetry")); @@ -350,7 +350,7 @@ void shouldDelayFirstOverloadedRetryUsingZeroBasedRetryIndex() throws Throwable @Test void shouldNotRetryWhenMethodDisablesRetry() { - TestableInterceptor interceptor = interceptorWithConfig(true, 3, 0, 0, 0, 0); + TestableInterceptor interceptor = interceptorWithConfig(true, 4, 0, 0, 0, 0); interceptor.enqueueOutcome(new ConfigurableStatusException(BAD_SESSION), "ok"); ConfigurableStatusException exception = @@ -364,7 +364,7 @@ void shouldNotRetryWhenMethodDisablesRetry() { @Test void shouldNotRetryWhenGlobalConfigDisablesRetryEvenIfMethodEnablesIt() { - TestableInterceptor interceptor = interceptorWithConfig(false, 3, 0, 0, 0, 0); + TestableInterceptor interceptor = interceptorWithConfig(false, 4, 0, 0, 0, 0); interceptor.enqueueOutcome(new ConfigurableStatusException(BAD_SESSION), "ok"); ConfigurableStatusException exception = @@ -377,7 +377,7 @@ void shouldNotRetryWhenGlobalConfigDisablesRetryEvenIfMethodEnablesIt() { } interface InterfaceAnnotatedService { - @YdbTransactional(maxRetries = 2, idempotent = true) + @YdbTransactional(maxAttempts = 3, idempotent = true) String interfaceAnnotatedIdempotentRetry(); } diff --git a/spring-ydb-retry/src/test/java/tech/ydb/retry/integration/HappyPathIntegrationTest.java b/spring-ydb-retry/src/test/java/tech/ydb/retry/integration/HappyPathIntegrationTest.java index 0d21a015..1ab27c6c 100644 --- a/spring-ydb-retry/src/test/java/tech/ydb/retry/integration/HappyPathIntegrationTest.java +++ b/spring-ydb-retry/src/test/java/tech/ydb/retry/integration/HappyPathIntegrationTest.java @@ -50,7 +50,7 @@ void shouldSaveRaw() { @Test void shouldSaveWithMaxRetries3() { User user = createUser(3L, "user3", "first", "last"); - userService.saveWithMaxRetries3(user); + userService.saveWithMaxAttempts4(user); User found = userService.findById(3L); assertNotNull(found); diff --git a/spring-ydb-retry/src/test/java/tech/ydb/retry/integration/MaxRetriesExhaustedTest.java b/spring-ydb-retry/src/test/java/tech/ydb/retry/integration/MaxRetriesExhaustedTest.java index 4e1510be..9ec57ae5 100644 --- a/spring-ydb-retry/src/test/java/tech/ydb/retry/integration/MaxRetriesExhaustedTest.java +++ b/spring-ydb-retry/src/test/java/tech/ydb/retry/integration/MaxRetriesExhaustedTest.java @@ -38,7 +38,7 @@ void shouldExhaustMaxRetriesAndThrow() { assertThrows( Exception.class, - () -> userService.saveWithMaxRetries3(createUser(1L, "user1", "first1", "last1"))); + () -> userService.saveWithMaxAttempts4(createUser(1L, "user1", "first1", "last1"))); assertEquals(4, DeterministicErrorChannel.getCallCount("executeQuery")); } @@ -48,7 +48,7 @@ void shouldSucceedOnLastAttemptMaxRetries() { .onError("executeQuery", 1, StatusCode.ABORTED) .onError("executeQuery", 2, StatusCode.ABORTED); - userService.saveWithMaxRetries3(createUser(2L, "user2", "first2", "last2")); + userService.saveWithMaxAttempts4(createUser(2L, "user2", "first2", "last2")); assertEquals(3, DeterministicErrorChannel.getCallCount("executeQuery")); assertNotNull(userService.findById(2L)); diff --git a/spring-ydb-retry/src/test/java/tech/ydb/retry/integration/NonRetryableCommitIntegrationTest.java b/spring-ydb-retry/src/test/java/tech/ydb/retry/integration/NonRetryableCommitIntegrationTest.java index a5d83401..73380638 100644 --- a/spring-ydb-retry/src/test/java/tech/ydb/retry/integration/NonRetryableCommitIntegrationTest.java +++ b/spring-ydb-retry/src/test/java/tech/ydb/retry/integration/NonRetryableCommitIntegrationTest.java @@ -51,7 +51,7 @@ void shouldNotRetryNonRetryableCommitErrorWithYdbTransactional(StatusCode code) assertThrows( Exception.class, - () -> userService.saveWithMaxRetries3(createUser(2L, "user2", "first2", "last2"))); + () -> userService.saveWithMaxAttempts4(createUser(2L, "user2", "first2", "last2"))); assertEquals(1, DeterministicErrorChannel.getCallCount("commitTransaction")); assertNull(userService.findById(2L)); } diff --git a/spring-ydb-retry/src/test/java/tech/ydb/retry/integration/app/UserService.java b/spring-ydb-retry/src/test/java/tech/ydb/retry/integration/app/UserService.java index b493645a..e989233c 100644 --- a/spring-ydb-retry/src/test/java/tech/ydb/retry/integration/app/UserService.java +++ b/spring-ydb-retry/src/test/java/tech/ydb/retry/integration/app/UserService.java @@ -23,8 +23,8 @@ public void save(User user) { userRepository.save(user); } - @YdbTransactional(maxRetries = 3) - public void saveWithMaxRetries3(User user) { + @YdbTransactional(maxAttempts = 4) + public void saveWithMaxAttempts4(User user) { userRepository.save(user); } @@ -33,7 +33,7 @@ public void saveIdempotent(User user) { userRepository.save(user); } - @YdbTransactional(maxRetries = 50, idempotent = true) + @YdbTransactional(maxAttempts = 51, idempotent = true) public void updateFirstname(Long id, String firstname) { userRepository.findById(id); userRepository.updateFirstnameById(id, firstname); diff --git a/spring-ydb-retry/src/test/resources/application-enabled.properties b/spring-ydb-retry/src/test/resources/application-enabled.properties index 378bc968..5363076f 100644 --- a/spring-ydb-retry/src/test/resources/application-enabled.properties +++ b/spring-ydb-retry/src/test/resources/application-enabled.properties @@ -2,7 +2,7 @@ spring.datasource.hikari.maximum-pool-size=5 spring.datasource.hikari.connection-timeout=10000 ydb.transaction.retry.enabled=true -ydb.transaction.retry.max-retries=5 +ydb.transaction.retry.max-attempts=5 ydb.transaction.retry.slow-backoff-base-ms=50 ydb.transaction.retry.fast-backoff-base-ms=5 ydb.transaction.retry.slow-cap-backoff-ms=5000 From 702cc35590eebc7a16d8d1f5cba771eb7084b8ae Mon Sep 17 00:00:00 2001 From: Kirill Kurdyukov Date: Mon, 15 Jun 2026 14:02:03 +0300 Subject: [PATCH 2/2] fix test --- .../ydb/retry/YdbTransactionInterceptor.java | 4 ---- .../tech/ydb/retry/YdbRetryPolicyTest.java | 4 +++- .../CommitTransactionRetryTest.java | 22 ++++++++++++++++--- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/spring-ydb-retry/src/main/java/tech/ydb/retry/YdbTransactionInterceptor.java b/spring-ydb-retry/src/main/java/tech/ydb/retry/YdbTransactionInterceptor.java index 079adc92..8f481895 100644 --- a/spring-ydb-retry/src/main/java/tech/ydb/retry/YdbTransactionInterceptor.java +++ b/spring-ydb-retry/src/main/java/tech/ydb/retry/YdbTransactionInterceptor.java @@ -22,10 +22,6 @@ public class YdbTransactionInterceptor extends TransactionInterceptor { private final YdbRetryPolicyConfig retryConfig; private final BackoffSleeper backoffSleeper; - public YdbTransactionInterceptor() { - this(new YdbRetryPolicyConfig(), Thread::sleep); - } - YdbTransactionInterceptor(YdbRetryPolicyConfig retryConfig, BackoffSleeper backoffSleeper) { this.retryConfig = retryConfig; this.backoffSleeper = backoffSleeper; diff --git a/spring-ydb-retry/src/test/java/tech/ydb/retry/YdbRetryPolicyTest.java b/spring-ydb-retry/src/test/java/tech/ydb/retry/YdbRetryPolicyTest.java index a4c9315f..10a4d354 100644 --- a/spring-ydb-retry/src/test/java/tech/ydb/retry/YdbRetryPolicyTest.java +++ b/spring-ydb-retry/src/test/java/tech/ydb/retry/YdbRetryPolicyTest.java @@ -92,8 +92,10 @@ void shouldUseZeroDelayForSessionStatuses() { @Test void shouldReturnEmptyWhenAttemptBudgetExhausted() { YdbRetryPolicyConfig config = new YdbRetryPolicyConfig(true, 2, 0, 0, 0, 0); + // maxAttempts counts the initial execution, so with budget=2 only the first + // failure (attempt=0) is allowed to schedule a retry; the second one exhausts the budget. assertTrue(YdbRetryPolicy.getNextRetryDelayMs(ABORTED, 0, config, false).isPresent()); - assertTrue(YdbRetryPolicy.getNextRetryDelayMs(ABORTED, 1, config, false).isPresent()); + assertTrue(YdbRetryPolicy.getNextRetryDelayMs(ABORTED, 1, config, false).isEmpty()); assertTrue(YdbRetryPolicy.getNextRetryDelayMs(ABORTED, 2, config, false).isEmpty()); } diff --git a/spring-ydb-retry/src/test/java/tech/ydb/retry/integration/CommitTransactionRetryTest.java b/spring-ydb-retry/src/test/java/tech/ydb/retry/integration/CommitTransactionRetryTest.java index a93a1e5f..bf3d7dd8 100644 --- a/spring-ydb-retry/src/test/java/tech/ydb/retry/integration/CommitTransactionRetryTest.java +++ b/spring-ydb-retry/src/test/java/tech/ydb/retry/integration/CommitTransactionRetryTest.java @@ -13,6 +13,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; @SpringBootTest(classes = UserApplication.class) @ActiveProfiles({"enabled", "ydb"}) @@ -28,6 +29,13 @@ void cleanUp() { userService.deleteAll(); } + /** + * When the first CommitTransaction RPC returns a retryable status, the retry must (a) re-issue + * an ExecuteQuery for the INSERT against a fresh transaction and (b) issue a second + * CommitTransaction RPC. Both gRPC counters are observed via the deterministic error channel. + * Without (a) the data would never reach the new tx; without (b) Spring would silently swallow + * a failed commit. + */ @ParameterizedTest(name = "CommitTransaction") @EnumSource( value = StatusCode.class, @@ -35,10 +43,18 @@ void cleanUp() { void shouldRecoverFromRetryableCommitError(StatusCode code) { DeterministicErrorChannel.configure().onError("commitTransaction", 1, code); - userService.save(createUser(1L, "user1", "first1", "last1")); + User user = createUser(1L, "user1", "first1", "last1"); + userService.save(user); - assertEquals(2, DeterministicErrorChannel.getCallCount("commitTransaction")); - assertNotNull(userService.findById(1L)); + assertEquals(2, DeterministicErrorChannel.getCallCount("commitTransaction"), + "commit must be attempted twice: first attempt fails with " + code + + ", retry must honestly invoke CommitTransaction again"); + assertTrue(DeterministicErrorChannel.getCallCount("executeQuery") >= 2, + "INSERT must be re-executed against a fresh transaction on retry"); + + User persisted = userService.findById(user.getId()); + assertNotNull(persisted, "user must end up persisted by the successful retry commit"); + assertEquals(user.getUsername(), persisted.getUsername()); } @ParameterizedTest(name = "CommitTransaction")