Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft: port durable file, add incremental tests #42

Closed
wants to merge 1 commit into from
Closed
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
62 changes: 62 additions & 0 deletions exercise-book/src/durable-file.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Durable File

In this exercise, we will to implement `std` library traits for a `DurableFile`, a type of file which tracks it's syncs and writes.

## After completing this exercise you will be able to

* implement the `Drop` and `Write` traits.
* use the “Drop guard” pattern.
* test panicking APIs.

## Prerequisites

For completing this exercise you need to have:

* an understanding of what `self`, `&self` and `&mut self` means
* knowledge of how to write and wrap a `struct`
* basic knowledge of how to write a `trait`
* the basics on using `Return` and `?`

`std::fs::File` type has a [`sync_all`](https://doc.rust-lang.org/stable/std/fs/struct.File.html#method.sync_all) method which ensures that all data is written to disk. This method is not called by default: syncing is slow and the OS has good heuristics for optimistically delaying syncing.

In this assignment, we’ll implement a `DurableFile` wrapper for `File`, which helps to ensure that applications calls [`sync_all`](https://doc.rust-lang.org/stable/std/fs/struct.File.html#method.sync_all). Specifically, `DurableFile` tracks syncs and writes. If a `DurableFile` is dropped with an outstanding write, its `Drop` panics. Not calling [`sync_all`](https://doc.rust-lang.org/stable/std/fs/struct.File.html#method.sync_all) before disposing the file is considered a bug in this case. A `panic!` helps to elevate silent potential data loss into a loud failure.

## Task

1. Create a new libary crate with `cargo new --lib durable-file`
2. Implement the `DurableFile` data structure and its constructor

```rust
use std::fs::File;

struct DurableFile {
file: File,
needs_sync: bool,
}

impl DurableFile {
pub fn new(file: std::fs::File) -> DurableFile {
...
}
}
```

Optional: implement `From<std::fs::File>` for `DurableFile`

3. Implement the [std::io::Write](https://doc.rust-lang.org/stable/std/io/trait.Write.html) trait for `DurableFile`. Use [sync_all](https://doc.rust-lang.org/stable/std/fs/struct.File.html#method.sync_all) in the implementation of the flush method. All write operations should set the `needs_sync` flag, the flush method should clear it.

4. Implement the [std::ops::Drop](https://doc.rust-lang.org/std/ops/trait.Drop.html) trait for `DurableFile` so that it panics if the file is not flushed. What is the right behavior for `Drop`? Are there any edge cases to worry about?

5. Add an inherent `close` method for `DurableFile`, to explicitly sync, dispose the file, and handle potential errors. What is the appropriate signature (type of self and the return type) for close?

6. Write a couple of simple smoke tests. You might find the [tempdir](https://docs.rs/tempdir/0.3.7/tempdir/) crate and [#[should_panic]](https://doc.rust-lang.org/reference/attributes/testing.html#the-should_panic-attribute) attribute useful!

```rust
#[test]
#[should_panic(expected = "not yet implemented")]
fn smoke_test() {
let dir = tempdir::TempDir::new("tests").unwrap();
let file = std::fs::File::create(dir.path().join("foo.txt")).unwrap();
todo!()
}
```
1 change: 1 addition & 0 deletions exercise-solutions/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ members = [
"shapes-part-1",
"shapes-part-2",
"shapes-part-3",
"durable-file/*",
]
8 changes: 8 additions & 0 deletions exercise-solutions/durable-file/step2/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[package]
name = "step2"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
14 changes: 14 additions & 0 deletions exercise-solutions/durable-file/step2/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
use std::fs::File;
struct DurableFile {
file: File,
needs_sync: bool,
}

impl DurableFile {
pub fn new(file: std::fs::File) -> DurableFile {
DurableFile {
file,
needs_sync: false,
}
}
}
8 changes: 8 additions & 0 deletions exercise-solutions/durable-file/step3/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[package]
name = "step3"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
30 changes: 30 additions & 0 deletions exercise-solutions/durable-file/step3/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use std::fs::File;
use std::io::Write;

struct DurableFile {
file: File,
needs_sync: bool,
}

impl DurableFile {
pub fn new(file: std::fs::File) -> DurableFile {
DurableFile {
file,
needs_sync: false,
}
}
}

impl Write for DurableFile {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let written_bytes = self.file.write(buf)?;
self.needs_sync = true;
Ok(written_bytes)
}

fn flush(&mut self) -> std::io::Result<()> {
self.file.sync_all()?;
self.needs_sync = false;
Ok(())
}
}
8 changes: 8 additions & 0 deletions exercise-solutions/durable-file/step4/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[package]
name = "step4"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
39 changes: 39 additions & 0 deletions exercise-solutions/durable-file/step4/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
use std::fs::File;
use std::io::Write;

struct DurableFile {
file: File,
needs_sync: bool,
}

impl DurableFile {
pub fn new(file: File) -> DurableFile {
DurableFile {
file,
needs_sync: false,
}
}
}

impl Write for DurableFile {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let written_bytes = self.file.write(buf)?;
self.needs_sync = true;
Ok(written_bytes)
}

fn flush(&mut self) -> std::io::Result<()> {
self.file.sync_all()?;
self.needs_sync = false;
Ok(())
}
}

impl Drop for DurableFile {
fn drop(&mut self) {
// Any edge cases?
if self.needs_sync {
panic!("You forgot to sync!");
}
}
}
8 changes: 8 additions & 0 deletions exercise-solutions/durable-file/step5/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[package]
name = "step5"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
56 changes: 56 additions & 0 deletions exercise-solutions/durable-file/step5/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
use std::fs::File;
use std::io::{Result, Write};

#[derive(Debug)]
pub struct DurableFile {
file: File,
needs_sync: bool,
}

#[derive(Debug)]
pub struct CloseError {
file: DurableFile,
error: std::io::Error,
}

impl Write for DurableFile {
fn write(&mut self, buf: &[u8]) -> Result<usize> {
let amt = self.file.write(buf)?;
self.needs_sync = true;
Ok(amt)
}
fn flush(&mut self) -> Result<()> {

self.file.sync_all()?;
self.needs_sync = false;
Ok(())
}
}

impl Drop for DurableFile {
fn drop(&mut self) {
// Any edge cases?
if self.needs_sync {
panic!("You forgot to sync!");
}
}
}

impl DurableFile {
pub fn new(file: File) -> DurableFile {
DurableFile {
file,
needs_sync: false,
}
}

pub fn close(mut self) -> std::result::Result<(), CloseError> {
match self.flush() {
Ok(()) => Ok(()),
Err(e) => Err(CloseError {
file: self,
error: e,
}),
}
}
}
9 changes: 9 additions & 0 deletions exercise-solutions/durable-file/step6/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[package]
name = "step6"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
tempdir = "0.3.7"
104 changes: 104 additions & 0 deletions exercise-solutions/durable-file/step6/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
use std::fs::File;
use std::io::{Result, Write};

#[derive(Debug)]
pub struct DurableFile {
file: File,
needs_sync: bool,
}

#[derive(Debug)]
pub struct CloseError {
file: DurableFile,
error: std::io::Error,
}

impl Write for DurableFile {
fn write(&mut self, buf: &[u8]) -> Result<usize> {
let amt = self.file.write(buf)?;
self.needs_sync = true;
Ok(amt)
}
fn flush(&mut self) -> Result<()> {

self.file.sync_all()?;
self.needs_sync = false;
Ok(())
}
}

impl Drop for DurableFile {
fn drop(&mut self) {
// Any edge cases?
if self.needs_sync {
panic!("You forgot to sync!");
}
}
}

impl DurableFile {
pub fn new(file: File) -> DurableFile {
DurableFile {
file,
needs_sync: false,
}
}

pub fn close(mut self) -> std::result::Result<(), CloseError> {
match self.flush() {
Ok(()) => Ok(()),
Err(e) => Err(CloseError {
file: self,
error: e,
}),
}
}
}

#[cfg(test)]
mod test {
use super::*;
use tempdir;

#[test]
fn create_no_write() {
let dir = tempdir::TempDir::new("tests").unwrap();
let file = std::fs::File::create(dir.path().join("foo.txt")).unwrap();
let _durable = DurableFile::new(file);

// No writes, let the file drop naturally.
}

#[test]
#[should_panic(expected = "You forgot to sync!")]
fn create_write_panics() {
let dir = tempdir::TempDir::new("tests").unwrap();
let file = std::fs::File::create(dir.path().join("foo.txt")).unwrap();
let mut durable = DurableFile::new(file);
durable.write_all(b"Hello, world!").unwrap();
// Should panic, we forgot to sync!
}

#[test]
fn create_write_sync() {
let dir = tempdir::TempDir::new("tests").unwrap();
let file = std::fs::File::create(dir.path().join("foo.txt")).unwrap();
let mut durable = DurableFile::new(file);
durable.write_all(b"Hello, world!").unwrap();
durable.flush().unwrap();

// We now drop, shouldn't panic because we flush'd.
}

#[test]
fn create_write_close() {
let dir = tempdir::TempDir::new("tests").unwrap();
let file = std::fs::File::create(dir.path().join("foo.txt")).unwrap();
let mut durable = DurableFile::new(file);
durable.write_all(b"Hello, world!").unwrap();

// This will close and drop the durable file, it
// shouldn't panic because we closed it manually.
durable.close().unwrap();
}
}
Loading