Skip to content

Commit d20a616

Browse files
dev: Tidy up slo-workload sources and document the workload contract
1 parent aa13e94 commit d20a616

24 files changed

Lines changed: 172 additions & 156 deletions

File tree

slo-workload/AGENTS.md

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
# AGENTS.md — slo-workload
2+
3+
Guide for AI agents working on the YDB SLO workloads. Pairs with
4+
[ydb-platform/ydb-slo-action](https://github.com/ydb-platform/ydb-slo-action),
5+
which is the GitHub Action that actually runs these images.
6+
7+
## What this module is
8+
9+
This is the **workload-side half** of the ydb-slo-action contract. The action
10+
deploys a YDB cluster + Prometheus + a chaos monkey, then runs two copies of
11+
your workload container (current vs baseline) inside the same Docker network
12+
and compares their metrics. Everything here exists to satisfy that contract.
13+
14+
Concretely:
15+
16+
- `core/` — shared harness (`Config`, `Launcher`, `Metrics`, `kv/*`). Reads
17+
the env vars the action injects, drives setup → run → teardown, pushes OTLP
18+
metrics with `ref={current|baseline}` labels.
19+
- `query/`, `jdbc/`, `spring-data-jdbc/`, `spring-data-jpa/` — one workload per
20+
client under test. Each module is ~thin: a `Main` that wires a `KvClient`
21+
implementation into the shared `Launcher`.
22+
- `docker/Dockerfile.sdk` — single shared Dockerfile used by the **SDK**
23+
CI (`ydb-java-sdk`). Picks the workload module via build args.
24+
- `<module>/Dockerfile` — per-workload Dockerfile used by the **JDBC driver**
25+
CI (`ydb-jdbc-driver`). Optionally builds the driver from source when its
26+
checkout is present in the build context.
27+
28+
## Two-repo build pattern (this is the load-bearing trick)
29+
30+
The CI workflows in `ydb-java-sdk` and `ydb-jdbc-driver` need to test SDK/driver
31+
code **as it is in the PR** — not whatever version is pinned in this repo's
32+
`pom.xml`. The pattern is:
33+
34+
1. The component-under-test repo has the workflow file (`slo.yml` / `slo.yaml`)
35+
and a `build-slo-image.sh` script.
36+
2. The workflow checks out **three** trees with `actions/checkout`:
37+
- current SDK/driver (the PR's HEAD)
38+
- baseline SDK/driver (the merge-base commit with `master`)
39+
- `ydb-java-examples@master` (this repo)
40+
3. `build-slo-image.sh` assembles a temporary Docker build context by
41+
hard-linking both trees into one directory, then runs `docker build` with
42+
the Dockerfile from this repo.
43+
4. The Dockerfile sees both trees, installs the component-under-test from
44+
source (`mvn install`), reads back its version via `help:evaluate`, pins
45+
the workload's dependency property to that exact version with
46+
`versions:set-property`, then packages the workload module.
47+
48+
Net effect: a workload jar built against the **exact** SDK/driver under test,
49+
not a stale release pinned in this repo. The baseline image is built the same
50+
way against the merge-base, so any chaos regression you see in the report is
51+
isolated to the PR's changes — the rest of the stack is identical.
52+
53+
Two flavors of this Dockerfile pattern live in this repo:
54+
55+
- **`docker/Dockerfile.sdk`** — copies `ydb-java-sdk/` and `ydb-java-examples/`
56+
side-by-side, installs the SDK first, then sets `ydb.sdk.version` on the
57+
workload and packages the module named by `WORKLOAD_MODULE`. One image for
58+
all four SDK workloads.
59+
- **`<module>/Dockerfile`** — copies the examples repo at context root and
60+
`ydb-jdbc-driver/` next to it. If the driver checkout is present, install it
61+
and pin `ydb.jdbc.version`; otherwise fall back to `YDB_JDBC_VERSION` or the
62+
property already in the POM. One Dockerfile per workload (the JDBC line has
63+
three workloads — `jdbc`, `spring-data-jdbc`, `spring-data-jpa`).
64+
65+
The shapes differ because of which `pom.xml` property is being pinned, and
66+
because the SDK workflow shares a single Dockerfile across the matrix while
67+
the JDBC workflow uses per-module Dockerfiles. Both shapes use the same
68+
build-context trick.
69+
70+
## The ydb-slo-action contract
71+
72+
The workload reads environment variables (set by the action) and writes
73+
metrics. The contract is asymmetric: the action provides connection details,
74+
the workload provides metrics. No filesystem state crosses the boundary.
75+
76+
### Inputs the action provides
77+
78+
| Env var | Used for |
79+
| --- | --- |
80+
| `YDB_JDBC_URL` / `YDB_CONNECTION_STRING` / `YDB_ENDPOINT`+`YDB_DATABASE` | Connection (first set wins) |
81+
| `YDB_TOKEN` | Optional auth token |
82+
| `WORKLOAD_REF` | Value of the `ref` label on every metric (action sets this to `current` or `baseline`) |
83+
| `WORKLOAD_NAME` | Workload name (also the table-name prefix) |
84+
| `WORKLOAD_DURATION` | Run duration in seconds (0 = unlimited) |
85+
| `OTEL_EXPORTER_OTLP_ENDPOINT` | Prometheus OTLP HTTP receiver |
86+
87+
Plus per-workload CLI flags via JCommander (see README).
88+
89+
### Metrics the workload produces
90+
91+
All metrics carry `ref=<WORKLOAD_REF>`. The action's report compares the two
92+
`ref` values pairwise.
93+
94+
- `sdk_operations_total{operation_type, operation_status}` — counter
95+
- `sdk_errors_total{operation_type, error_kind}` — counter
96+
- `sdk_retry_attempts_total{operation_type, operation_status}` — counter
97+
- `sdk_pending_operations{operation_type}` — up/down counter
98+
- `sdk_operation_latency_p{50,95,99}_seconds{operation_type, operation_status=success}` — gauges, fed from per-second HdrHistogram snapshots, reset after each scrape
99+
100+
Latency percentiles cover **only** successful operations on purpose: failure
101+
latency is dominated by retry budgets/timeouts and would mask real
102+
regressions during chaos. The total/error counters cover both branches, so
103+
availability still computes correctly.
104+
105+
## When adding a new workload
106+
107+
1. Create a module next to `jdbc/`. Reuse `core/` (don't re-implement Config,
108+
Metrics, Launcher, the KV runner — the cross-implementation comparability
109+
in the report depends on every workload writing identical rows via
110+
`RowGenerator.numericHash`).
111+
2. Implement `KvClient` + a thin `Main` that calls
112+
`Launcher.launch(programName, defaultWorkloadName, args, factory)`.
113+
3. Add a `Dockerfile` next to the module that installs the component under
114+
test from source (mirror `jdbc/Dockerfile`), or extend `Dockerfile.sdk`
115+
and pass `WORKLOAD_MODULE=slo-workload/<name>` from CI.
116+
4. Register the module in `pom.xml` under the `jdk17-slo-workload` profile.
117+
5. Wire it into the SLO workflow of the component-under-test repository
118+
(`ydb-java-sdk/.github/workflows/slo.yml` or
119+
`ydb-jdbc-driver/.github/workflows/slo.yaml`). This repo does **not** run
120+
SLO CI by itself — there's no YDB cluster here.
121+
122+
## Local sanity checks
123+
124+
```bash
125+
# Build a single workload module
126+
mvn -pl slo-workload/jdbc -am -DskipTests package
127+
128+
# Smoke-run against a local YDB without OTLP export
129+
export YDB_CONNECTION_STRING="grpc://localhost:2136/local"
130+
export WORKLOAD_REF=local
131+
export WORKLOAD_NAME=java-slo-jdbc
132+
java -jar slo-workload/jdbc/target/ydb-slo-jdbc-workload.jar \
133+
--duration 60 --read-rps 100 --write-rps 10 --prefill-count 100
134+
```
135+
136+
If `OTEL_EXPORTER_OTLP_ENDPOINT` is unset, metrics are recorded in-process
137+
but not pushed — useful for verifying the workload runs cleanly before
138+
pushing to CI.
139+
140+
## Things that bite
141+
142+
- **The `export` stage in `Dockerfile.sdk` must `FROM workload-build`**, not
143+
the bare Maven image. Starting from a clean image silently drops `/src/`
144+
and breaks the COPY in the runtime stage. (Cost a fix commit; don't repeat
145+
it.)
146+
- **`WORKLOAD_REF=unknown`** is the silent default if the env var is missing.
147+
Locally that's fine; in CI it would merge current and baseline series. The
148+
action always sets it, but watch out when reproducing CI behaviour
149+
manually.
150+
- **Prefill failure threshold is 50%**. If half the writes during prefill
151+
fail, the runner refuses to start the read loop — empty key-space means
152+
meaningless read latency. Don't lower the threshold to "fix" a flaky test.
153+
- **`acceptUnknownOptions(false)`** on JCommander is intentional. A typo in
154+
the ydb-slo-action `workload_*_command` input should fail loudly, not
155+
silently fall back to defaults.
156+
- **HdrHistograms are `AtomicHistogram`** for lock-free hot-path recording.
157+
Don't swap them back to `synchronized` Histogram — the contention shows up
158+
at high RPS.

slo-workload/CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
See [AGENTS.md](AGENTS.md).

slo-workload/core/src/main/java/tech/ydb/slo/core/Config.java

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,10 @@ private Config(
2727
this.otlpEndpoint = otlpEndpoint;
2828
}
2929

30-
31-
3230
public String connectionString() {
3331
return connectionString;
3432
}
3533

36-
37-
3834
public String jdbcUrl() {
3935
return jdbcUrl;
4036
}
@@ -59,8 +55,6 @@ public String otlpEndpoint() {
5955
return otlpEndpoint;
6056
}
6157

62-
63-
6458
public static Config fromEnv(String defaultWorkloadName) {
6559
String connectionString = resolveConnectionString();
6660
if (connectionString == null || connectionString.isEmpty()) {
@@ -87,8 +81,6 @@ public static Config fromEnv(String defaultWorkloadName) {
8781
);
8882
}
8983

90-
91-
9284
private static String resolveConnectionString() {
9385
String jdbc = System.getenv("YDB_JDBC_URL");
9486
if (jdbc != null && !jdbc.isEmpty()) {
@@ -115,8 +107,6 @@ private static String stripJdbcPrefix(String value) {
115107
return value;
116108
}
117109

118-
119-
120110
private static String toJdbcUrl(String connectionString) {
121111
if (connectionString.startsWith("jdbc:")) {
122112
return connectionString;

slo-workload/core/src/main/java/tech/ydb/slo/core/Launcher.java

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,14 @@
1212
public final class Launcher {
1313
private static final Logger logger = LoggerFactory.getLogger(Launcher.class);
1414

15-
16-
1715
@FunctionalInterface
1816
public interface ClientFactory {
1917
KvClient create(Config config, KvWorkloadParams params, String tablePath) throws Exception;
2018
}
2119

2220
private Launcher() {
23-
2421
}
2522

26-
27-
2823
public static void launch(
2924
String programName,
3025
String defaultWorkloadName,
@@ -34,8 +29,6 @@ public static void launch(
3429
System.exit(run(programName, defaultWorkloadName, args, factory));
3530
}
3631

37-
38-
3932
public static int run(
4033
String programName,
4134
String defaultWorkloadName,
@@ -54,8 +47,6 @@ public static int run(
5447
try {
5548
JCommander.newBuilder()
5649
.programName(programName)
57-
58-
5950
.acceptUnknownOptions(false)
6051
.addObject(params)
6152
.build()
@@ -65,7 +56,6 @@ public static int run(
6556
return 2;
6657
}
6758

68-
6959
if (params.durationSeconds() <= 0) {
7060
params.setDurationSeconds(config.durationSeconds());
7161
}
@@ -120,8 +110,6 @@ public static int run(
120110
return exitCode;
121111
}
122112

123-
124-
125113
public static String tablePathFor(Config config) {
126114
return sanitize(config.workloadName()) + "_" + sanitize(config.ref());
127115
}
@@ -137,8 +125,6 @@ private static void closeQuietly(AutoCloseable closeable, String name) {
137125
}
138126
}
139127

140-
141-
142128
private static String sanitize(String value) {
143129
StringBuilder sb = new StringBuilder(value.length());
144130
for (int i = 0; i < value.length(); i++) {

slo-workload/core/src/main/java/tech/ydb/slo/core/Metrics.java

Lines changed: 0 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
import org.HdrHistogram.Histogram;
2121

2222
public final class Metrics implements AutoCloseable {
23-
2423
public enum OperationType {
2524
READ("read"),
2625
WRITE("write");
@@ -60,7 +59,6 @@ public String label() {
6059
private static final AttributeKey<String> ATTR_REF =
6160
AttributeKey.stringKey("ref");
6261

63-
6462
private static final long HDR_MIN_MICROS = 1L;
6563
private static final long HDR_MAX_MICROS = 60L * 1_000_000L;
6664
private static final int HDR_SIGNIFICANT_DIGITS = 3;
@@ -92,8 +90,6 @@ private Metrics(
9290
this.histograms = histograms;
9391
}
9492

95-
96-
9793
public static Metrics create(Config config) {
9894
String ref = config.ref();
9995

@@ -142,26 +138,10 @@ public static Metrics create(Config config) {
142138

143139
Map<OperationType, Histogram> histograms = new ConcurrentHashMap<>();
144140

145-
146-
147-
148-
149-
150-
151141
for (OperationType type : OperationType.values()) {
152142
histograms.put(type, newHistogram());
153143
}
154144

155-
156-
157-
158-
159-
160-
161-
162-
163-
164-
165145
ObservableDoubleMeasurement p50Observer = meter.gaugeBuilder("sdk.operation.latency.p50.seconds")
166146
.setUnit("s")
167147
.setDescription("p50 operation latency in seconds")
@@ -194,10 +174,6 @@ public static Metrics create(Config config) {
194174
}
195175

196176
private static String metricsEndpoint(String otlpEndpoint) {
197-
198-
199-
200-
201177
String trimmed = otlpEndpoint.endsWith("/")
202178
? otlpEndpoint.substring(0, otlpEndpoint.length() - 1)
203179
: otlpEndpoint;
@@ -207,8 +183,6 @@ private static String metricsEndpoint(String otlpEndpoint) {
207183
return trimmed + "/v1/metrics";
208184
}
209185

210-
211-
212186
public Span startOperation(OperationType type) {
213187
pendingOperations.add(1, Attributes.of(
214188
ATTR_REF, ref,
@@ -217,8 +191,6 @@ public Span startOperation(OperationType type) {
217191
return new Span(this, type, System.nanoTime());
218192
}
219193

220-
221-
222194
public void flush() {
223195
meterProvider.forceFlush().join(10, TimeUnit.SECONDS);
224196
}
@@ -248,12 +220,6 @@ private void recordOutcome(
248220
ATTR_OPERATION_TYPE, type.label()
249221
));
250222

251-
252-
253-
254-
255-
256-
257223
if (status == OperationStatus.SUCCESS) {
258224
Histogram histogram = histograms.computeIfAbsent(type, k -> newHistogram());
259225
long clamped = Math.max(HDR_MIN_MICROS, Math.min(HDR_MAX_MICROS, latencyMicros));
@@ -267,8 +233,6 @@ private void recordOutcome(
267233
}
268234
}
269235

270-
271-
272236
private static void observeAndResetPercentiles(
273237
Map<OperationType, Histogram> histograms,
274238
String ref,
@@ -289,10 +253,6 @@ private static void observeAndResetPercentiles(
289253
long p95Micros = snapshot.getValueAtPercentile(95.0);
290254
long p99Micros = snapshot.getValueAtPercentile(99.0);
291255

292-
293-
294-
295-
296256
Attributes attrs = Attributes.of(
297257
ATTR_REF, ref,
298258
ATTR_OPERATION_TYPE, type.label(),
@@ -337,5 +297,4 @@ private void finish(OperationStatus status, int attempts, String errorKind) {
337297
metrics.recordOutcome(type, status, attempts, latencyMicros, errorKind);
338298
}
339299
}
340-
341300
}

0 commit comments

Comments
 (0)