5

I wrote a bot that performs some network queries once every so often and dumps the latest state into a file stored on disk. This is my code:

let log_file = OpenOptions::new()
    .read(true)
    .write(true)
    .create(true)
    .open(&log_file_name)
    .unwrap();
serde_json::to_writer(
    log_file,
    &State {
        foo: self.foo,
        bar: self.bar
    },
)
.unwrap();

The issue is that, occasionally, the size of the newest state json will be smaller than the size of the previous state JSON. This leaves that portion of the previous state JSON that is longer than the new state JSON to remain in the log file:

{"foo": 1,"bar": 2}"baz": 3}

In the example above, "baz": 3} is the leftover data from the previous state JSON.

The question is: how can I rewrite the code so that I first flush all the contents of the file and then write the new state json?

I know deleting the file is a solution but I would prefer not to do that.

Herohtar
  • 5,347
  • 4
  • 31
  • 41
Paul Razvan Berg
  • 16,949
  • 9
  • 76
  • 114
  • Is there a concrete reason why deleting the file is not desirable? A common issue with state files is that they need to be replaced atomically. They need to contain either the old contents or the new contents, never something in between. The solutions using truncate might leave an empty file for example. Is that a problem? – Jonathan Giddy Jan 19 '21 at 12:57
  • The main reason was my being curious about how to truncate the contents of the file. But you made a good point: I should refactor the code to delete the file, since that would give better guarantees. – Paul Razvan Berg Jan 19 '21 at 13:16

2 Answers2

12

You additionally need the truncate flag, which results in the file being truncated to length 0 if it already exists. Additionally, you should remove the read flag, since you are not actually reading from the log file.

Since write, create, truncate is a very common combination of flags, there is a shortcut: You don't need to use the more low-level OpenOptions interface, and can instead use File::create():

let log_file = File::create(&log_file_name).unwrap();

The implementation of File::create() simply spells out the long version using OpenOptions:

pub fn create<P: AsRef<Path>>(path: P) -> io::Result<File> {
    OpenOptions::new().write(true).create(true).truncate(true).open(path.as_ref())
}
Sven Marnach
  • 574,206
  • 118
  • 941
  • 841
4

Use .truncate(). Example:

let log_file = OpenOptions::new()
    .read(true)
    .write(true)
    .create(true)
    .truncate(true) // added here
    .open(&log_file_name)
    .unwrap();
pretzelhammer
  • 13,874
  • 15
  • 47
  • 98