From a178e9cb7484ad084a0de7a1b917eb30bc96f046 Mon Sep 17 00:00:00 2001 From: Avanti Date: Sun, 1 Nov 2026 10:38:31 +0530 Subject: [PATCH 1/2] test(relay): signal invite mint await instead of yield polling (#991) (cherry picked from commit a504b694277ab40e9cf1bff4debecdd198ad4c15) --- .changeset/invite-mint-signal.md | 12 ++++++++++++ crates/nostream-relay/src/testing.rs | 9 +++++++++ crates/nostream-relay/tests/handlers.rs | 14 ++++++-------- 3 files changed, 27 insertions(+), 8 deletions(-) create mode 100644 .changeset/invite-mint-signal.md diff --git a/.changeset/invite-mint-signal.md b/.changeset/invite-mint-signal.md new file mode 100644 index 00000000..60c5e69b --- /dev/null +++ b/.changeset/invite-mint-signal.md @@ -0,0 +1,12 @@ +--- +--- + +test(relay): signal invite mint await instead of yield polling (#991) + +`a_close_during_the_invite_mint_leaves_the_event_but_not_the_read` awaits a +notification from `InMemoryInviteCodeRepository` when the mint task starts rather +than sampling `creates_started()` across a 100-iteration `yield_now()` loop, +removing the failure mode where a loaded runner exhausts the loop and fails with +a message that accuses the mint. The test also documents why it authenticates +before subscribing (NIP-42 identity requirement) and records why the wire route +was abandoned for this assertion (after #978, `CLOSE` is a connection-loop prefix). diff --git a/crates/nostream-relay/src/testing.rs b/crates/nostream-relay/src/testing.rs index 6a17ca30..1ae17e1e 100644 --- a/crates/nostream-relay/src/testing.rs +++ b/crates/nostream-relay/src/testing.rs @@ -10,11 +10,13 @@ //! test-only module. use std::collections::BTreeMap; +use std::future::Future; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::{Arc, Mutex}; use futures_util::StreamExt; use futures_util::stream; +use tokio::sync::Notify; use nostream_config::settings::{ Info, Network, Nip42Settings, Nip43Settings, Nip45Settings, Nip50Settings, Nip56Settings, RateLimiterSettings, Settings, @@ -857,6 +859,7 @@ pub struct InMemoryInviteCodeRepository { /// invite mint's one `await` open long enough for a `CLOSE` to land in it. create_gate: Mutex>>, creates: AtomicU64, + create_signal: Notify, } impl InMemoryInviteCodeRepository { @@ -881,6 +884,11 @@ impl InMemoryInviteCodeRepository { gate } + /// Resolves when a `create` call has started, including one a gate is holding. + pub fn create_started(&self) -> impl Future + '_ { + self.create_signal.notified() + } + /// How many `create` calls have started, including one a gate is holding. pub fn creates_started(&self) -> u64 { self.creates.load(Ordering::SeqCst) @@ -918,6 +926,7 @@ impl InviteCodeRepository for InMemoryInviteCodeRepository { ) -> StorageFuture<'a, InviteCode> { Box::pin(async move { self.creates.fetch_add(1, Ordering::SeqCst); + self.create_signal.notify_one(); let gate = self .create_gate diff --git a/crates/nostream-relay/tests/handlers.rs b/crates/nostream-relay/tests/handlers.rs index 0bea2886..5d57b49c 100644 --- a/crates/nostream-relay/tests/handlers.rs +++ b/crates/nostream-relay/tests/handlers.rs @@ -1210,7 +1210,9 @@ async fn an_invite_request_mints_one_kind_28935_event_before_eose() { async fn a_close_during_the_invite_mint_leaves_the_event_but_not_the_read() { // The `CLOSE` is a direct handler call: `unsubscribe::handle` is synchronous, so in this // task the abort is a program-order predecessor of the gate release. A `CLOSE` over a - // socket is covered by `a_close_aborts_an_in_flight_read_without_a_frame`. + // socket is covered by `a_close_aborts_an_in_flight_read_without_a_frame` and `wire_parity`; + // after #978, `CLOSE` is a prefix running on the connection loop, so a wire test's timer + // could only say whether 50 ms was long enough, never whether the prefix ran. let mut settings = settings(); settings.nip43 = Some(nip43_settings()); @@ -1225,6 +1227,8 @@ async fn a_close_during_the_invite_mint_leaves_the_event_but_not_the_read() { "127.0.0.1".to_owned(), nostream_core::nip42::Nip42Session::with_challenge("challenge", 0), )); + // The invite mint is behind the NIP-42 identity check (`subscribe.rs:167-190`), + // so the test authenticates before it subscribes. assert!(connection.authenticate( nostream_core::crypto::public_key(&CLIENT_SECRET).expect("client public key"), nostream_core::EventId::from_byte_array([3; 32]), @@ -1259,13 +1263,7 @@ async fn a_close_during_the_invite_mint_leaves_the_event_but_not_the_read() { ); // The mint's one `await` is held, which is the window a `CLOSE` lands in. - for _ in 0..100 { - if fixture.invites.creates_started() == 1 { - break; - } - - tokio::task::yield_now().await; - } + fixture.invites.create_started().await; assert_eq!( fixture.invites.creates_started(), From 453d057caea04520d9f8912b4ecea22d7f3ff571 Mon Sep 17 00:00:00 2001 From: chappie-daemon Date: Sun, 1 Nov 2026 09:25:14 -0500 Subject: [PATCH 2/2] fix(relay): format the new import, and bound the mint wait Two review findings from Greptile on the contributed change, neither about the fix itself: - `cargo fmt --all --check` failed: `use tokio::sync::Notify` sat inside the futures import group, before the `nostream_config` block, so the required format step would have stopped the checks before the test ran. - `create_started().await` had no deadline, so a handler that panicked before minting would hang the test instead of failing it. The wait is now wrapped in a five-second timeout with a message that says which wait expired. `cargo fmt --all --check` is clean, and `cargo test -p nostream-relay --test handlers a_close_during_the_invite_mint` passes. --- crates/nostream-relay/src/testing.rs | 2 +- crates/nostream-relay/tests/handlers.rs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/nostream-relay/src/testing.rs b/crates/nostream-relay/src/testing.rs index 1ae17e1e..a9137f59 100644 --- a/crates/nostream-relay/src/testing.rs +++ b/crates/nostream-relay/src/testing.rs @@ -16,7 +16,6 @@ use std::sync::{Arc, Mutex}; use futures_util::StreamExt; use futures_util::stream; -use tokio::sync::Notify; use nostream_config::settings::{ Info, Network, Nip42Settings, Nip43Settings, Nip45Settings, Nip50Settings, Nip56Settings, RateLimiterSettings, Settings, @@ -30,6 +29,7 @@ use nostream_storage::repositories::{ NewReport, Nip05Verification, Nip05VerificationRepository, QueryStream, ReconciliationItem, Report, ReportRepository, StorageFuture, User, UserPatch, UserRepository, }; +use tokio::sync::Notify; use crate::connection::{Connection, Subscription}; use crate::error::RelayError; diff --git a/crates/nostream-relay/tests/handlers.rs b/crates/nostream-relay/tests/handlers.rs index 5d57b49c..cfaec136 100644 --- a/crates/nostream-relay/tests/handlers.rs +++ b/crates/nostream-relay/tests/handlers.rs @@ -1263,7 +1263,9 @@ async fn a_close_during_the_invite_mint_leaves_the_event_but_not_the_read() { ); // The mint's one `await` is held, which is the window a `CLOSE` lands in. - fixture.invites.create_started().await; + tokio::time::timeout(Duration::from_secs(5), fixture.invites.create_started()) + .await + .expect("the invite mint starts within five seconds"); assert_eq!( fixture.invites.creates_started(),