Skip to content
Merged
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
9 changes: 9 additions & 0 deletions docs/operability/http-concurrency-evidence.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ The custom metrics separate:
- `lineageweave_ask_enqueue_duration`: time to persist and acknowledge the job;
- `lineageweave_read_duration{endpoint:posts|lineage}`: ordinary reader paths;
- `lineageweave_ask_poll_duration`: owner-scoped status polling.
- `lineageweave_ask_state_observations{job_status:...}`: how many observations
occurred while the one queued job was queued, running, or settled.

The harness observes one Ask job's real lifecycle; it does not keep provider
work running artificially. Report reader distributions with the state counts
so a long settled tail is not misrepresented as contended capacity. Per-VU
authentication is renewed after an HTTP 401 and the failed batch is retried
once, so observation windows longer than the realm access-token lifetime do
not silently become rejection measurements.

There are deliberately no pass/fail thresholds. A latency or concurrency SLO
requires a named deployment, representative workload, capacity evidence, and
Expand Down
8 changes: 3 additions & 5 deletions docs/product-requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,13 +126,11 @@ vendor selector, duplicate identity store, or psychometric substitute appears.
during provider execution. Authenticated concurrent HTTP behavior is
measured end to end against synthetic Compose data; latency and concurrency
become release thresholds only after a named deployment and representative
workload establish an approved capacity/SLO contract.
workload establish an approved capacity/SLO contract. Release evidence
includes observed Ask-job state counts, measured bottlenecks, and a capacity
envelope rather than an unmeasured concurrency claim.
- Public APIs have bounded inputs, stable typed responses, and provenance-
preserving failure states.
- Long-running web work executes asynchronously with observable job state;
release evidence includes an end-to-end k6 concurrency test, measured
bottlenecks, and a capacity envelope rather than an unmeasured concurrency
claim.
- WCAG 2.2 AA, keyboard/touch parity, responsive layouts, reduced motion,
design tokens, Storybook edge states, and screenshot review apply to every
customer-facing surface.
Expand Down
54 changes: 37 additions & 17 deletions scripts/k6_http_e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import http from "k6/http";
import { check, fail } from "k6";
import { Trend } from "k6/metrics";
import { Counter, Trend } from "k6/metrics";

const backendUrl = (__ENV.BACKEND_URL || "http://localhost:18420").replace(/\/$/, "");
const keycloakUrl = (__ENV.KEYCLOAK_URL || "http://localhost:18080").replace(/\/$/, "");
Expand All @@ -20,9 +20,12 @@ const password = __ENV.K6_PASSWORD || "lineageweave-demo-only";
const askEnqueueDuration = new Trend("lineageweave_ask_enqueue_duration", true);
const readDuration = new Trend("lineageweave_read_duration", true);
const askPollDuration = new Trend("lineageweave_ask_poll_duration", true);
const askStateObservations = new Counter("lineageweave_ask_state_observations");

export function setup() {
const tokenResponse = http.post(
let vuToken;

function authenticate() {
const response = http.post(
`${keycloakUrl}/realms/${realm}/protocol/openid-connect/token`,
{
grant_type: "password",
Expand All @@ -32,11 +35,28 @@ export function setup() {
},
{ tags: { endpoint: "oidc_token" } },
);
if (tokenResponse.status !== 200) {
fail(`synthetic OIDC login failed with HTTP ${tokenResponse.status}`);
if (response.status !== 200) {
fail(`synthetic OIDC login failed with HTTP ${response.status}`);
}
return response.json("access_token");
}

function readBatch(token, askJobId) {
const params = { headers: { Authorization: `Bearer ${token}` } };
return http.batch([
["GET", `${backendUrl}/api/posts`, null, { ...params, tags: { endpoint: "posts" } }],
["GET", `${backendUrl}/api/lineage`, null, { ...params, tags: { endpoint: "lineage" } }],
[
"GET",
`${backendUrl}/api/ask/jobs/${askJobId}`,
null,
{ ...params, tags: { endpoint: "ask_poll" } },
],
]);
}

const token = tokenResponse.json("access_token");
export function setup() {
const token = authenticate();
const headers = { Authorization: `Bearer ${token}`, "Content-Type": "application/json" };
const submitted = http.post(
`${backendUrl}/api/ask`,
Expand All @@ -51,21 +71,21 @@ export function setup() {
}

export default function (data) {
const params = { headers: { Authorization: `Bearer ${data.token}` } };
const responses = http.batch([
["GET", `${backendUrl}/api/posts`, null, { ...params, tags: { endpoint: "posts" } }],
["GET", `${backendUrl}/api/lineage`, null, { ...params, tags: { endpoint: "lineage" } }],
[
"GET",
`${backendUrl}/api/ask/jobs/${data.askJobId}`,
null,
{ ...params, tags: { endpoint: "ask_poll" } },
],
]);
vuToken ||= data.token;
let responses = readBatch(vuToken, data.askJobId);
if (responses.some((response) => response.status === 401)) {
vuToken = authenticate();
responses = readBatch(vuToken, data.askJobId);
}
Comment thread
seonghobae marked this conversation as resolved.

readDuration.add(responses[0].timings.duration, { endpoint: "posts" });
readDuration.add(responses[1].timings.duration, { endpoint: "lineage" });
askPollDuration.add(responses[2].timings.duration);
if (responses[2].status === 200) {
askStateObservations.add(1, {
job_status: String(responses[2].json("job_status_code") || "unknown"),
});
}
check(responses[0], { "posts read succeeds": (response) => response.status === 200 });
check(responses[1], { "lineage read succeeds": (response) => response.status === 200 });
check(responses[2], { "Ask poll succeeds": (response) => response.status === 200 });
Expand Down
14 changes: 14 additions & 0 deletions tests/test_k6_http_e2e_contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from pathlib import Path


SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "k6_http_e2e.js"


def test_k6_harness_renews_expired_auth_and_discloses_job_state() -> None:
"""Long observations retry expired authentication and separate job states."""
source = SCRIPT.read_text(encoding="utf-8")

assert "responses.some((response) => response.status === 401)" in source
assert source.count("responses = readBatch(vuToken, data.askJobId)") == 2
assert "lineageweave_ask_state_observations" in source
assert 'job_status: String(responses[2].json("job_status_code")' in source
Loading