Skip to content

Commit

Permalink
Implementation of durable file rename
Browse files Browse the repository at this point in the history
  • Loading branch information
clkamp committed May 2, 2020
1 parent 08bf2bd commit d9f367f
Show file tree
Hide file tree
Showing 2 changed files with 37 additions and 0 deletions.
25 changes: 25 additions & 0 deletions unliftio/src/UnliftIO/IO/File.hs
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,14 @@ module UnliftIO.IO.File
, withBinaryFileDurable
, withBinaryFileDurableAtomic
, ensureFileDurable
, renameFileDurable
)
where

import Data.ByteString as B (ByteString, writeFile)
import Control.Monad.IO.Unlift
import UnliftIO.IO (Handle, IOMode(..), withBinaryFile)
import UnliftIO.Directory (renameFile)

#if WINDOWS

Expand All @@ -102,6 +104,8 @@ withBinaryFileDurable = withBinaryFile
withBinaryFileDurableAtomic = withBinaryFile
withBinaryFileAtomic = withBinaryFile

renameFileDurable = renameFile

#else

import qualified Data.ByteString as B (hPut)
Expand All @@ -119,8 +123,29 @@ writeBinaryFileAtomic fp bytes =
withBinaryFileDurable = Posix.withBinaryFileDurable
withBinaryFileDurableAtomic = Posix.withBinaryFileDurableAtomic
withBinaryFileAtomic = Posix.withBinaryFileAtomic

renameFileDurable = Posix.renameFileDurable
#endif

-- | When a file is renamed, it is necessary to execute @fsync()@ on the
-- directory that contains the file now and afterwards on the directory where
-- the file was before, so that the rename is durable.
--
-- Remark: This is also atomic if both locations of the file are on the same
-- filesystem. However, it could happen that the operation leads to data loss,
-- if a crash happens after the rename and before the first fsync finishes. This
-- is because on an async filesystem the write of the old directory might
-- already written to disk and the change on the new directory is not. It the
-- function call returns, the change is durable. Nevertheless, this will not
-- happen on filesystems using journaling, that is, allmost modern filesystems.
--
-- === Cross-Platform support
--
-- This function is a noop on Windows platforms.
--
renameFileDurable :: MonadIO m => FilePath -> FilePath -> m ()
-- Implementation is at the top of the module

-- | After a file is closed, this function opens it again and executes @fsync()@
-- internally on both the file and the directory that contains it. Note that this function
-- is intended to work around the non-durability of existing file APIs, as opposed to
Expand Down
12 changes: 12 additions & 0 deletions unliftio/src/UnliftIO/IO/File/Posix.hs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ module UnliftIO.IO.File.Posix
, withBinaryFileDurableAtomic
, withBinaryFileAtomic
, ensureFileDurable
, renameFileDurable
)
where

Expand Down Expand Up @@ -39,6 +40,7 @@ import System.IO.Error (ioeGetErrorType, isAlreadyExistsError,
import qualified System.Posix.Files as Posix
import System.Posix.Internals (CFilePath, c_close, c_safe_open, withFilePath)
import System.Posix.Types (CMode(..), Fd(..), FileMode)
import UnliftIO.Directory (renameFile)
import UnliftIO.Exception
import UnliftIO.IO
import UnliftIO.MVar
Expand Down Expand Up @@ -580,3 +582,13 @@ withBinaryFileAtomic filePath iomode action =
liftIO $ atomicTempFileRename Nothing mFileMode eTmpFile filePath
pure res


-- | See `renameFileDurable`
renameFileDurable ::
MonadIO m => FilePath -> FilePath -> m ()
renameFileDurable oldName newName =
liftIO $ withDirectory (takeDirectory oldName) $ \oldDirFd ->
withDirectory (takeDirectory newName) $ \newDirFd -> do
renameFile oldName newName
fsyncDirectoryFd "renameFileDurable" newDirFd
fsyncDirectoryFd "renameFileDurable" oldDirFd

0 comments on commit d9f367f

Please sign in to comment.