open-standards/joule-code/joule-code-rs/crates/joule-moq-relay/tests/relay_energy.rs
Transaction Science 24fd4ab374
Some checks failed
wai-audit collector and player integration / does this change touch it (push) Successful in 15s
wai-app and Create / wai-app · does this change touch it (push) Successful in 19s
joule-moq-http live HTTP egress / joule-moq-http · does this change touch it (push) Successful in 32s
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Successful in 25s
wai-quantum studio and desktop / does this change touch it (push) Successful in 13s
wai-sink-swift native sink package / does this change touch it (push) Successful in 14s
wai-sink-android native sink library / does this change touch it (push) Successful in 15s
WAI Studio / does this change touch it (push) Successful in 13s
wai-quantum for JavaScript / does this change touch it (push) Successful in 4m23s
joule-moq-remote host agent / joule-moq-remote · does this change touch it (push) Successful in 6m50s
wai-ledger service / does this change touch it (push) Successful in 6m41s
WAI operation catalogue / does this change touch it (push) Successful in 6m42s
wai-fleet service / does this change touch it (push) Successful in 6m46s
wai-pack / wai-pack · does this change touch it (push) Successful in 6m45s
joule-moq-room room service / joule-moq-room · does this change touch it (push) Successful in 6m55s
joule-moq-agent AI participants / joule-moq-agent · does this change touch it (push) Successful in 7m2s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (push) Has been cancelled
WAI Studio / Studio in Chromium and Firefox · macos (push) Has been cancelled
WAI Studio / Studio in WebKit · linux (push) Has been cancelled
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (push) Has been cancelled
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (push) Has been cancelled
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (push) Has been cancelled
wai release binaries build on Windows / the seven release binaries · windows (push) Has been cancelled
wai-quantum studio and desktop / studio · browser (push) Has been cancelled
wai-quantum studio and desktop / desktop app · macos (push) Has been cancelled
wai-quantum studio and desktop / desktop app · linux (push) Has been cancelled
wai-quantum studio and desktop / desktop app · windows (push) Has been cancelled
wai-quantum for JavaScript / javascript · linux (push) Has been cancelled
wai-pack / wai-pack · linux (push) Has been cancelled
wai-pack / wai-pack · linux-arm64 (push) Has been cancelled
wai-pack / wai-pack · macos (push) Has been cancelled
wai-pack / wai-pack · independent verifier (Python) (push) Has been cancelled
wai-pack / wai-pack · the floor through a media prober (push) Has been cancelled
wai-pack / wai-pack · dependency licences (push) Has been cancelled
wai-ledger service / service · linux (push) Has been cancelled
wai-ledger service / service · macos (push) Has been cancelled
wai-fleet service / service · linux (push) Has been cancelled
wai-fleet service / service · macos (push) Has been cancelled
WAI examples / WAI examples · does this change touch them (push) Has been cancelled
WAI examples / WAI examples · portable · linux (push) Has been cancelled
WAI examples / WAI examples · the archive, the readiness check, the browser and Apple · macos (push) Has been cancelled
WAI examples / WAI examples · android-app · linux-arm64 (push) Has been cancelled
WAI operation catalogue / the catalogue and every door · linux (push) Has been cancelled
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Has been cancelled
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Has been cancelled
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Has been cancelled
wai-audit collector and player integration / collector · linux (push) Has been cancelled
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (push) Has been cancelled
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Has been cancelled
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Has been cancelled
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Has been cancelled
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Has been cancelled
joule-moq-room room service / joule-moq-room, every target (push) Has been cancelled
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (push) Has been cancelled
joule-moq-remote host agent / joule-moq-remote, every target (macos) (push) Has been cancelled
joule-moq-http live HTTP egress / joule-moq-http, every target (push) Has been cancelled
joule-moq-agent AI participants / joule-moq-agent, every target (push) Has been cancelled
joule-code conformance gate / Build + round-trip C1-C4 vectors (push) Has been cancelled
joule-code conformance gate / joule-moq-relay, every target (push) Has been cancelled
joule-code conformance gate / joule-moq-relay, its own energy on a counter (macos) (push) Has been cancelled
joule-code conformance gate / joule-moq-ingest, every target (push) Has been cancelled
joule-moq-relay: the relay's energy shared among the rooms it served in a window, by the bytes each moved (WAI D79)
The relay's counter reads the whole process (macOS task energy) or the whole package (RAPL),
never one room, so a room's record carried the unmetered marker over any span the relay also
served another room in: on a relay carrying more than one room at a time almost no record had a
figure (completeness item N3).

The rule, written down as WAI decision D79 and live-metering §3.11: over a sealed window, J is the
counter's advance across its span, B the payload bytes every session of the window moved in and
out on every track (other rooms', tracks of no room and hop links' included), and b the bytes a
room's sessions moved on its tracks, which are its record's track entries' bytes. A room served
alone carries J whole, OnChipCounter, as before. A room that shared the window carries
floor(J x b / B) uJ, exact integers, under ModelBased (tag 2, no evidence): the counter's figure
divided by a model, never stated under the counter's class. No bytes, a share under 1 uJ, a
window with no byte moved, a window with no figure (no counter, not live, started again in): the
unmetered marker, never 0 uJ. A room with no session in the window has no share; a room whose
clock begins inside the window gets no share of time before it; a window's shares sum to at most
J, the rest (tracks of no room, rounding) attributed to no room. Bytes, not CPU time: the relay
holds no CPU time per room, and energy-measurement 6.3 rejects dividing by busy time; live-
metering 4.6 already divides a hop's figure among grants by bytes. A record over ended sessions
keeps the rule of before.

- window.rs: WindowEnergy (the rule's name, J with its evidence or why none, B, each room's bytes,
  what its record carries and the uJ) in the window statement; share(); check_record holds a
  room's record to it and check_shares recomputes every share from the statement's parts, each
  fault refused by its own reason.
- relay.rs: window_energy; window_record carries the share; window_statement states it; a seal
  holding joule ceilings attributes shares too. Relay::with_counter and
  RelayServer::start_with_counter take a counter from the embedding application.
- metering.rs: HopRecord::with_share (tag 2), the reader takes tag 2; moved, served_alone,
  rooms_of.
- tests/room_energy_shares.rs (every host, a counter of its own): two rooms and a track of no
  room in one window, the shares, a room with no session, a room whose clock began inside the
  window, then the room that left charged nothing and the room alone carrying the whole.
  relay_energy.rs: a window beside another room's objects carries the room's share. Eleven planted
  faults (shares rounding up, B without the no-room bytes, a room with no session given the
  whole, a room charged before its clock, a started-again window given a figure, the rule dropped
  from the statement, a share under the counter's class, a share of 0 uJ, and three weakened
  checks) each caught.
- WAI: live-metering 3.11 (new) and 8; energy-methods 3.1 and 8.5's Allocation field, with the
  energy-methods corpus regenerated for that text alone; fleet 4.3; D79; change note.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-12 07:03:02 -05:00

326 lines
20 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! The relay's own energy in a room's per-hop session record (WAI `live-metering.md` §8,
//! `energy-measurement.md` §2.3, §3.1), on a host whose counter the relay reads: macOS's task
//! counter, or Linux's package counter where the configuration states the host runs the
//! relay alone (this test states it, to read the counter; the record is not published).
//!
//! A publisher sends a room's video for over a second to one subscriber. The record then
//! carries `OnChipCounter` with its evidence, reads back and verifies; the same room beside a
//! second room's session carries the unmetered marker, as does a relay whose configuration
//! turns the meter off. A record over a window of sessions still open carries the relay's
//! figure for that window too, a quiet session beside the room sharing nothing, and with another
//! room's objects in the window the room's share of the window's figure by the bytes each room
//! moved, `ModelBased` (WAI decision D79; `tests/room_energy_shares.rs` holds the rule on a
//! counter of its own, on every host). Where the host offers no counter (a
//! container, another platform, macOS on an Intel processor, whose task counter reads 0 while
//! the relay works) the relay says why, its record carries the unmetered marker and never a
//! figure of 0 J, and the test prints SKIPPED with the reason and passes only if
//! `JOULE_MOQ_RELAY_ENERGY_REQUIRE` is unset: a skip is never a pass where the counter is required.
//! A relay told to hold joule ceilings ends the sessions of a grant its own shares pass, sealing
//! on its own interval with nobody flushing it, and refuses the grant's next SETUP; one not told
//! to states the ceiling and ends nothing.
mod common;
use common::*;
use joule_moq::draft22::{SessionError, Token};
use joule_moq_relay::client::{Client, ClientOptions};
use joule_moq_relay::config::{EnergyMode, MeteredGrant};
use joule_moq_relay::metering::HopRecord;
use joule_moq_relay::transport::{quic, TransportError};
use std::time::Duration;
const ROOM: &str = "5f3c2b1a09f8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c5b4a39281706f5e";
const OTHER_ROOM: &str = "a0b1c2d3e4f5061728394a5b6c7d8e9fa0b1c2d3e4f5061728394a5b6c7d8e9f";
const GROUPS: u64 = 40;
fn key(i: u8) -> ed25519_dalek::SigningKey {
ed25519_dalek::SigningKey::from_bytes(&[i; 32])
}
fn required() -> bool {
std::env::var_os("JOULE_MOQ_RELAY_ENERGY_REQUIRE").is_some()
}
/// A publisher of `room` sends `GROUPS` groups over about 1.6 s to one subscriber; with
/// `other`, a second room's publisher stays connected across it. Both close before the
/// record is signed, and the sampler has passed the span's end.
async fn session(server: &joule_moq_relay::RelayServer, other: bool) -> HopRecord {
let relay = server.relay().clone();
assert!(until(STEP, || relay.energy_sampled_until().is_some()).await, "the sampler has a first sample");
let k = key(7);
let owner = hex::encode(k.verifying_key().to_bytes());
let t = Tracks::new(ns(&[ROOM, &owner, "video"]), "0", 10, 1, 0);
let (p, _ep) = connect(server, Via::Quic, Default::default()).await;
let _pt = publish_tracks(&p, &t, None).await;
let second = if other {
let (q, eq) = connect(server, Via::Quic, Default::default()).await;
let u = Tracks::new(ns(&[OTHER_ROOM, &owner, "video"]), "0", 20, 2, 0);
let pu = publish_tracks(&q, &u, None).await;
Some((q, eq, pu))
} else {
None
};
let (s, _es) = connect(server, Via::Quic, Default::default()).await;
let next = joule_moq::draft22::Parameters::new().with(joule_moq::draft22::Parameter::LocationFilter(joule_moq::draft22::LocationFilter::NextObject));
let v = subscribe_both(&s, &t, next).await;
let publish = async {
for g in 0..GROUPS {
publish_group(&p, &k, &t, g, &payload("e", g, 4096)).await;
tokio::time::sleep(Duration::from_millis(40)).await;
}
};
let read = collect_concurrent(&s, STEP, Duration::from_secs(1), |got| got.iter().filter(|x| x.header.track_alias == v.media_alias).count() as u64 == GROUPS);
let ((), got) = tokio::join!(publish, read);
assert_eq!(got.iter().filter(|x| x.header.track_alias == v.media_alias).count() as u64, GROUPS, "every group reached the subscriber");
s.close(0, "done");
p.close(0, "done");
let want = if other { 3 } else { 2 };
if let Some((q, _, _)) = &second {
q.close(0, "done");
}
assert!(until(STEP, || relay.summaries().len() >= want).await, "the sessions' summaries");
let closed_us = relay.summaries().iter().map(|s| (s.summary.closed_unix_ms + 1) * 1000).max().unwrap();
assert!(until(STEP, || relay.energy_sampled_until().is_some_and(|t| t > closed_us + 200_000)).await, "the sampler passed the span's end");
let room: [u8; 32] = hex::decode(ROOM).unwrap().try_into().unwrap();
relay.hop_record(room, 0)
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn a_room_served_alone_carries_the_relays_own_energy() {
let server = relay(|c| c.metering.dedicated_host = true).await;
let Some(scope) = server.relay().energy_scope() else {
let why = server.relay().energy_unavailable().expect("a relay with no counter says why").to_string();
assert!(!required(), "JOULE_MOQ_RELAY_ENERGY_REQUIRE is set, and this host offers the relay no counter: {why}");
// No counter: the record carries the unmetered marker and the reason is stated, never a
// figure of 0 J (on macOS on an Intel processor the task counter reads 0 while work runs).
unmetered_run(&server, 11).await;
println!("SKIPPED: this host offers the relay no energy counter (macOS task counter, or a readable RAPL package counter); nothing was measured: {why}");
server.shutdown().await;
return;
};
let rec = tokio::time::timeout(Duration::from_secs(120), session(&server, false)).await.expect("in time");
let bytes = rec.to_bytes();
let back = HopRecord::decode(&bytes, &rec.ledger_bytes()).expect("the record reads back");
assert_eq!(back, rec);
rec.verify().expect("the record verifies under the relay's key");
assert_eq!(rec.class_tag, 4, "OnChipCounter: the counter was live, the room alone ({} µJ)", rec.joules);
assert!(rec.joules > 0);
let ppm = u32::from_be_bytes(rec.class_evidence[..4].try_into().unwrap());
let window = u64::from_be_bytes(rec.class_evidence[4..12].try_into().unwrap());
assert_eq!(rec.class_evidence[12], 0, "filtering Unknown: an unprivileged reader cannot see one");
assert_eq!(window, rec.end - rec.start, "the window is the record's span");
assert!(rec.joules * u64::from(ppm) >= 288_676, "the resolution floor");
assert_eq!(bytes.len(), 281 + 13 + 90 * rec.entries.len(), "tag 4's evidence, and nothing per object or per viewer");
println!(
"measured: {} µJ over {} µs, scope {}, relative standard uncertainty {} ppm ({:.3} W)",
rec.joules,
window,
scope.label(),
ppm,
rec.joules as f64 / window as f64
);
server.shutdown().await;
// On this counter too: a record over a window of sessions still open, and a hop told to hold
// a joule ceiling. Beside the tests of this file, not new ones: the counter's CI job holds
// the file to their number.
a_window_of_open_sessions_carries_the_relays_own_energy().await;
tokio::time::timeout(Duration::from_secs(120), ceiling_run(true)).await.expect("in time");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn a_room_beside_another_rooms_session_carries_the_unmetered_marker() {
let server = relay(|c| c.metering.dedicated_host = true).await;
if server.relay().energy_scope().is_none() {
let why = server.relay().energy_unavailable().expect("a relay with no counter says why");
assert!(!required(), "JOULE_MOQ_RELAY_ENERGY_REQUIRE is set, and this host offers the relay no counter: {why}");
println!("SKIPPED: this host offers the relay no energy counter; nothing was measured: {why}");
server.shutdown().await;
return;
}
let rec = tokio::time::timeout(Duration::from_secs(120), session(&server, true)).await.expect("in time");
assert_eq!((rec.joules, rec.class_tag, rec.class_evidence.len()), (0, 0, 0), "another room's work in the span: the relay's energy is not this room's");
server.shutdown().await;
a_window_beside_another_rooms_objects_carries_the_rooms_share().await;
tokio::time::timeout(Duration::from_secs(120), ceiling_run(false)).await.expect("in time");
}
/// The record over a window of `ROOM`, its sessions open throughout: a publisher sends for over
/// a second to a subscriber, beside a session that counts nothing; with `other`, a second room's
/// publisher sends its objects in the window too. With the window's statement of every session.
async fn window_of_open_sessions(server: &joule_moq_relay::RelayServer, other: bool) -> (HopRecord, joule_moq_relay::window::SignedWindow) {
let relay = server.relay().clone();
assert!(until(STEP, || relay.energy_sampled_until().is_some()).await, "the sampler has a first sample");
let k = key(7);
let owner = hex::encode(k.verifying_key().to_bytes());
let t = Tracks::new(ns(&[ROOM, &owner, "video"]), "0", 10, 1, 0);
let (p, _ep) = connect(server, Via::Quic, Default::default()).await;
let _pt = publish_tracks(&p, &t, None).await;
let (quiet, _eq) = connect(server, Via::Quic, Default::default()).await;
let (s, _es) = connect(server, Via::Quic, Default::default()).await;
let next = joule_moq::draft22::Parameters::new().with(joule_moq::draft22::Parameter::LocationFilter(joule_moq::draft22::LocationFilter::NextObject));
let v = subscribe_both(&s, &t, next).await;
let u = Tracks::new(ns(&[OTHER_ROOM, &owner, "video"]), "0", 20, 2, 0);
let (q, _eo) = connect(server, Via::Quic, Default::default()).await;
let _pu = publish_tracks(&q, &u, None).await;
// The window opens here, with every session up and the sampler running.
let (w, _, _) = relay.flush();
assert_eq!(relay.seal(w), vec![w]);
let publish = async {
for g in 0..GROUPS {
publish_group(&p, &k, &t, g, &payload("w", g, 4096)).await;
if other {
publish_group(&q, &k, &u, g, &payload("o", g, 64)).await;
}
tokio::time::sleep(Duration::from_millis(40)).await;
}
};
let read = collect_concurrent(&s, STEP, Duration::from_secs(1), |got| got.iter().filter(|x| x.header.track_alias == v.media_alias).count() as u64 == GROUPS);
let ((), got) = tokio::join!(publish, read);
assert_eq!(got.iter().filter(|x| x.header.track_alias == v.media_alias).count() as u64, GROUPS, "every group reached the subscriber");
let window = relay.flush_and_seal(Duration::from_millis(300)).await;
assert_eq!(relay.stats().sessions_open, 4, "nobody left");
assert!(until(STEP, || relay.energy_sampled_until().is_some_and(|t| t > window.to_unix_us + 200_000)).await, "the sampler passed the window's end");
let room: [u8; 32] = hex::decode(ROOM).unwrap().try_into().unwrap();
let rec = relay.window_record(room, 0, window.index).expect("the window's record");
let statement = relay.window_statement(window.index, None).expect("the window's statement");
for c in [&p, &quiet, &s, &q] {
c.close(0, "done");
}
(rec, statement)
}
async fn a_window_of_open_sessions_carries_the_relays_own_energy() {
let server = relay(|c| c.metering.dedicated_host = true).await;
let (rec, statement) = tokio::time::timeout(Duration::from_secs(120), window_of_open_sessions(&server, false)).await.expect("in time");
statement.check_shares().expect("the window's figure, the room's whole");
statement.check_record(&rec, 0).expect("the record carries what the statement states");
let back = HopRecord::decode(&rec.to_bytes(), &rec.ledger_bytes()).expect("the record reads back");
assert_eq!(back, rec);
rec.verify().expect("the record verifies under the relay's key");
assert_eq!(rec.class_tag, 4, "OnChipCounter: the counter was live, the room alone in the window, a quiet session beside it ({} µJ)", rec.joules);
assert!(rec.joules > 0);
let window = u64::from_be_bytes(rec.class_evidence[4..12].try_into().unwrap());
assert_eq!(window, rec.end - rec.start, "the evidence's window is the window's span");
println!("measured over a window of open sessions: {} µJ over {window} µs", rec.joules);
server.shutdown().await;
}
async fn a_window_beside_another_rooms_objects_carries_the_rooms_share() {
let server = relay(|c| c.metering.dedicated_host = true).await;
let (rec, statement) = tokio::time::timeout(Duration::from_secs(120), window_of_open_sessions(&server, true)).await.expect("in time");
statement.check_shares().expect("every share recomputes from the window's parts");
statement.check_record(&rec, 0).expect("the record carries the room's share the statement states");
let e = statement.statement.energy.as_ref().expect("the window's figure and shares");
let j = e.joules_micro.expect("the counter was live across the window");
assert_eq!(rec.class_tag, joule_moq_relay::metering::TAG_MODEL_BASED, "another room's objects in the window: the relay's figure is not this room's, and its share is a model's, not the counter's ({} µJ of {j})", rec.joules);
assert!(rec.joules > 0 && rec.joules < j && rec.class_evidence.is_empty());
assert_eq!(rec.joules, joule_moq_relay::window::share(j, rec.entry_bytes(), e.bytes).unwrap(), "⌊J × b / B⌋");
println!("shared over a window with another room's objects: {} µJ of {j} µJ, {} of {} bytes", rec.joules, rec.entry_bytes(), e.bytes);
server.shutdown().await;
}
/// A publisher under a grant with no ceiling sends for over a second to a subscriber under a
/// grant whose joule ceiling is one microjoule, and the window closes on their counts.
async fn ceiling_run(hold: bool) {
let (free, ceiled) = (b"no-ceiling".to_vec(), b"one-microjoule".to_vec());
let bearer = |t: &Vec<u8>| ClientOptions { tokens: vec![Token::UseValue { token_type: 0, value: t.clone() }], ..ClientOptions::default() };
let metered = |t: &Vec<u8>, grant: String, joules: u64| MeteredGrant { token_sha256: hex::encode(joule_moq_relay::auth::sha256(t)), grant, joules_ceiling_micro: joules, funds_ceiling: u64::MAX, currency: String::new() };
let tiny = "c1".repeat(32);
let server = relay(|c| {
c.metering.dedicated_host = true;
c.metering.grants = vec![metered(&free, "c0".repeat(32), u64::MAX), metered(&ceiled, tiny.clone(), 1)];
c.control.hold_joule_ceilings = hold;
// The grace outlasts the sampler's interval many times over: a seal shares the window's
// figure, which needs a sample past its end.
if hold {
c.control.flush_interval_ms = Some(500);
c.control.flush_grace_ms = 1_000;
}
})
.await;
let relay = server.relay().clone();
assert!(until(STEP, || relay.energy_sampled_until().is_some()).await, "the sampler has a first sample");
let k = key(7);
let t = Tracks::new(ns(&[ROOM, &hex::encode(k.verifying_key().to_bytes()), "video"]), "0", 10, 1, 0);
let (p, _ep) = connect(&server, Via::Quic, bearer(&free)).await;
let _pt = publish_tracks(&p, &t, None).await;
let (s, _es) = connect(&server, Via::Quic, bearer(&ceiled)).await;
let next = joule_moq::draft22::Parameters::new().with(joule_moq::draft22::Parameter::LocationFilter(joule_moq::draft22::LocationFilter::NextObject));
let v = subscribe_both(&s, &t, next).await;
if !hold {
let (w, _, _) = relay.flush();
assert_eq!(relay.seal(w), vec![w]);
}
let publish = async {
for g in 0..GROUPS {
publish_group(&p, &k, &t, g, &payload("c", g, 4096)).await;
tokio::time::sleep(Duration::from_millis(40)).await;
}
};
let read = collect_concurrent(&s, STEP, Duration::from_secs(1), |got| got.iter().filter(|x| x.header.track_alias == v.media_alias).count() as u64 == GROUPS);
let ((), _) = tokio::join!(publish, read);
if hold {
// Nothing here flushes: the relay's own interval seals the window, and holds the ceiling.
match tokio::time::timeout(Duration::from_secs(60), s.conn.closed()).await.expect("closed in time") {
TransportError::Closed { code, reason, .. } => {
assert_eq!(code, SessionError::UNAUTHORIZED.0);
assert!(reason.starts_with(joule_moq_relay::relay::PAST_CEILING), "{reason}");
}
other => panic!("{other:?}"),
}
assert_eq!(relay.grants_past_ceiling(), vec![tiny], "its share of the relay's figure passed one microjoule");
assert!(until(STEP, || relay.stats().sessions_open == 1).await, "the grant with no ceiling goes on");
// The grant's next SETUP is refused: the relay closes the session at its door, with the
// bare reason. The relay reads the client's SETUP, sends its own saying it refuses, and
// closes the session once the client has left or its wait has passed (session.rs); this
// client stays, as one that does not know GRANT_REFUSED does, so `Client::connect`'s Ok
// is not an admission. What is held is the relay's own close, within STEP of the
// client's SETUP, and its reason exactly the door's: a session admitted and then ended at the relay's next seal closes with the
// same code and a longer reason, which says the grant's joules passed its ceiling.
let ep = quic::client_endpoint(loopback(), vec![server.identity.chain[0].clone()], vec![joule_moq::draft22::ALPN.to_vec()], None).unwrap();
let conn: joule_moq_relay::transport::SharedConnection = std::sync::Arc::new(quic::connect(&ep, server.quic_addr.unwrap(), "localhost").await.expect("a QUIC session"));
let client = Client::connect(conn.clone(), ClientOptions { ignore_refusal: true, ..bearer(&ceiled) }).await;
match tokio::time::timeout(STEP, conn.closed()).await {
Ok(TransportError::Closed { code, reason, by_peer: true }) => {
assert_eq!(code, SessionError::UNAUTHORIZED.0, "{reason}");
assert_eq!(reason, joule_moq_relay::relay::PAST_CEILING, "a grant past its ceiling here was admitted, and its session ended after");
}
Ok(other) => panic!("the grant's next session ended otherwise: {other:?}"),
Err(_) => panic!("a grant past its ceiling here was admitted: its session was still open {STEP:?} after its SETUP (connect: {:?})", client.as_ref().map(|_| "Ok")),
}
drop(client);
} else {
// The grace outlasts the sampler's interval, as the relay's own seal's does above.
relay.flush_and_seal(Duration::from_secs(2)).await;
assert!(relay.grants_past_ceiling().is_empty(), "the ceiling is stated, and the deployment's meter holds it");
assert_eq!(relay.stats().sessions_open, 2, "nobody ended");
s.close(0, "done");
}
p.close(0, "done");
server.shutdown().await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn a_relay_whose_meter_is_off_carries_the_unmetered_marker() {
let server = relay(|c| c.metering.energy = EnergyMode::Off).await;
assert!(server.relay().energy_scope().is_none());
assert_eq!(server.relay().energy_unavailable(), Some("metering.energy is off"));
unmetered_run(&server, 9).await;
server.shutdown().await;
}
/// A relay with no counter: a room's record over a session carries the unmetered marker, and
/// the session's share is never read.
async fn unmetered_run(server: &joule_moq_relay::RelayServer, seed: u8) {
let relay = server.relay().clone();
let k = key(seed);
let t = Tracks::new(ns(&[ROOM, &hex::encode(k.verifying_key().to_bytes()), "video"]), "0", 10, 1, 0);
let (p, _ep) = connect(server, Via::Quic, Default::default()).await;
let _pt = publish_tracks(&p, &t, None).await;
publish_group(&p, &k, &t, 0, &payload("e", 0, 64)).await;
p.close(0, "done");
assert!(until(STEP, || !relay.summaries().is_empty()).await);
let rec = relay.hop_record(hex::decode(ROOM).unwrap().try_into().unwrap(), 0);
assert_eq!((rec.joules, rec.class_tag), (0, 0), "the unmetered marker, never a figure of 0 J");
assert_eq!(relay.summaries()[0].summary.relay_energy.acquisition, "unmetered", "a session's share is never read");
}