Skip to content
Merged
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
5 changes: 5 additions & 0 deletions spring-ydb-retry/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
19 changes: 11 additions & 8 deletions spring-ydb-retry/README.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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`
Expand All @@ -27,6 +31,7 @@ for transactional operations with [YDB](https://ydb.tech).
For Maven, add the following dependency to your pom.xml:

```xml

<dependency>
<groupId>tech.ydb</groupId>
<artifactId>spring-ydb-retry</artifactId>
Expand Down Expand Up @@ -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
}
```

Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion spring-ydb-retry/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

<groupId>tech.ydb</groupId>
<artifactId>spring-ydb-retry</artifactId>
<version>0.9.0</version>
<version>0.10.0</version>
<packaging>jar</packaging>

<name>Spring YDB Retry</name>
Expand Down
2 changes: 1 addition & 1 deletion spring-ydb-retry/slo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion spring-ydb-retry/slo/playground/chaos/compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion spring-ydb-retry/slo/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
<dependency>
<groupId>tech.ydb</groupId>
<artifactId>spring-ydb-retry</artifactId>
<version>0.9.0</version>
<version>0.10.0</version>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
Expand Down
4 changes: 2 additions & 2 deletions spring-ydb-retry/slo/src/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 |

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand All @@ -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
Expand All @@ -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;
Expand All @@ -69,8 +69,8 @@ public boolean isEnabled() {
return enabled;
}

public int getMaxRetries() {
return maxRetries;
public int getMaxAttempts() {
return maxAttempts;
}

public int getSlowBackoffBaseMs() {
Expand Down Expand Up @@ -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;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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() {
Expand Down Expand Up @@ -63,7 +63,7 @@ public void setFastCapBackoffMs(int fastCapBackoffMs) {
public YdbRetryPolicyConfig toConfig() {
return new YdbRetryPolicyConfig(
enabled,
maxRetries,
maxAttempts,
slowBackoffBaseMs,
fastBackoffBaseMs,
slowCapBackoffMs,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
31 changes: 18 additions & 13 deletions spring-ydb-retry/src/main/java/tech/ydb/retry/YdbTransactional.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading