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
12 changes: 12 additions & 0 deletions .changeset/invite-mint-signal.md
Original file line number Diff line number Diff line change
@@ -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).
9 changes: 9 additions & 0 deletions crates/nostream-relay/src/testing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
//! test-only module.

use std::collections::BTreeMap;
use std::future::Future;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex};

Expand All @@ -28,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;
Expand Down Expand Up @@ -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<Option<Arc<tokio::sync::Semaphore>>>,
creates: AtomicU64,
create_signal: Notify,
}

impl InMemoryInviteCodeRepository {
Expand All @@ -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<Output = ()> + '_ {
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)
Expand Down Expand Up @@ -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
Expand Down
16 changes: 8 additions & 8 deletions crates/nostream-relay/tests/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());

Expand All @@ -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]),
Expand Down Expand Up @@ -1259,13 +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.
for _ in 0..100 {
if fixture.invites.creates_started() == 1 {
break;
}

tokio::task::yield_now().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(),
Expand Down
Loading