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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
<module>jdbc</module>
<module>project-course</module>
<module>slo</module>
<module>slo-workload</module>
</modules>

<dependencyManagement>
Expand Down
144 changes: 144 additions & 0 deletions slo-workload/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# YDB SLO Workload Tests

This module hosts SLO (Service Level Objective) workloads that test the
reliability of YDB Java clients under load and chaos using the
[YDB SLO action](https://github.com/ydb-platform/ydb-slo-action).

Each submodule is a self-contained, runnable workload that follows the same
contract as the SDK SLO workload in [`../slo`](../slo): it reads its
configuration from environment variables, runs setup/run/teardown phases, and
pushes OpenTelemetry (OTLP) metrics that the action scrapes and compares
between the current PR run and a baseline run.

| Module | Component under test | Description |
| --- | --- | --- |
| [`jdbc`](jdbc) | `ydb-jdbc-driver` | Plain JDBC KV workload (no framework) |

## How a workload behaves

Every workload runs three phases:

1. **Setup** — creates a partitioned KV table and prefills it with rows.
2. **Run** — drives concurrent read and write loops at fixed RPS for the
configured duration. Each operation is timed and retried; the outcome is
recorded as OTLP metrics.
3. **Teardown** — drops the workload table even if the run failed, so the
cluster is left clean.

While the workload runs, the SLO action injects chaos (node restarts, network
black holes, container pauses). The metrics show how well the client copes.

## Metrics

Every metric carries a `ref` label taken from the `WORKLOAD_REF` environment
variable, which lets the report action separate the **current** run from the
**baseline** run. Names are shown below in Prometheus form (dots become
underscores during the OTLP → Prometheus conversion).

| Metric | Type | Labels |
| --- | --- | --- |
| `sdk_operations_total` | counter | `operation_type`, `operation_status` |
| `sdk_errors_total` | counter | `operation_type`, `error_kind` |
| `sdk_retry_attempts_total` | counter | `operation_type`, `operation_status` |
| `sdk_pending_operations` | up/down counter | `operation_type` |
| `sdk_operation_latency_p50_seconds` | gauge | `operation_type`, `operation_status` (always `success`) |
| `sdk_operation_latency_p95_seconds` | gauge | `operation_type`, `operation_status` (always `success`) |
| `sdk_operation_latency_p99_seconds` | gauge | `operation_type`, `operation_status` (always `success`) |

Latency percentiles are computed from per-operation HDR histograms and reflect
only successful operations — failure latency is dominated by retry budgets and
timeouts and would mask real regressions during chaos. Counters cover both
branches, so availability is computed correctly.

## Inputs

Connection details and run parameters come from environment variables:

| Variable | Description |
| --- | --- |
| `YDB_JDBC_URL` | Full JDBC URL (`jdbc:ydb:...`), used verbatim if set |
| `YDB_CONNECTION_STRING` | YDB connection string; prefixed with `jdbc:ydb:` |
| `YDB_ENDPOINT` + `YDB_DATABASE` | Used to compose the connection string if the above are unset |
| `YDB_TOKEN` | Optional auth token |
| `WORKLOAD_REF` | Value of the `ref` label on every metric |
| `WORKLOAD_NAME` | Workload name (also part of the table name) |
| `WORKLOAD_DURATION` | Run duration in seconds |
| `OTEL_EXPORTER_OTLP_ENDPOINT` | OTLP HTTP endpoint to push metrics to |

KV tunables are passed on the command line and parsed by JCommander:

```
--read-rps <int> Target read RPS (default 1000)
--write-rps <int> Target write RPS (default 100)
--read-timeout-ms <int> Per-attempt read timeout in ms (default 10000)
--write-timeout-ms <int> Per-attempt write timeout in ms (default 10000)
--prefill-count <int> Rows to prefill before the run phase (default 1000)
--partition-size <int> Auto-partitioning partition size in MB (default 1)
--min-partition-count <int> Minimum number of table partitions (default 6)
--max-partition-count <int> Maximum number of table partitions (default 1000)
--duration <int> Override WORKLOAD_DURATION when > 0
```

Unknown flags are ignored, so a workload accepts command strings designed for
other SDKs without erroring.

## How CI uses this module

This repository only hosts the workload sources. The CI that actually runs them
lives in the repository of the component under test — for `jdbc` that is
[`ydb-jdbc-driver`](https://github.com/ydb-platform/ydb-jdbc-driver) — mirroring
how the SDK SLO workload in [`../slo`](../slo) is driven from the
`ydb-java-sdk` repository rather than from here.

The driver's workflow, via [`ydb-platform/ydb-slo-action`](https://github.com/ydb-platform/ydb-slo-action):

1. checks out the driver under test (current and baseline) and this repository
for the workload sources;
2. `ydb-platform/ydb-slo-action/init` deploys a YDB cluster (storage + database
nodes), Prometheus with an OTLP receiver, and a chaos monkey, exposing the
database node IPs and the OTLP endpoint as step outputs;
3. builds the workload jar and runs it, pointing `YDB_CONNECTION_STRING` at a
database node and `OTEL_EXPORTER_OTLP_ENDPOINT` at the Prometheus OTLP
receiver;
4. `ydb-platform/ydb-slo-action/report` compares the current run against the
baseline and posts a summary to the PR.

## Building locally

From the `ydb-java-examples` repository root:

```bash
mvn -pl slo-workload/jdbc -am -DskipTests package
```

The resulting jar is at
`slo-workload/jdbc/target/ydb-slo-jdbc-workload.jar`. To run it against a
local YDB:

```bash
export YDB_CONNECTION_STRING="grpc://localhost:2136/local"
export WORKLOAD_REF=local
export WORKLOAD_NAME=java-slo-jdbc

java -jar slo-workload/jdbc/target/ydb-slo-jdbc-workload.jar \
--duration 60 --read-rps 100 --write-rps 10 --prefill-count 100
```

If `OTEL_EXPORTER_OTLP_ENDPOINT` is not set, metrics are still recorded
in-process but never exported — handy for verifying that the workload runs
cleanly before pushing to CI.

## Adding a new workload

1. Create a module next to `jdbc` (e.g. `spring-data-jpa`).
2. Reuse `Config`, `Metrics`, and the `kv` package; replace only the data
access layer with the framework under test.
3. Register the module in `slo-workload/pom.xml` and wire it into the SLO
workflow of the component under test (in its own repository).

## Links

- [SDK SLO workload (reference)](../slo)
- [YDB SLO Action](https://github.com/ydb-platform/ydb-slo-action)
- [YDB JDBC Driver](https://github.com/ydb-platform/ydb-jdbc-driver)
- [YDB Documentation](https://ydb.tech/docs/)
51 changes: 51 additions & 0 deletions slo-workload/jdbc/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Multi-stage Dockerfile for the YDB JDBC SLO workload.
#
# The image can be consumed by the YDB SLO action
# (`ydb-platform/ydb-slo-action`): the workload reads its connection details
# and run parameters from environment variables and pushes OTLP metrics to the
# endpoint the action provides.
#
# Build context: the `ydb-java-examples` repository root.
#
# Optional build args:
# MAVEN_IMAGE Builder image. Defaults to `maven:3.9-eclipse-temurin-17`.
# RUNTIME_IMAGE Runtime image. Defaults to `eclipse-temurin:17-jre`.
# YDB_JDBC_VERSION Override the ydb-jdbc-driver version under test.

ARG MAVEN_IMAGE=maven:3.9-eclipse-temurin-17
ARG RUNTIME_IMAGE=eclipse-temurin:17-jre

# ---------- builder ---------------------------------------------------------
FROM ${MAVEN_IMAGE} AS workload-build

WORKDIR /src
COPY . /src

ARG YDB_JDBC_VERSION=""

# Pin the JDBC driver version under test when provided, then build only the
# workload module (and the parent context it needs).
RUN if [ -n "${YDB_JDBC_VERSION}" ]; then \
echo "Pinning ydb-jdbc-driver to ${YDB_JDBC_VERSION}" && \
mvn -B -q versions:set-property \
-Dproperty=ydb.jdbc.version \
-DnewVersion="${YDB_JDBC_VERSION}" \
-DgenerateBackupPoms=false \
-pl slo-workload ; \
fi && \
mvn -B -q -pl slo-workload/jdbc -am \
-DskipTests \
-Dmaven.javadoc.skip=true \
package

# ---------- runtime ---------------------------------------------------------
FROM ${RUNTIME_IMAGE}

WORKDIR /app

# The jar's manifest Class-Path points at libs/, so a single `java -jar` call
# is enough.
COPY --from=workload-build /src/slo-workload/jdbc/target/ydb-slo-jdbc-workload.jar /app/ydb-slo-jdbc-workload.jar
COPY --from=workload-build /src/slo-workload/jdbc/target/libs /app/libs

ENTRYPOINT ["java", "-jar", "/app/ydb-slo-jdbc-workload.jar"]
91 changes: 91 additions & 0 deletions slo-workload/jdbc/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# JDBC SLO workload

A plain-JDBC SLO workload that exercises the
[YDB JDBC driver](https://github.com/ydb-platform/ydb-jdbc-driver) under load
and chaos. It mirrors the structure and metrics contract of the SDK SLO
workload in [`../../slo`](../../slo), so reports are directly comparable.

> See the [parent README](../README.md) for the shared metrics, environment
> variables, CLI flags and CI flow.

## What it does

The workload runs as a standalone jar (`tech.ydb.slo.Main`) and goes through
three phases against a partitioned KV table:

1. **Setup** — `CREATE TABLE IF NOT EXISTS` plus a prefill of `--prefill-count`
rows.
2. **Run** — dedicated read and write thread pools, each paced by a Guava
`RateLimiter` to the target RPS, running until the configured duration.
3. **Teardown** — `DROP TABLE`.

Every worker thread owns its own JDBC `Connection` (the driver's connections
are not thread-safe) and reuses prepared statements. On a connection-level
error the connection is transparently reopened on the next attempt.

## Schema

```
hash Uint64 -- primary key, derived from id on the client
id Uint64 -- primary key
payload_str Utf8
payload_double Double
payload_timestamp Timestamp
payload_hash Uint64
```

The primary-key `hash` column is derived from `id` with a SplitMix64-style mix
(`KvWorkload#numericHash`) so reads and writes target the same key without
relying on server-side YQL builtins inside parameterized statements.

## Retries

Operations are retried with exponential backoff (up to 10 attempts). An error
is considered retryable when the driver throws a `SQLRecoverableException` or
`SQLTransientException` (which covers the driver's
`YdbRetryableException`, `YdbConditionallyRetryableException`,
`YdbUnavailbaleException` and `YdbTimeoutException`). The number of retries is
recorded in `sdk_retry_attempts_total`, and the failure reason is reported via
the `error_kind` label on `sdk_errors_total` (using the YDB status code when
available).

## Files

```
jdbc/
├── Dockerfile
├── pom.xml
├── README.md
└── src/main/
├── java/tech/ydb/slo/
│ ├── Config.java Reads env vars, resolves the JDBC URL
│ ├── Main.java Entry point
│ ├── Metrics.java OTLP metrics + HDR histograms
│ └── kv/
│ ├── KvWorkload.java Setup/run/teardown loop over JDBC
│ ├── KvWorkloadParams.java JCommander-bound CLI flags
│ ├── Row.java Row data class
│ └── RowGenerator.java Random payload generator
└── resources/
└── log4j2.xml Console logging config
```

## Building and running locally

```bash
# From the repository root
mvn -pl slo-workload/jdbc -am -DskipTests package

export YDB_CONNECTION_STRING="grpc://localhost:2136/local"
export WORKLOAD_REF=local
export WORKLOAD_NAME=java-slo-jdbc

java -jar slo-workload/jdbc/target/ydb-slo-jdbc-workload.jar \
--duration 60 --read-rps 100 --write-rps 10 --prefill-count 100
```

Build the container image (context is the repository root):

```bash
docker build -f slo-workload/jdbc/Dockerfile -t ydb-slo-jdbc-workload .
```
102 changes: 102 additions & 0 deletions slo-workload/jdbc/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>tech.ydb.examples</groupId>
<artifactId>slo-workload</artifactId>
<version>1.1.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

<artifactId>jdbc</artifactId>
<packaging>jar</packaging>
<name>JDBC SLO workload</name>
<description>SLO workload exercising the YDB JDBC driver, compatible with ydb-slo-action</description>

<dependencies>
<!-- The component under test -->
<dependency>
<groupId>tech.ydb.jdbc</groupId>
<artifactId>ydb-jdbc-driver</artifactId>
</dependency>

<!-- CLI parameters -->
<dependency>
<groupId>com.beust</groupId>
<artifactId>jcommander</artifactId>
</dependency>

<!-- Rate limiting for the read/write loops -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>

<!-- Latency percentiles -->
<dependency>
<groupId>org.hdrhistogram</groupId>
<artifactId>HdrHistogram</artifactId>
</dependency>

<!-- OpenTelemetry metrics + OTLP exporter -->
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-api</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk-metrics</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-otlp</artifactId>
</dependency>

<!-- Logging -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId>
</dependency>
</dependencies>

<build>
<finalName>ydb-slo-jdbc-workload</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<release>17</release>
</configuration>
</plugin>

<!-- Copy transitive dependencies into target/libs so a single
`java -jar` works (Class-Path points at libs/). -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
</plugin>

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<classpathPrefix>libs/</classpathPrefix>
<mainClass>tech.ydb.slo.Main</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>
Loading
Loading