|
| 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. |
0 commit comments