Skip to content

feat(api): idempotent test creation and an sla on flat requests - #607

Merged
bmscomp merged 2 commits into
mainfrom
feat/test-idempotency-key
Nov 5, 2026
Merged

bmscomp merged 2 commits into
mainfrom
feat/test-idempotency-key

Conversation

@bmscomp

@bmscomp bmscomp commented Nov 5, 2026

Copy link
Copy Markdown
Owner

Why

A Kubernetes operator that crashes between POST /api/tests and recording the run id would start a second run on restart: the API had no client-supplied key, and GET /api/tests could not find a run by one. A second gap: only a scenario could carry an sla, so a flat request could not be graded by the API. This is PR 1 of the operator work and stands on its own.

What changed

  • POST /api/tests takes Idempotency-Key. The key is stored in test_runs.client_key (V29, unique) from the run's first INSERT. A repeat answers 200 with the existing run, takes no permit and writes no handler audit row. A blank key, one over 128 characters or one with a control character is a 400 with fieldErrors.
  • GET /api/tests?clientKey= finds the run for a caller that must not create. TestRun returns clientKey, and /api/health lists capabilities: [clientKey], so a client can tell an API that honours the key.
  • CreateTestRequest takes a top-level sla, threaded onto the run on the spec path. sla beside a scenario is refused with a 400: the scenario path would otherwise drop it and grade the run without the gate.
  • A failed save leaves the key free: the stranded FAILED row is stored with no key, so a retry can start the run. Without this a database blip would burn the key.
  • WireSchemaTest pins the Jackson property names of TestSpec, SlaDefinition, TestScenario and ScenarioPhase to the request schema, minus a named file-only list.
  • File.java:lines refs under cli/cmd are re-pointed for the shifted lines. All schema mirrors are synced.

Deliberately not done

  • docs/book/11-api-reference.md is not updated; the header, the query parameter and the capability are documented in the OpenAPI annotations only.
  • A replay does not compare bodies: the same key with a different body gets the first run.

Known limits

  • Keyed creates are serialised by 64 striped locks. On the scenario path the lock also covers topic creation and submission, so an unrelated key on the same stripe can wait that long.
  • After an index race the loser leaves its own keyless FAILED row in the run list, as an unkeyed failed save already does.

Tests

Passing locally: ./mvnw verify (1,812 unit, 3 skipped; 100 ITs on Postgres, including IdempotentCreateIT, which forces the race with a table lock), spotless:check, go test ./..., go vet ./..., golangci-lint --new-from-rev=origin/main, govulncheck, check-cli-style.sh, check-cli-compat.sh.

The Maven runs used -Dquarkus.grpc.codegen.skip=true. Native image, CodeQL, Trivy and Kind were not run locally.

bmscomp and others added 2 commits November 5, 2026 18:03
A controller that crashes between POST /api/tests and recording the run
id would start a second run on restart, and only a scenario could carry
an sla. The Kubernetes operator needs both.

POST /api/tests takes an Idempotency-Key, stored in a unique column from
the run's first INSERT, so a repeat answers 200 with the run and starts
nothing. GET /api/tests?clientKey= finds the run, and /api/health lists
the capability. A save that fails leaves the key free, so the retry can
start the run once the fault is gone.

A flat request takes a top-level sla. One beside a scenario is refused,
since the scenario path would otherwise drop it and grade the run
without the gate.
CodeQL flagged the cast in TopicService.createTopic once this branch's
request path reached it: an int too large for a short silently wrapped
into a different replication factor. Out-of-range values now fail with
IllegalArgumentException, which @Retry already aborts on, and POST
/topics answers 400 rather than a 500. -1 still means the broker default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bmscomp
bmscomp merged commit 4c030c0 into main Nov 5, 2026
61 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant