docs(tests): clarify readiness and timestamp rationale

This commit is contained in:
DanConwayDev
2026-06-19 12:11:13 +00:00
parent 7f69b29b01
commit a0a2f5be1a
3 changed files with 32 additions and 17 deletions
+8 -2
View File
@@ -71,7 +71,10 @@ impl AuditClient {
client.add_relay(relay_url).await?; client.add_relay(relay_url).await?;
client.connect().await; client.connect().await;
// Wait for connection to establish (with retries) // Wait up to ~5s for nostr-sdk's background relay task to finish the
// WebSocket handshake. Full audit suites can briefly starve the task
// scheduler under load, so the older ~2s window produced flakes even
// when the relay was already accepting connections.
let mut attempts = 0; let mut attempts = 0;
let mut connected = false; let mut connected = false;
while attempts < 50 { while attempts < 50 {
@@ -141,7 +144,10 @@ impl AuditClient {
client.add_relay(relay_url).await?; client.add_relay(relay_url).await?;
client.connect().await; client.connect().await;
// Wait for connection to establish (with retries) // Wait up to ~5s for nostr-sdk's background relay task to finish the
// WebSocket handshake. Full audit suites can briefly starve the task
// scheduler under load, so the older ~2s window produced flakes even
// when the relay was already accepting connections.
let mut attempts = 0; let mut attempts = 0;
let mut connected = false; let mut connected = false;
while attempts < 50 { while attempts < 50 {
+13 -9
View File
@@ -24,7 +24,7 @@ use crate::common::port::{self, PortReservation};
/// How long to wait for the spawned ngit-grasp subprocess to handle HTTP /// How long to wait for the spawned ngit-grasp subprocess to handle HTTP
/// requests before giving up on a single attempt. /// requests before giving up on a single attempt.
const READY_TIMEOUT: Duration = Duration::from_secs(5); const READY_TIMEOUT: Duration = Duration::from_secs(5);
/// How often to retry the TCP probe while waiting for readiness. /// How often to retry the HTTP probe while waiting for readiness.
const READY_POLL: Duration = Duration::from_millis(100); const READY_POLL: Duration = Duration::from_millis(100);
/// Extra grace after the HTTP service responds before declaring the relay /// Extra grace after the HTTP service responds before declaring the relay
/// ready. /// ready.
@@ -537,17 +537,18 @@ impl TestRelay {
&self.relay_data_path &self.relay_data_path
} }
/// Probe the listener with async TCP connects until it accepts, /// Probe the relay with a real HTTP request until it responds 200,
/// while concurrently watching for the subprocess to exit early /// while concurrently watching for the subprocess to exit early (the
/// (the signature of a lost port-bind race). Without the early-exit /// signature of a lost port-bind race). A raw TCP connect can succeed
/// check we'd burn the full [`READY_TIMEOUT`] on a process that's /// before Hyper has installed the service that accepts test clients,
/// already dead. /// so HTTP readiness is the point where WebSocket clients may safely
/// begin racing the relay.
async fn wait_for_ready_or_early_exit(&mut self) -> ReadyOutcome { async fn wait_for_ready_or_early_exit(&mut self) -> ReadyOutcome {
let deadline = Instant::now() + READY_TIMEOUT; let deadline = Instant::now() + READY_TIMEOUT;
loop { loop {
// Check whether the subprocess has already exited. If so the // Check whether the subprocess has already exited. If so no
// TCP probe will never succeed — bail immediately so the // readiness probe can succeed — bail immediately so the caller
// caller can retry with a fresh port. // can retry with a fresh port.
match self.process.try_wait() { match self.process.try_wait() {
Ok(Some(status)) => return ReadyOutcome::EarlyExit { status }, Ok(Some(status)) => return ReadyOutcome::EarlyExit { status },
Ok(None) => { /* still running */ } Ok(None) => { /* still running */ }
@@ -579,6 +580,9 @@ impl TestRelay {
async fn probe_http_ready(&self) -> std::io::Result<()> { async fn probe_http_ready(&self) -> std::io::Result<()> {
let probe = async { let probe = async {
let mut stream = tokio::net::TcpStream::connect(("127.0.0.1", self.port)).await?; let mut stream = tokio::net::TcpStream::connect(("127.0.0.1", self.port)).await?;
// Use the relay's HTTP handler rather than a bare TCP connect:
// this verifies the accept loop and Hyper service are both live,
// which is what the immediately-following WebSocket tests need.
let request = format!( let request = format!(
"GET / HTTP/1.1\r\nHost: 127.0.0.1:{}\r\nConnection: close\r\n\r\n", "GET / HTTP/1.1\r\nHost: 127.0.0.1:{}\r\nConnection: close\r\n\r\n",
self.port self.port
+11 -6
View File
@@ -74,10 +74,11 @@ async fn newer_30618_supersedes_older_and_preserves_old_in_history() {
.expect("create audit client"); .expect("create audit client");
let (_announcement, repo_id) = publish_served_repo(&client, "history-30618").await; let (_announcement, repo_id) = publish_served_repo(&client, "history-30618").await;
// The fixture published by `publish_served_repo` already saved a 30618 // `publish_served_repo` already saved a 30618 for this coordinate. Nostr
// for this coordinate. Use a deterministic later timestamp so this test's // replaceable ordering is only second-granular, so a same-second "old"
// "old" version is always a valid replacement instead of racing the // event can be rejected instead of superseding the fixture event. Start the
// fixture event within the same one-second Nostr timestamp bucket. // test sequence in a deterministic future second to make both replacements
// strictly newer.
let base_ts = Timestamp::from_secs(Timestamp::now().as_secs() + 1); let base_ts = Timestamp::from_secs(Timestamp::now().as_secs() + 1);
let old_state = build_state_version(&client, &repo_id, base_ts, "state-v1"); let old_state = build_state_version(&client, &repo_id, base_ts, "state-v1");
let new_state = build_state_version( let new_state = build_state_version(
@@ -102,6 +103,9 @@ async fn newer_30618_supersedes_older_and_preserves_old_in_history() {
let records = history let records = history
.superseded_records_for_coordinate_before( .superseded_records_for_coordinate_before(
&coordinate, &coordinate,
// The deterministic future timestamps can be ahead of wall-clock
// `Timestamp::now()`, so query just after the newest test event
// rather than using "now" as the history cutoff.
Timestamp::from_secs(new_state.created_at.as_secs() + 1), Timestamp::from_secs(new_state.created_at.as_secs() + 1),
) )
.await; .await;
@@ -251,8 +255,9 @@ async fn serving_behavior_unchanged_latest_version_is_still_served() {
.expect("create audit client"); .expect("create audit client");
let (_announcement, repo_id) = publish_served_repo(&client, "history-serving").await; let (_announcement, repo_id) = publish_served_repo(&client, "history-serving").await;
// The helper publishes an initial 30618 for this coordinate. Ensure the // `publish_served_repo` already saved a 30618 for this coordinate. Keep the
// state versions created here are strictly newer than that fixture event. // explicit state sequence in future seconds so the relay's second-granular
// replaceable ordering cannot tie it with the fixture event.
let base_ts = Timestamp::from_secs(Timestamp::now().as_secs() + 1); let base_ts = Timestamp::from_secs(Timestamp::now().as_secs() + 1);
let old_state = build_state_version(&client, &repo_id, base_ts, "state-old"); let old_state = build_state_version(&client, &repo_id, base_ts, "state-old");
let new_state = build_state_version( let new_state = build_state_version(