Some checks failed
joule-moq-http live HTTP egress / joule-moq-http · does this change touch it (push) Successful in 15s
joule-moq-remote host agent / joule-moq-remote · does this change touch it (push) Successful in 16s
joule-moq-room room service / joule-moq-room · does this change touch it (push) Successful in 18s
wai-key-proxy / does this change touch it (push) Successful in 16s
wai-quantum views in agent hosts / does this change touch it (push) Successful in 19s
wai-ledger service / does this change touch it (push) Successful in 1m35s
wai-audit collector and player integration / does this change touch it (push) Successful in 1m52s
wai-quantum for Python / does this change touch it (push) Successful in 1m36s
wai-quantum service / does this change touch it (push) Successful in 1m36s
wai-fleet service / does this change touch it (push) Successful in 1m46s
wai-quantum docs, course and locales / does this change touch it (push) Successful in 1m37s
wai-quantum contract / does this change touch it (push) Successful in 1m38s
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Successful in 1m55s
wai-quantum service / service energy · linux packages (push) Successful in 2m54s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (push) Successful in 1m47s
joule-code conformance gate / joule-moq-relay, every target (push) Successful in 5m59s
joule-moq-http live HTTP egress / joule-moq-http, every target (push) Successful in 7m39s
joule-moq-room room service / joule-moq-room, every target (push) Successful in 7m17s
joule-code conformance gate / joule-moq-ingest, every target (push) Successful in 18m3s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Failing after 23m19s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 11s
wai-key-proxy / proxy · linux (push) Failing after 57s
wai-ledger service / service · linux (push) Successful in 3m5s
wai-quantum contract / the policy is linked, and the deprecations page reads (push) Successful in 7s
wai-audit collector and player integration / collector · linux (push) Successful in 2m22s
wai-quantum docs, course and locales / the Studio's words in every locale · linux (push) Successful in 11s
joule-code conformance gate / Build + round-trip C1-C4 vectors (push) Successful in 6m13s
wai-quantum views in agent hosts / views · the page and its engine (push) Successful in 7m42s
wai-fleet service / service · linux (push) Successful in 2m46s
wai-quantum docs, course and locales / the course and the command line's languages · linux (push) Successful in 3m38s
wai-quantum views in agent hosts / views · a scripted host (push) Successful in 8m36s
wai-quantum contract / API compatibility · linux (push) Successful in 8m47s
wai-quantum for Python / python · linux (push) Successful in 8m49s
wai-quantum contract / schemas and the committed tool lists · linux (push) Successful in 10m18s
wai-quantum service / service · linux (push) Successful in 9m47s
joule-code conformance gate / joule-moq-relay, its own energy on a counter (macos) (push) Successful in 53s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (push) Successful in 54s
wai-ledger service / service · macos (push) Successful in 1m7s
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (push) Successful in 2m19s
wai-fleet service / service · macos (push) Successful in 48s
wai-quantum for Python / python · macos (push) Successful in 4m9s
wai-quantum service / service · macos (push) Successful in 8m38s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Successful in 39s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Successful in 18m53s
wai-quantum docs, course and locales / every command and snippet in the docs · linux (push) Successful in 37m17s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Successful in 9m25s
tests/depth.rs:1286 failed on CI's Linux runner at 3966ee245 (wai-quantum service / service · linux):
"the decodes ended before a lent turn's 5 s: [6.680335661, 4.522442124]". Both calls had been
answered 200. The test sized its decode alone first (4.6 s at 20,000 shots, then 11.2 s at 51,944)
and then held that two of it sent at once each took more than 5.5 s by the test's clock; the runner
ran one of the two in 4.5 s a moment later. The service did nothing wrong: the test asked a shared
host to keep its speed between a probe and the run.
What the test is for: a call on a turn lent past its client's share runs past the turn's 5 s
(offload::LENT_FOR) while no one waits, where it was once stopped at 5 s. The service now says so
itself. GET /metrics has wai_quantum_lent_turns_total{pool,event}, every series from the start:
lent (a turn lent), and of the readings on one, moved (onto the client's own turn as that came
free), kept (still on the lent turn after its 5 s, as no one waited) and taken_back (stopped for a
request that waited). Each is counted in offload::read_until by the service's own clock, before
the call is answered. No answer, limit or refusal changes.
The test holds, of the two decodes at once: both answered 200, no lent turn taken back, and one
kept past its 5 s by that count. No time it measures is held to anything. A round in which no lent
turn lasted 5 s checked nothing, so the decode is sized again from the time it took (to about 12 s)
and the two are sent again, four rounds at most; so is a decode the service projects past the time
a call may take (J031), from its projection. Any other refusal fails the test in the round it is
in: the judged calls are never sent again after a failure. lent_decode no longer asserts that its
sizing landed within 8 to 24 s, which was the same wager.
Other assertions of that shape in tests/depth.rs and tests/contract.rs: none in contract.rs. One
lower bound on a measured time remains in depth.rs (a `?wait=2` poll of another client held 1.9 s,
a_long_poll_past_its_client_s_holds_says_when_to_poll_again): the service's own timer holds it
against a fixed job of tens of seconds that nothing sizes, and load only lengthens both.
Gates, on the Framework (Linux x86_64, itself at load 46 on 32 cores), the test held to two CPUs:
quiet 2 in 2; beside six busy loops 3 in 3; eight busy loops on for 7 s and off for 7 s, 5 in 5
(one sized again after a first round of 4.3 s); ten busy loops stopped as the sizing ended, CI's
case, 4 in 4, every one through a second round (first rounds of 0.9 to 4.6 s, kept 0; second
rounds kept 1); six busy loops started as the sizing ended, 3 in 3, one through a J031 refusal
(projected past 100 s) and a second round. Main's test under the same stopped load fails 3 in 3
at depth.rs:1286 as on CI ("ended before a lent turn's 5 s: [3.1, 3.2]", "[2.4, 2.5]",
"[1.9, 1.9]"). Planted fault, the one the test is for (Others::waiting always true): fails 2 in 2
in its first round, the lent decode refused at 5.0 s (503, J032), taken_back counted 1. The whole
crate passes with cargo test --locked --release (depth.rs's 42 among them), and cargo clippy
--locked --release --all-targets -- -D warnings is clean, on Rust 1.99.1.
Co-Authored-By: Claude Fable 5.2 <[email protected]>
159 lines
8.1 KiB
Rust
159 lines
8.1 KiB
Rust
//! `GET /metrics` of the service, run as it is deployed (`wai-quantum-service serve`) on loopback:
|
|
//! it reads strictly as the text format, every family present from the start; a job run moves the
|
|
//! jobs held by status and the requests by route, method and status; `HEAD` is answered as `GET`,
|
|
//! its length stated and without the text; another method is `405`; a request through a front is
|
|
//! a path the service does not have, `404`; and no job, client or key of those requests is a
|
|
//! label value in it.
|
|
|
|
use serde_json::{Value, json};
|
|
use std::io::{Read, Write};
|
|
use std::net::{TcpListener, TcpStream};
|
|
use std::process::{Command, Stdio};
|
|
use std::time::{Duration, Instant};
|
|
use wai_metrics::read::{Family, identifier_like, label_values, parse, value};
|
|
|
|
mod common;
|
|
|
|
use common::Running;
|
|
|
|
fn free_port() -> u16 {
|
|
TcpListener::bind("127.0.0.1:0").unwrap().local_addr().unwrap().port()
|
|
}
|
|
|
|
/// One request, and its head and body as sent.
|
|
fn raw(port: u16, method: &str, path: &str, extra: &str, body: &str) -> (String, String) {
|
|
let mut s = TcpStream::connect(("127.0.0.1", port)).unwrap();
|
|
s.set_read_timeout(Some(Duration::from_secs(120))).unwrap();
|
|
write!(s, "{method} {path} HTTP/1.1\r\nHost: 127.0.0.1\r\nContent-Type: application/json\r\nContent-Length: {}\r\n{extra}Connection: close\r\n\r\n{body}", body.len()).unwrap();
|
|
let mut out = String::new();
|
|
s.read_to_string(&mut out).unwrap();
|
|
let (head, body) = out.split_once("\r\n\r\n").unwrap_or((out.as_str(), ""));
|
|
(head.to_string(), body.to_string())
|
|
}
|
|
|
|
fn status(head: &str) -> u16 {
|
|
head.split_whitespace().nth(1).and_then(|c| c.parse().ok()).unwrap_or(0)
|
|
}
|
|
|
|
fn header<'a>(head: &'a str, name: &str) -> Option<&'a str> {
|
|
head.lines().find_map(|l| l.split_once(':').filter(|(n, _)| n.eq_ignore_ascii_case(name)).map(|(_, v)| v.trim()))
|
|
}
|
|
|
|
/// `GET /metrics`, read strictly, no label value naming a party.
|
|
fn scrape(port: u16) -> Vec<Family> {
|
|
let (head, body) = raw(port, "GET", "/metrics", "", "");
|
|
assert_eq!(status(&head), 200, "{head}");
|
|
assert_eq!(header(&head, "content-type"), Some(wai_metrics::CONTENT_TYPE), "{head}");
|
|
let f = parse(&body).unwrap_or_else(|e| panic!("the exposition does not read: {e:?}\n{body}"));
|
|
for v in label_values(&f) {
|
|
assert!(!identifier_like(v), "a label value that names a party: {v}\n{body}");
|
|
}
|
|
f
|
|
}
|
|
|
|
/// The service, and the directory of its store, removed once it has ended.
|
|
struct Service(Running, std::path::PathBuf);
|
|
|
|
impl Drop for Service {
|
|
fn drop(&mut self) {
|
|
self.0.stop();
|
|
let _ = std::fs::remove_dir_all(&self.1);
|
|
}
|
|
}
|
|
|
|
fn start() -> (Service, u16) {
|
|
let dir = std::env::temp_dir().join(format!("wai-qsvc-metrics-{}", std::process::id()));
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
let port = free_port();
|
|
let child = Command::new(env!("CARGO_BIN_EXE_wai-quantum-service"))
|
|
.arg("serve")
|
|
.env("WAI_QSVC_ADDR", format!("127.0.0.1:{port}"))
|
|
.env("WAI_QSVC_STORE", dir.join("store"))
|
|
.env("WAI_QSVC_KEY", dir.join("signing.key"))
|
|
.env("WAI_QSVC_JOB_GB", "2")
|
|
.env("WAI_QSVC_BUDGET_GB", "4")
|
|
.env("WAI_QSVC_WORKERS", "1")
|
|
.env("WAI_QSVC_ENERGY", "0")
|
|
.stdout(Stdio::null())
|
|
.stderr(Stdio::null())
|
|
.spawn()
|
|
.expect("the service starts");
|
|
let service = Service(Running::new(child), dir);
|
|
let t = Instant::now();
|
|
while TcpStream::connect(("127.0.0.1", port)).is_err() || status(&raw(port, "GET", "/v1/health", "", "").0) != 200 {
|
|
assert!(t.elapsed() < Duration::from_secs(60), "the service did not answer");
|
|
std::thread::sleep(Duration::from_millis(50));
|
|
}
|
|
(service, port)
|
|
}
|
|
|
|
#[test]
|
|
fn the_service_answers_metrics_that_move_with_its_jobs() {
|
|
let (_service, port) = start();
|
|
|
|
// Before any job: every family, the jobs at 0 in every status.
|
|
let f = scrape(port);
|
|
for s in ["queued", "running", "done", "failed", "cancelled", "interrupted"] {
|
|
assert_eq!(value(&f, "wai_quantum_jobs", &[("status", s)]), Some(0.0), "{s}");
|
|
}
|
|
assert_eq!(value(&f, "wai_quantum_memory_held_bytes", &[]), Some(0.0));
|
|
assert_eq!(value(&f, "wai_quantum_energy_unmetered", &[]), Some(1.0), "the service was started with energy metering off");
|
|
assert_eq!(value(&f, "wai_quantum_restored_jobs", &[("outcome", "finished")]), Some(0.0), "an empty store");
|
|
for pool in ["reading", "checks"] {
|
|
for event in ["lent", "moved", "kept", "taken_back"] {
|
|
assert_eq!(value(&f, "wai_quantum_lent_turns_total", &[("pool", pool), ("event", event)]), Some(0.0), "{pool} {event}: no turn lent yet");
|
|
}
|
|
}
|
|
assert!(f.iter().any(|x| x.name == "wai_quantum_requests_total"));
|
|
assert_eq!(value(&f, "wai_quantum_requests_total", &[("route", "/v1/health"), ("method", "GET"), ("code", "200")]).map(|v| v >= 1.0), Some(true), "the health checks while it started");
|
|
|
|
// A job run: done, held, and its requests counted by route, not by its id.
|
|
let bell = json!({ "qasm": "OPENQASM 3.0; qubit[2] q; bit[2] c; h q[0]; cx q[0], q[1]; c = measure q;", "shots": 100 });
|
|
let (head, body) = raw(port, "POST", "/v1/run", "", &bell.to_string());
|
|
let run = status(&head);
|
|
let made: Value = serde_json::from_str(&body).unwrap_or(Value::Null);
|
|
// On a loaded host the run may still be running when its wait ends: 202, then polled.
|
|
assert!(run == 200 || run == 202, "{head}\n{body}");
|
|
let id = made["id"].as_str().unwrap_or_else(|| panic!("the job's id: {body}")).to_string();
|
|
let t = Instant::now();
|
|
let mut polls = 0;
|
|
loop {
|
|
let (h, b) = raw(port, "GET", &format!("/v1/jobs/{id}?wait=20"), "", "");
|
|
assert_eq!(status(&h), 200, "{h}\n{b}");
|
|
polls += 1;
|
|
let j: Value = serde_json::from_str(&b).unwrap_or(Value::Null);
|
|
if j["status"] != "queued" && j["status"] != "running" {
|
|
assert_eq!(j["status"], "done", "{b}");
|
|
break;
|
|
}
|
|
assert!(t.elapsed() < Duration::from_secs(180), "the job did not finish: {b}");
|
|
}
|
|
let f = scrape(port);
|
|
assert_eq!(value(&f, "wai_quantum_jobs", &[("status", "done")]), Some(1.0));
|
|
let code = run.to_string();
|
|
assert_eq!(value(&f, "wai_quantum_requests_total", &[("route", "/v1/run"), ("method", "POST"), ("code", code.as_str())]), Some(1.0));
|
|
assert_eq!(value(&f, "wai_quantum_requests_total", &[("route", "/v1/jobs/{id}"), ("method", "GET"), ("code", "200")]), Some(f64::from(polls)), "each poll, by the route's pattern");
|
|
assert_eq!(value(&f, "wai_quantum_requests_total", &[("route", "/metrics"), ("method", "GET"), ("code", "200")]), Some(1.0), "the scrape before this one");
|
|
let text = raw(port, "GET", "/metrics", "", "").1;
|
|
assert!(!text.contains(&id), "the job's id is in the exposition");
|
|
|
|
// HEAD as GET, its length stated and without the text; another method 405.
|
|
let (head, body) = raw(port, "HEAD", "/metrics", "", "");
|
|
assert_eq!(status(&head), 200, "{head}");
|
|
assert_eq!(header(&head, "content-type"), Some(wai_metrics::CONTENT_TYPE), "{head}");
|
|
assert!(header(&head, "content-length").and_then(|n| n.parse::<usize>().ok()).is_some_and(|n| n > 0), "{head}");
|
|
assert!(body.is_empty(), "HEAD was answered with the text: {body:?}");
|
|
let (head, _) = raw(port, "POST", "/metrics", "", "{}");
|
|
assert_eq!((status(&head), header(&head, "allow")), (405, Some("GET, HEAD")), "{head}");
|
|
|
|
// Through a front, by any of the fields a front sets: a path the service does not have.
|
|
for front in wai_metrics::FRONT_FIELDS {
|
|
let (head, body) = raw(port, "GET", "/metrics", &format!("{front}: 192.0.2.7\r\n"), "");
|
|
assert_eq!(status(&head), 404, "{front}: {head}");
|
|
assert!(!body.contains("# HELP"), "{front}: a request through a front was answered the counters");
|
|
}
|
|
let f = scrape(port);
|
|
assert_eq!(value(&f, "wai_quantum_requests_total", &[("route", "/metrics"), ("method", "GET"), ("code", "404")]), Some(wai_metrics::FRONT_FIELDS.len() as f64));
|
|
assert_eq!(value(&f, "wai_quantum_requests_total", &[("route", "/metrics"), ("method", "HEAD"), ("code", "200")]), Some(1.0));
|
|
}
|