|
7 | 7 | // except according to those terms.
|
8 | 8 |
|
9 | 9 | mod big_fs;
|
10 |
| -#[expect(unused)] // TODO |
11 | 10 | mod dmsetup;
|
12 |
| -#[expect(unused)] // TODO |
13 | 11 | mod losetup;
|
14 | 12 |
|
15 | 13 | use anyhow::{bail, Context, Result};
|
16 | 14 | use clap::{Parser, Subcommand};
|
| 15 | +use dmsetup::{DmDevice, DmFlakey}; |
| 16 | +use losetup::LoopDevice; |
17 | 17 | use nix::fcntl::{self, FallocateFlags};
|
18 | 18 | use std::fs::{self, OpenOptions};
|
19 | 19 | use std::os::fd::AsRawFd;
|
@@ -258,6 +258,103 @@ impl DiskParams {
|
258 | 258 | Ok(())
|
259 | 259 | }
|
260 | 260 |
|
| 261 | + /// Create a filesystem that was not unmounted cleanly. The root |
| 262 | + /// directory contains a number of subdirectories that are only in |
| 263 | + /// the journal. |
| 264 | + fn create_with_journal(&self) -> Result<()> { |
| 265 | + // Multiple attempts may be needed to get a filesystem with the |
| 266 | + // desired journal state. |
| 267 | + for i in 1..=10 { |
| 268 | + println!("creating filesystem with journal, attempt {i}"); |
| 269 | + |
| 270 | + self.create()?; |
| 271 | + self.make_filesystem_need_recovery()?; |
| 272 | + |
| 273 | + // Verify that the journal contains at least one block. The |
| 274 | + // full output should look something like this: |
| 275 | + // |
| 276 | + // ``` |
| 277 | + // Journal starts at block 1, transaction 2 |
| 278 | + // Found expected sequence 2, type 1 (descriptor block) at block 1 |
| 279 | + // [...] |
| 280 | + // Found expected sequence 2, type 1 (descriptor block) at block 1303 |
| 281 | + // Found expected sequence 2, type 2 (commit block) at block 1311 |
| 282 | + // ``` |
| 283 | + let logdump = self.run_debugfs("logdump")?; |
| 284 | + let logdump = str::from_utf8(&logdump)?; |
| 285 | + if logdump.contains("Found expected sequence") { |
| 286 | + return Ok(()); |
| 287 | + } |
| 288 | + } |
| 289 | + |
| 290 | + bail!("failed to create filesystem"); |
| 291 | + } |
| 292 | + |
| 293 | + /// Modify the filesystem so that some data is written to the |
| 294 | + /// journal, but not yet flushed to the main filesystem. |
| 295 | + /// |
| 296 | + /// This uses losetup and dmsetup to simulate a power failure after |
| 297 | + /// data is written to the journal. |
| 298 | + fn make_filesystem_need_recovery(&self) -> Result<()> { |
| 299 | + // Get the number of sectors in the filesystem. |
| 300 | + let num_sectors = { |
| 301 | + let sector_size = 512; |
| 302 | + u64::from(self.size_in_kilobytes) * 1024 / sector_size |
| 303 | + }; |
| 304 | + |
| 305 | + // Use losetup to create a block device from the file containing |
| 306 | + // the filesystem. |
| 307 | + let loop_dev = LoopDevice::new(&self.path)?; |
| 308 | + |
| 309 | + // Create a device-mapper device using the dm-flakey |
| 310 | + // target. This target allows us to cut off writes at a certain |
| 311 | + // point, simulating a power failure. In its initial state |
| 312 | + // however, this acts as a simple pass-through device. |
| 313 | + let table = DmFlakey { |
| 314 | + start_sector: 0, |
| 315 | + num_sectors, |
| 316 | + block_dev: loop_dev.path().to_owned(), |
| 317 | + offset: 0, |
| 318 | + up_interval: 100, |
| 319 | + down_interval: 0, |
| 320 | + features: Vec::new(), |
| 321 | + }; |
| 322 | + let dm_device = DmDevice::create("flakey-dev", &table.as_string())?; |
| 323 | + |
| 324 | + // Mount the filesystem from the flakey device, and create a |
| 325 | + // bunch of directories. |
| 326 | + let mount = Mount::new(&dm_device.path(), ReadOnly(false))?; |
| 327 | + for i in 0..1000 { |
| 328 | + fs::create_dir(mount.path().join(format!("dir{i}")))?; |
| 329 | + } |
| 330 | + |
| 331 | + // At this point, the directory blocks have likely been written |
| 332 | + // to the journal, but not yet written to their final locations |
| 333 | + // on disk. (This is somewhat timing dependant however, so this |
| 334 | + // whole function is called in a loop until the desired |
| 335 | + // conditions are met.) |
| 336 | + |
| 337 | + // Change the device configuration so that all writes are |
| 338 | + // dropped. When the filesystem is unmounted below, any data not |
| 339 | + // already written will be lost. |
| 340 | + dm_device.suspend()?; |
| 341 | + let drop_writes_table = DmFlakey { |
| 342 | + up_interval: 0, |
| 343 | + down_interval: 100, |
| 344 | + features: vec!["drop_writes"], |
| 345 | + ..table |
| 346 | + }; |
| 347 | + dm_device.load_table(&drop_writes_table.as_string())?; |
| 348 | + dm_device.resume()?; |
| 349 | + |
| 350 | + // Clean up. |
| 351 | + mount.unmount()?; |
| 352 | + dm_device.remove()?; |
| 353 | + loop_dev.detach()?; |
| 354 | + |
| 355 | + Ok(()) |
| 356 | + } |
| 357 | + |
261 | 358 | /// Check some properties of the filesystem.
|
262 | 359 | fn check(&self) -> Result<()> {
|
263 | 360 | self.check_dir_htree_depth("/medium_dir", 0)?;
|
@@ -412,6 +509,16 @@ fn create_test_data() -> Result<()> {
|
412 | 509 | disk.fill_ext2()?;
|
413 | 510 | zstd_compress(&disk.path)?;
|
414 | 511 |
|
| 512 | + let path = dir.join("test_disk_4k_block_journal.bin"); |
| 513 | + let disk = DiskParams { |
| 514 | + path: path.to_owned(), |
| 515 | + size_in_kilobytes: 1024 * 64, |
| 516 | + fs_type: FsType::Ext4, |
| 517 | + block_size: 4096, |
| 518 | + }; |
| 519 | + disk.create_with_journal()?; |
| 520 | + zstd_compress(&disk.path)?; |
| 521 | + |
415 | 522 | Ok(())
|
416 | 523 | }
|
417 | 524 |
|
|
0 commit comments