atomic-file-ops- Functions to atomically write to files

Safe HaskellSafe



Often there's no clear way to preform atomic file system writes. The usual way around this is to lock the file exclusively before writing. But file system locks are generally advisory, so if an independent process which does not attempt to gain a lock attempts to read the file, it may read an intermediate inconsistent state.

This package contains some functions for avoid this state of affairs. It does this by creating a temp file, performing the writes to the temp file, and then moving the temp file over the existing file. Generally in filesystems, moves are atomic, so using this scheme a reading process should only see the old version of the file or the new version of the file, and not an intermediate state.

One may be concerned if a file is being read as it is replaced that the start of the old file is read and the end of the new file is read. However, this is not likely to be the case, as when a file is opened, that same file is kept open and readable even if it is say, deleted. As these functions do not modify the file, but instead replace it, opening the file before it is atomically modified will just result in old data being read, not an intermediate state.

The functions in this package reference classes from the package "io-string-like", which means one can write Strings, ByteStrings or even Texts to the file using the same function.



atomicReplaceFile :: CanPutStr contents => Maybe AtomicTempOptions -> FilePath -> contents -> IO () Source #

atomicReplaceFile options @file@ contents

atomically replaces file's with contents.

options should be Just some AtomicTempOptions, which detail where the temporary file is placed, or Nothing for the default of creating a temp file in the existing directory.

If the target file must exist.

As moving is atomic on filesystems generally, the target file should only ever be in two states, either what it was like before it was written or what it was like after.

atomicModifyFile :: (CanGetContents contents, CanPutStr contents) => Maybe AtomicTempOptions -> FilePath -> (contents -> IO (IO a, Maybe contents)) -> IO a Source #

Atomically modifies a file, by reading it and applying a function to it's contents. This is different to atomicReplaceFile, which is best illistrated by an example.

Lets say a file contains a single integer, say "1". An IO statement is writen that reads the file, adds one to the number, and then calls atomicReplaceFile to write it back out.

Running this should result the file being replaced with the contents "2".

Now lets say we run this from two different processes.

If the processes run sequentially, we will read "1", then write "2", then read "2", then write "3".

But if the processes run overlapping, the first process may read "1", and before it writes "2", the second process may read "1". The first process will then write "2" but then also the second process will write "2".

We've clobbered a write, and the behavour is inconsistent based on timing.

This is where atomicModifyFile comes in.

atomicModifyFile gets passed a function, and will lock the file, to ensure two processes can not run on it at the same time.

If the two processes in the above case both used atomicModifyFile, then the result would be "3" in call cases.

But it's worth mentioning that locking is advisory in many file systems. If you don't consistently use atomicModifyFile with all your writers you may get the timing issues discussed previously. Indeed if you mix atomicReplaceFile with atomicModifyFile you may still get timing issues.

One may ask, if you're going to lock the files, why bother with temp files at all?

Well, you wouldn't need to if all your processes asked for locks. But atomicModifyFile achieves two things:

  1. From writers that use atomicModifyFile: Writes occur sequentially
  2. From all readers (including those who do not attempt to gain a lock): Will only ever see a consistent state.

Just using locks and not an "atomic style" temp file and replace does not achieve point 2.

The arguments to atomicModifyFile are similar to atomicReplaceFile, except for the third argument, which details how to replace the contents of the file.

This replacement function argument is quite complex. Lets say we have the following:

f x = duringAction >> pure (afterAction, whatToWrite)
atomicModifyFile options file f

The whole result of f must be an IO action. Often this will be just pure something, but in the above case I've included an actual IO action: duringAction.

The following sequences of events will occur:

  1. The file will be exclusively locked.
  2. The entire contents of the file will be read.
  3. The IO duringAction will be executed, as f is run against the contents of the file.
  4. If whatToWrite is Just contents, contents will be written to to a temp file and then moved over the target file. But if whatToWrite is Nothing, do nothing.
  5. The file will be unlocked. In theory this is unnecessary as we're now only locking the now dead file we just replaced. However that depends on whether the filesystem locks based on filename or filehandle. Anyway lets we play it safe and unlock, which we would need to do if we did nothing in part 4 anyway.
  6. afterAction will be executed as the IO return value of atomicModifyFile.

So this function has a lot of flexibility, however, in many simple use cases it will look something like this:

atomicModifyFile options file (\contents -> pure (pure (), Just (f contents)))

Which just says apply f to the contents of the contents of the file and do nothing else.

Like atomicReplaceFile, atomicModifyFile will fail if the file does not exist.

data AtomicTempOptions Source #

This data type is passed to atomicReplaceFile and atomicModifyFile to change the behaviour of where the temp file is created.

The following will be called inside the atomic write functions:

openTempFile tempFileDir tempFileTemplate

See the documentation for openTempFile to see how these arguments are used.