Skip to content

Commit 3be6630

Browse files
authored
Fix non-deterministic tests (#944)
* Increase SYNC_MAX_DELAY * Increase mine_block timeout duration * Increase test_writer_reentrance timeout * Ignore underflows when dropping a UtxosChangedSubscription * Fix integration tests to use 100 as FD limit * Make TEST_FD_LIMIT maximum for tests, in case OS limit is lower * Remove OS free port allocation and just use a random port instead * clippy * Increase timeout_duration for mine_block
1 parent aecd2cf commit 3be6630

8 files changed

Lines changed: 55 additions & 49 deletions

File tree

notify/src/notifier.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -544,7 +544,7 @@ pub mod test_helpers {
544544
use async_channel::Sender;
545545
use std::time::Duration;
546546

547-
pub const SYNC_MAX_DELAY: Duration = Duration::from_secs(2);
547+
pub const SYNC_MAX_DELAY: Duration = Duration::from_secs(10);
548548

549549
pub type TestConnection = ChannelConnection<TestNotification>;
550550
pub type TestNotifier = Notifier<TestNotification, ChannelConnection<TestNotification>>;

notify/src/subscription/single.rs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -383,11 +383,17 @@ impl Display for UtxosChangedSubscription {
383383

384384
impl Drop for UtxosChangedSubscription {
385385
fn drop(&mut self) {
386-
trace!(
387-
"UtxosChangedSubscription: {} in total (drop {})",
388-
UTXOS_CHANGED_SUBSCRIPTIONS.fetch_sub(1, Ordering::SeqCst) - 1,
389-
self
390-
);
386+
// TODO: subscriptions were updated with `UTXOS_CHANGED_SUBSCRIPTIONS.fetch_sub(1, Ordering::SeqCst) - 1`
387+
// before, but due to some race condition it overflowed in some cases. Since the counter is only used for
388+
// logging purposes, we can afford to have an inaccurate count rather than risking an underflow panic.
389+
// It's still worth investigating the root cause of the race condition and fixing it.
390+
let subscriptions =
391+
match UTXOS_CHANGED_SUBSCRIPTIONS.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |count| count.checked_sub(1)) {
392+
Ok(previous) => previous - 1,
393+
Err(current) => current,
394+
};
395+
396+
trace!("UtxosChangedSubscription: {} in total (drop {})", subscriptions, self);
391397
}
392398
}
393399

testing/integration/src/common/daemon.rs

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use kaspa_grpc_server::service::GrpcService;
66
use kaspa_notify::subscription::context::SubscriptionContext;
77
use kaspa_rpc_core::notify::mode::NotificationMode;
88
use kaspa_rpc_service::service::RpcCoreService;
9-
use kaspa_utils::triggers::Listener;
9+
use kaspa_utils::{networking::ContextualNetAddress, triggers::Listener};
1010
use kaspad_lib::{args::Args, daemon::create_core_with_runtime};
1111
use parking_lot::RwLock;
1212
use std::{ops::Deref, sync::Arc, time::Duration};
@@ -96,25 +96,26 @@ pub struct Daemon {
9696
_appdir_tempdir: TempDir,
9797
}
9898

99-
impl Daemon {
100-
pub fn fill_args_with_random_ports(args: &mut Args) {
101-
// This should ask the OS to allocate free port for socket 1 to 4.
102-
let socket1 = std::net::TcpListener::bind(format!("127.0.0.1:{}", args.rpclisten.map_or(0, |x| x.normalize(0).port))).unwrap();
103-
let rpc_port = socket1.local_addr().unwrap().port();
104-
105-
let socket2 = std::net::TcpListener::bind(format!("127.0.0.1:{}", args.listen.map_or(0, |x| x.normalize(0).port))).unwrap();
106-
let p2p_port = socket2.local_addr().unwrap().port();
107-
108-
let socket3 = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
109-
let rpc_json_port = socket3.local_addr().unwrap().port();
99+
fn free_port() -> u16 {
100+
loop {
101+
let port = rand::random::<u16>() % (u16::MAX - 1024) + 1024;
102+
if let Ok(listener) = std::net::TcpListener::bind(format!("127.0.0.1:{}", port)) {
103+
drop(listener);
104+
return port;
105+
}
106+
}
107+
}
110108

111-
let socket4 = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
112-
let rpc_borsh_port = socket4.local_addr().unwrap().port();
109+
fn port_from_address(addr: Option<ContextualNetAddress>) -> u16 {
110+
addr.and_then(|x| if x.has_port() { Some(x.normalize(0).port) } else { None }).unwrap_or_else(free_port)
111+
}
113112

114-
drop(socket1);
115-
drop(socket2);
116-
drop(socket3);
117-
drop(socket4);
113+
impl Daemon {
114+
pub fn fill_args_with_random_ports(args: &mut Args) {
115+
let rpc_port = port_from_address(args.rpclisten);
116+
let p2p_port = port_from_address(args.listen);
117+
let rpc_json_port = free_port();
118+
let rpc_borsh_port = free_port();
118119

119120
args.rpclisten = Some(format!("0.0.0.0:{rpc_port}").try_into().unwrap());
120121
args.listen = Some(format!("0.0.0.0:{p2p_port}").try_into().unwrap());

testing/integration/src/common/utils.rs

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -192,24 +192,19 @@ pub async fn mine_block(pay_address: Address, submitting_client: &GrpcClient, li
192192
let block_hash = header.hash;
193193
submitting_client.submit_block(template.block, false).await.unwrap();
194194

195+
let timeout_duration = Duration::from_millis(10_000);
196+
195197
// Wait for each listening client to get notified the submitted block was added to the DAG
196198
for client in listening_clients.iter() {
197-
let block_daa_score: u64 = match timeout(Duration::from_millis(500), client.block_added_listener().unwrap().receiver.recv())
198-
.await
199-
.unwrap()
200-
.unwrap()
201-
{
202-
Notification::BlockAdded(BlockAddedNotification { block }) => {
203-
assert_eq!(block.header.hash, block_hash);
204-
block.header.daa_score
205-
}
206-
_ => panic!("wrong notification type"),
207-
};
208-
match timeout(Duration::from_millis(500), client.virtual_daa_score_changed_listener().unwrap().receiver.recv())
209-
.await
210-
.unwrap()
211-
.unwrap()
212-
{
199+
let block_daa_score: u64 =
200+
match timeout(timeout_duration, client.block_added_listener().unwrap().receiver.recv()).await.unwrap().unwrap() {
201+
Notification::BlockAdded(BlockAddedNotification { block }) => {
202+
assert_eq!(block.header.hash, block_hash);
203+
block.header.daa_score
204+
}
205+
_ => panic!("wrong notification type"),
206+
};
207+
match timeout(timeout_duration, client.virtual_daa_score_changed_listener().unwrap().receiver.recv()).await.unwrap().unwrap() {
213208
Notification::VirtualDaaScoreChanged(VirtualDaaScoreChangedNotification { virtual_daa_score }) => {
214209
assert_eq!(virtual_daa_score, block_daa_score + 1);
215210
}

testing/integration/src/rpc_tests.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ async fn sanity_test() {
5353
..Default::default()
5454
};
5555

56-
let fd_total_budget = fd_budget::limit();
56+
let fd_total_budget = fd_budget::test_limit();
5757
let mut daemon = Daemon::new_random_with_args(args, fd_total_budget);
5858
let client = daemon.start().await;
5959
let (sender, _) = async_channel::unbounded();

testing/integration/src/tasks/daemon.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ impl DaemonTask {
148148
impl Task for DaemonTask {
149149
fn start(&self, stop_signal: SingleTrigger) -> Vec<JoinHandle<()>> {
150150
let ready_signal = self.ready_signal.trigger.clone();
151-
let fd_total_budget = fd_budget::limit();
151+
let fd_total_budget = fd_budget::test_limit();
152152
let mut daemon = Daemon::with_manager(self.client_manager.clone(), fd_total_budget);
153153
let task = tokio::spawn(async move {
154154
warn!("Daemon task starting...");

utils/src/fd_budget.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,18 @@ pub fn try_set_fd_limit(limit: u64) -> std::io::Result<u64> {
6161
}
6262
}
6363

64+
const TEST_FD_LIMIT: i32 = 100;
65+
66+
// Many tests can be run in parallel, and each of them may acquire some FDs, so we set a lower limit for tests to avoid hitting the actual OS limit.
67+
// Note: Integration tests need to explicitly use this constant and not `limit()`, since they set `#[cfg(test)]` to false.
68+
pub fn test_limit() -> i32 {
69+
limit().min(TEST_FD_LIMIT)
70+
}
71+
6472
pub fn limit() -> i32 {
6573
cfg_if::cfg_if! {
6674
if #[cfg(test)] {
67-
100
75+
TEST_FD_LIMIT
6876
}
6977
else if #[cfg(target_os = "windows")] {
7078
rlimit::getmaxstdio() as i32
@@ -78,10 +86,6 @@ pub fn limit() -> i32 {
7886
}
7987
}
8088

81-
pub fn remainder() -> i32 {
82-
limit() - ACQUIRED_FD.load(Ordering::Relaxed)
83-
}
84-
8589
#[cfg(test)]
8690
mod tests {
8791
use super::*;

utils/src/sync/rwlock.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,9 +136,9 @@ mod tests {
136136
rx.await.unwrap();
137137
// Make sure the reader acquires the lock during writer yields. We give the test a few chances to acquire
138138
// in order to make sure it passes also in slow CI environments where the OS thread-scheduler might take its time
139-
let read = timeout(Duration::from_millis(18), l.read()).await.unwrap_or_else(|_| panic!("failed at iteration {i}"));
139+
let read = timeout(Duration::from_millis(36), l.read()).await.unwrap_or_else(|_| panic!("failed at iteration {i}"));
140140
drop(read);
141-
timeout(Duration::from_millis(500), tokio::task::spawn_blocking(move || h.join())).await.unwrap().unwrap().unwrap();
141+
timeout(Duration::from_millis(1000), tokio::task::spawn_blocking(move || h.join())).await.unwrap().unwrap().unwrap();
142142
}
143143
}
144144

0 commit comments

Comments
 (0)