Imported from proselytear/proselyte-1m-rps (
AGENTS.md). Install upstream withnpx skills add proselytear/proselyte-1m-rps. Copyright stays with the author.
AGENTS.md — engineering rules for this repository
For anyone changing this repository: human or agent. Five minutes to read, and
it applies to every commit. Longer reasoning lives in docs/; AI-session
specifics live in CLAUDE.md.
This is a lab, not a product. Its output is not throughput — it is evidence that survives being checked by a reader who re-runs the commands.
1. Measurement discipline
- Nothing in this repository has been measured yet. Do not add a number to a document, a comment, a dashboard title or a commit message unless you ran the command that produced it, on a machine you can describe.
- A result without the command that produced it is not a result. State the
scenario,
TARGET_RPS,DURATION, the mode, the machine and the CPU/RAM Docker had. The template is indocs/local-limitations.md§12. - A run with
k6_dropped_iterations_total > 0is not a measurement. The generator was the limit, not the service. Re-run with a lowerTARGET_RPSor a higherMAX_VUS; do not reinterpret the numbers you already have. - Never claim throughput. This stand cannot prove 1M RPS or any absolute capacity figure, and no future change will make it able to. "1M RPS" is the question the series asks.
- Never compare across machines. Only before/after on the same machine, in the same session, counts.
make smokeis a contract check, not a measurement. No number from ~600 requests may be quoted.
2. One variable at a time
- Change the mode, or the pool size, or the TTL, or the working set, or the rate. Not two of them.
- The DB-only baseline (
app.cache.mode=none,make mode-db) must stay reachable forever. Nothing may replace it, and no mode may be deleted in favour of an "optimised" one — modes are alternatives, not stages. - Optimisations land additively, behind an existing seam (for example a new
CacheLoaderCoordinatorimplementation), with the previous behaviour still selectable and still tested. - Quantify the cost of not having a thing before adding it, and state what the
change cost next to what it gained. Full procedure:
docs/roadmap.md§"How a roadmap item is allowed to land"; step-by-step recipe inCLAUDE.md§7.
3. Cardinality
docs/metrics-catalog.md is the contract between the application and the
dashboards: a panel may only use a metric listed there, and a metric listed there
must exist at runtime.
Never a metric label: sku, region, requestId, traceId/spanId, a raw
URI or path, an exception message, a k6 url tag, a testid-like unbounded value
on an application metric.
uristays the templated route/api/v1/offers/{sku}.- Exception class name in logs (
exceptionType), never the message. - Label value sets are closed and enumerated in
docs/metrics-catalog.md§1; new values go there first. - Loki: a label is a stream, a field is free. High-cardinality data goes
in fields (
docs/logging.md§4). - k6:
systemTagsexcludesurl, every offer request carries the constantnametagGET /api/v1/offers/{sku}, and global tags are exactlytestid,scenario,service(load-tests/k6/lib/config.js). - All custom application metrics are defined in one file,
apps/offer-snapshot-java/src/main/java/com/proselyte/offersnapshot/metrics/OfferMetrics.java, so every emittable label value is visible in one place. Keep it that way.
4. Local-only, and secrets
- Every published port binds to
127.0.0.1. There is no authentication anywhere — not Grafana, not Mimir, not Loki, not the internal diagnostics API that can flush every cache. Never bind to another interface, never add a tunnel, never expose/internal/*through Nginx. - PostgreSQL and Redis are deliberately not published. Use
make psqlandmake cache-stats. .env.exampleholds intentionally fake local credentials. No real secret, token, password or hostname ever enters this repository, including in a comment, a test fixture or a dashboard JSON..envis local and untracked. Editversions.envand.env.example; letscripts/sync-versions.shpropagate.- Every lab-only setting in
infra/**(replication_factor: 1, in-memory rings, disabled auth and multitenancy, raised ingestion limits, 100 % trace sampling, filesystem storage) carries a marker. Keep the markers, and never copy those files anywhere shared.
5. Version pinning
versions.envis the single manifest. Nolatest, anywhere, ever.- A version bump changes the thing being measured and invalidates comparisons with earlier runs. Do it as its own task, never inside an unrelated change.
- A major bump requires a dedicated task and an ADR.
- Bumping k6 requires re-verifying the output flag name
(
experimental-prometheus-rwis version-sensitive;docs/load-testing.md§1). - Bumping the OpenTelemetry agent can change span attribute names — the JDBC
attribute check in
scripts/verify-observability.shis a WARN for that reason. - Checksums are verified against the file published upstream next to the artifact; never hardcode a constant you did not obtain by downloading.
6. When an ADR is required
Write one in docs/adr/ before merging when the change:
- alters
contracts/openapi/offer-snapshot-v1.yamlin any way — the Go implementation of article 7 must satisfy the same file; - bumps a major version of the JDK, Spring Boot, PostgreSQL, Redis, k6, the OTel agent or any observability backend;
- adds or removes a dependency, a component in
compose.yaml, or a telemetry backend; - changes signal ownership (Micrometer owns metrics, the OTel agent owns traces, Logback owns logs — two producers for one signal is the thing to prevent);
- changes a metric name, a label set or a log field name (these are read by
dashboards and by
scripts/verify-observability.sh, and renaming one breaks a panel silently); - changes the read-path model: modes, TTL semantics, failure policy, the loader-coordination strategy;
- changes the schema (
infra/postgres/migrations/) or the dataset shape; - removes a deliberate absence (retries, circuit breakers,
@Cacheable, JPA, WebFlux, Lombok, async logging) — each of those is a recorded decision.
An ADR states the decision, the alternatives, the cost accepted, and what would make it wrong.
7. Tests
Unit tests (*Test, make test-unit) — fast, no containers. Cover the HTTP
contract and headers, key normalisation and validation, row mapping including the
time-zone assertion, the read path branch by branch, Redis failure behaviour, and
MDC cleanup in RequestIdFilter (a leaked MDC entry attaches one request's id to
another request's logs).
Integration tests (*IT, make test-integration) — Testcontainers with a
real PostgreSQL and a real Redis, applying the same migration files the compose
stack uses. Required when the behaviour only exists against a real store, or
when the assertion is about what did not happen (DbBaselineModeIT: in none
mode nothing lands in Redis).
Rules:
- every new read-path branch gets a unit test;
- every new mode/layer interaction gets an integration test;
- every metric name the dashboards depend on gets asserted in
ObservabilityIT, not trusted; - never weaken an assertion to make a build green;
make testruns both and is the gate before any behavioural claim.
8. Documentation
- Documentation is part of the change, not a follow-up.
- A new metric or label value →
docs/metrics-catalog.md(and the dashboards may only use what is listed there). - A new panel or a changed query →
docs/dashboards.md. Dashboards are edited as JSON ininfra/grafana/dashboards/and validated withmake dashboards-validate; Grafana runs withallowUiUpdates: false, so a UI save is refused on purpose. - A new failure mode →
docs/runbook.md, symptom first. - A new deliberate absence →
docs/roadmap.md, with what must be measured first. - A change that affects an article →
docs/article-map.md, including the "must not change" column. - Every claim in a document must be checkable from a file in the repository or from a command the reader can run.
9. Definition of done
A change in this repository is done when all of these hold:
- It changes one thing, and the previous behaviour is still reachable and still tested.
- The DB-only baseline still works and is still the comparison point.
- No new high-cardinality label, in Micrometer, Loki or k6.
- No invented number, screenshot or
reports/artifact anywhere in the diff. - Every
maketarget and file path mentioned in the diff exists. - Versions come from
versions.env; nolatest; a major bump has its own task and an ADR. -
contracts/openapi/offer-snapshot-v1.yamlis unchanged, or the change has an ADR and the Go implementation could still satisfy it. - Unit tests cover the new branch; an integration test covers it if it needs a real store.
-
make checkpasses. -
make testpasses. - If the change touches the runtime:
make up,make seed,make smoke,make observability-verify,make article2-verifywere run, and the output is reported as it was — including[WARN]lines. - Docs updated in the same commit:
docs/metrics-catalog.md,docs/dashboards.md,docs/runbook.md,docs/roadmap.md,docs/article-map.mdas applicable, plus an ADR if §6 applies. - Anything you could not run is stated plainly, with the reason.