diff --git a/src/lib.rs b/src/lib.rs index 5870194..130f9cd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -675,6 +675,45 @@ mod tests { drop(iter2); } + /// Property: concurrent consumers of the same symbol each read the + /// identical, complete record set — thread scheduling cannot change the + /// outcome. Replaces a former flaky `/mnt`-dependent test that asserted + /// over a partial read; with the `SymbolGuard` ownership model the races + /// are closed, so a full-drain count is deterministic under contention. + #[test] + fn test_concurrent_consumers_read_identical_data() { + let dir = tempfile::tempdir().unwrap(); + write_m1_file(dir.path(), "EURUSD", 2017, 1); + write_m1_file(dir.path(), "EURUSD", 2017, 2); + write_m1_file(dir.path(), "EURUSD", 2017, 3); + + let server = Arc::new(DataServer::new(dir.path())); + + // Eight threads stream the same symbol at once, each draining fully. + let handles: Vec<_> = (0..8) + .map(|_| { + let srv = Arc::clone(&server); + std::thread::spawn(move || { + let mut iter = srv.stream_m1("EURUSD").unwrap(); + let mut count = 0u64; + while let Some(chunk) = iter.next_chunk() { + count += chunk.len() as u64; + } + count + }) + }) + .collect(); + + let counts: Vec = handles.into_iter().map(|h| h.join().unwrap()).collect(); + + // 3 files, one record each: every thread must read exactly 3. + assert!(counts[0] > 0); + assert!( + counts.iter().all(|&c| c == counts[0]), + "concurrent consumers must read identical data: {counts:?}" + ); + } + #[test] fn test_symbol_index_scan() { // Create a temp directory with mock files diff --git a/tests/data_server.rs b/tests/data_server.rs index 5b27fcc..ac18112 100644 --- a/tests/data_server.rs +++ b/tests/data_server.rs @@ -119,42 +119,3 @@ fn test_real_m1_full_iteration() { println!("AAPL.US M1: {total_records} records in {chunks} chunks"); assert!(total_records > 0, "Should have read some records"); } - -// FIXME: This test is flaky — thread scheduling can cause uneven read counts. -#[test] -fn test_real_concurrent_access() { - if skip_if_no_data() { - return; - } - let server = Arc::new(data_server::DataServer::new(TICKDATA_PATH)); - - // Spawn 4 threads all reading the same symbol simultaneously - let handles: Vec<_> = (0..4) - .map(|_| { - let srv = Arc::clone(&server); - std::thread::spawn(move || { - let mut iter = srv.stream_m1("EURUSD").expect("EURUSD"); - let mut count = 0u64; - // Read first 10 chunks only (for speed) - for _ in 0..10 { - if let Some(chunk) = iter.next_chunk() { - count += chunk.len() as u64; - } else { - break; - } - } - count - }) - }) - .collect(); - - let counts: Vec = handles.into_iter().map(|h| h.join().unwrap()).collect(); - - // All threads should read the same number of records - assert!(counts[0] > 0); - assert!( - counts.iter().all(|&c| c == counts[0]), - "All threads should read the same amount: {counts:?}" - ); - println!("Concurrent access: 4 threads each read {} records", counts[0]); -}