Skip to content

Commit d098041

Browse files
committed
test(storage): make the harnesses hold on CI's machines, not just this one
Three failures, each real rather than a runner being slow. **A fixed delay decided whether the migration crash test tested anything.** On the runner the child had copied nothing in its 150 ms; on this machine it had copied some. It uses the same failpoint as the other children now: ten chunks copied, the eleventh interrupted, the rest untouched, the same every time. The progress handshake it used instead is gone with it. **btrfs does not report freed space immediately.** The test slept 200 ms and took one reading, which on ext4 was enough and on btrfs was not. It polls for the space to come back, with a deadline, and returns the last real reading so a genuine failure still fails on the number rather than on the wait. **Two things only CI's toolchain sees.** Clippy 1.98 rejects an unbounded range in a for loop where 1.95 did not, and making the file store public under the test feature put a doc link to a private item in front of rustdoc for the first time. Local checks now include `cargo doc --features test-utils`, which is what would have caught the second one here.
1 parent 8ffdf90 commit d098041

3 files changed

Lines changed: 31 additions & 61 deletions

File tree

src/storage/file_store.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1749,8 +1749,7 @@ fn decode_chunk_name(name: &str) -> Option<XorName> {
17491749
/// # Errors
17501750
///
17511751
/// Returns the underlying I/O error. Off Unix there is no way to flush a directory through
1752-
/// the standard library, so this reports success without being able to promise anything;
1753-
/// see [`fsync_dir`].
1752+
/// the standard library, so this reports success without being able to promise anything.
17541753
pub fn fsync_path(path: &Path) -> std::io::Result<()> {
17551754
fsync_dir(path)
17561755
}

tests/migration_crash_safety.rs

Lines changed: 8 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -102,51 +102,6 @@ fn kill_child_at_failpoint(role: &str, root: &Path, let_through: u64) -> PathBuf
102102
marker
103103
}
104104

105-
/// Run a child for a while and then kill it, without a failpoint.
106-
///
107-
/// For the cases where the point is that the kill lands somewhere in a long stretch of
108-
/// work rather than at one named instant. The child reports progress so this never kills
109-
/// one that has not started.
110-
fn kill_child_once_it_is_working(role: &str, root: &Path, run_for: Duration) {
111-
let progress = root.join(format!("working-{role}"));
112-
let _ = std::fs::remove_file(&progress);
113-
114-
let exe = std::env::current_exe().expect("this test binary");
115-
let mut child = Command::new(exe)
116-
.arg("--exact")
117-
.arg(role)
118-
.arg("--nocapture")
119-
.arg("--ignored")
120-
.env("ANT_CRASH_TEST_ROOT", root)
121-
.env("ANT_CRASH_TEST_PROGRESS", &progress)
122-
.stdout(Stdio::null())
123-
.stderr(Stdio::null())
124-
.spawn()
125-
.expect("spawn the child");
126-
127-
let deadline = std::time::Instant::now() + Duration::from_secs(120);
128-
while !progress.exists() {
129-
if let Ok(Some(status)) = child.try_wait() {
130-
panic!("the child exited before doing any work: {status}");
131-
}
132-
if std::time::Instant::now() > deadline {
133-
let _ = child.kill();
134-
panic!("the child never started working");
135-
}
136-
std::thread::sleep(Duration::from_millis(10));
137-
}
138-
std::thread::sleep(run_for);
139-
child.kill().expect("kill the child");
140-
let _ = child.wait();
141-
}
142-
143-
/// Say that this child has started doing the work it was spawned for.
144-
fn report_working() {
145-
if let Ok(path) = std::env::var("ANT_CRASH_TEST_PROGRESS") {
146-
let _ = std::fs::write(path, b"working");
147-
}
148-
}
149-
150105
/// Where the child was told to work.
151106
fn child_root() -> PathBuf {
152107
PathBuf::from(std::env::var("ANT_CRASH_TEST_ROOT").expect("the child needs a root"))
@@ -165,14 +120,15 @@ async fn child_writes_until_killed() {
165120
.await
166121
.expect("open");
167122

168-
report_working();
169123
// Always a chunk it has not written before, so the kill lands in real work rather
170124
// than in a re-offer of something already on disk. An earlier version cycled the same
171125
// hundred keys and spent almost all its time confirming duplicates.
172-
for n in 0.. {
126+
let mut n = 0usize;
127+
loop {
173128
let content = chunk_bytes(n);
174129
let address = ant_node::client::compute_address(&content);
175130
let _ = store.put(&address, &content).await;
131+
n += 1;
176132
}
177133
}
178134

@@ -202,11 +158,6 @@ async fn child_migrates_until_killed() {
202158
break;
203159
};
204160
let _ = store.copy_batch(&[*key], 0, 0, &shutdown).await;
205-
// Said only after a copy has actually happened, so a copier that did nothing at
206-
// all cannot be mistaken for one that was interrupted part-way.
207-
if store.legacy_only_keys().len() < keys.len() {
208-
report_working();
209-
}
210161
}
211162
panic!("the child copied everything before it was killed, so nothing was interrupted");
212163
}
@@ -352,11 +303,11 @@ async fn a_killed_migration_still_has_every_chunk_somewhere() {
352303
std::fs::create_dir_all(&root).expect("mkdir");
353304
let keys = seed_legacy_from(&root, 0).await;
354305

355-
kill_child_once_it_is_working(
356-
"child_migrates_until_killed",
357-
&root,
358-
Duration::from_millis(150),
359-
);
306+
// Ten chunks copied, the eleventh interrupted. An earlier version killed the child
307+
// after a fixed delay, which on a fast runner meant it had copied everything and on a
308+
// slow one meant it had copied nothing; both make this test say something other than
309+
// what it claims.
310+
kill_child_at_failpoint("child_migrates_until_killed", &root, 10);
360311

361312
// Interrupted, which is two claims and not one: some chunks copied, and some not.
362313
// Only the upper bound was checked before, so a copier that did nothing at all passed

tests/migration_reclaims_disk.rs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -253,8 +253,12 @@ async fn retiring_the_legacy_environment_returns_its_bytes_to_the_filesystem() {
253253
// still holds it open keeps its blocks and shows in no directory, so measuring before
254254
// this point would be measuring the wrong thing.
255255
drop(store);
256-
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
257-
let free_at_end = free_space(&root);
256+
257+
// Polled rather than sampled once after a fixed pause. Not every filesystem updates
258+
// its accounting the instant a file goes: btrfs in particular defers it, and a single
259+
// reading taken too early says the space never came back when it is on its way. The
260+
// deadline is what makes this a test rather than a wait.
261+
let free_at_end = wait_for_space(&root, free_at_peak + environment_blocks / 2).await;
258262

259263
// Only the file store's copy should be left.
260264
let left_on_disk = allocated_bytes(&root);
@@ -297,6 +301,22 @@ async fn retiring_the_legacy_environment_returns_its_bytes_to_the_filesystem() {
297301
}
298302
}
299303

304+
/// Wait for the filesystem to report at least `wanted` bytes free, and return what it
305+
/// reports at the end.
306+
///
307+
/// Returns whatever it last saw when the deadline passes, so the caller's assertion is
308+
/// what fails rather than this helper, and the number in the failure is a real reading.
309+
async fn wait_for_space(path: &Path, wanted: u64) -> u64 {
310+
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(60);
311+
loop {
312+
let free = free_space(path);
313+
if free >= wanted || std::time::Instant::now() > deadline {
314+
return free;
315+
}
316+
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
317+
}
318+
}
319+
300320
/// Open the store again from scratch, which is what a restart does.
301321
async fn store_reopened(root: &Path) -> ChunkStore {
302322
ChunkStore::new(ChunkStoreConfig {

0 commit comments

Comments
 (0)