Skip to content

Commit a6e28c7

Browse files
Add fsync and hard link support, start using it in snapshots
* Add two new `HasBlockIO` primitives for `fsync`, one for synchronising files, one for directories. These are separate, since we can not synchronise directories on `Windows`. * Add one new `HasBlockIO` primitive for creating hard links. * Add `IO` specialisations to `blockio-api`. * Use the new `HasBlockIO` primitives in the snapshot implementation. Note that when we create a snapshot, that we make *both* the hard links and their parent directory (the named snapshot directory) durable. Co-authored-by: Recursion Ninja <[email protected]>
1 parent 82df7e4 commit a6e28c7

File tree

13 files changed

+349
-65
lines changed

13 files changed

+349
-65
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ key-value store.
2626

2727
## System requirements
2828

29-
This library only supports 64-bit, little-endian systems.
29+
This library only supports 64-bit, little-endian systems. On Windows, the
30+
library only works probably on drives with NTFS.
3031

3132
Provide the -threaded flag to executables, test suites and benchmark suites if
3233
you use this library on Linux systems.

blockio-api/src-linux/System/FS/BlockIO/Async.hs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,13 @@ asyncHasBlockIO ::
3232
-> (Handle HandleIO -> FileOffset -> FileOffset -> API.Advice -> IO ())
3333
-> (Handle HandleIO -> FileOffset -> FileOffset -> IO ())
3434
-> (FsPath -> LockMode -> IO (Maybe (API.LockFileHandle IO)))
35+
-> (Handle HandleIO -> IO ())
36+
-> (FsPath -> IO ())
37+
-> (FsPath -> FsPath -> IO ())
3538
-> HasFS IO HandleIO
3639
-> API.IOCtxParams
3740
-> IO (API.HasBlockIO IO HandleIO)
38-
asyncHasBlockIO hSetNoCache hAdvise hAllocate tryLockFile hasFS ctxParams = do
41+
asyncHasBlockIO hSetNoCache hAdvise hAllocate tryLockFile hSynchronise synchroniseDirectory createHardLink hasFS ctxParams = do
3942
ctx <- I.initIOCtx (ctxParamsConv ctxParams)
4043
pure $ API.HasBlockIO {
4144
API.close = I.closeIOCtx ctx
@@ -44,6 +47,9 @@ asyncHasBlockIO hSetNoCache hAdvise hAllocate tryLockFile hasFS ctxParams = do
4447
, API.hAdvise
4548
, API.hAllocate
4649
, API.tryLockFile
50+
, API.hSynchronise
51+
, API.synchroniseDirectory
52+
, API.createHardLink
4753
}
4854

4955
ctxParamsConv :: API.IOCtxParams -> I.IOCtxParams
@@ -110,11 +116,18 @@ ioopConv (IOOpWrite h off buf bufOff c) = handleFd h >>= \fd ->
110116
--
111117
-- TODO: if the handle were to have a reader/writer lock, then we could take the
112118
-- reader lock in 'submitIO'. However, the current implementation of 'Handle'
113-
-- only allows mutally exclusive access to the underlying file descriptor, so it
119+
-- only allows mutually exclusive access to the underlying file descriptor, so it
114120
-- would require a change in @fs-api@. See [fs-sim#49].
115121
handleFd :: Handle HandleIO -> IO Fd
116122
handleFd h = withOpenHandle "submitIO" (handleRaw h) pure
117123

124+
{-# SPECIALISE hzipWithM ::
125+
(VUM.Unbox b, VUM.Unbox c)
126+
=> (a -> b -> IO c)
127+
-> V.Vector a
128+
-> VU.Vector b
129+
-> IO (VU.Vector c)
130+
#-}
118131
-- | Heterogeneous blend of `V.zipWithM` and `VU.zipWithM`
119132
--
120133
-- The @vector@ package does not provide functions that take distinct vector

blockio-api/src-linux/System/FS/BlockIO/Internal.hs

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,17 @@ module System.FS.BlockIO.Internal (
44
ioHasBlockIO
55
) where
66

7-
import System.FS.API (Handle (..), HasFS)
7+
import qualified System.FS.API as FS
8+
import System.FS.API (FsPath, Handle (..), HasFS)
89
import qualified System.FS.BlockIO.API as FS
910
import System.FS.BlockIO.API (Advice (..), FileOffset, HasBlockIO,
1011
IOCtxParams)
1112
import System.FS.IO (HandleIO)
1213
import qualified System.FS.IO.Handle as FS
1314
import qualified System.Posix.Fcntl as Fcntl
1415
import qualified System.Posix.Fcntl.NoCache as Unix
16+
import qualified System.Posix.Files as Unix
17+
import qualified System.Posix.Unistd as Unix
1518

1619
#if SERIALBLOCKIO
1720
import qualified System.FS.BlockIO.Serial as Serial
@@ -24,9 +27,28 @@ ioHasBlockIO ::
2427
-> IOCtxParams
2528
-> IO (HasBlockIO IO HandleIO)
2629
#if SERIALBLOCKIO
27-
ioHasBlockIO hfs _params = Serial.serialHasBlockIO hSetNoCache hAdvise hAllocate (FS.tryLockFileIO hfs) hfs
30+
ioHasBlockIO hfs _params =
31+
Serial.serialHasBlockIO
32+
hSetNoCache
33+
hAdvise
34+
hAllocate
35+
(FS.tryLockFileIO hfs)
36+
hSynchronise
37+
(synchroniseDirectory hfs)
38+
(FS.createHardLinkIO hfs Unix.createLink)
39+
hfs
2840
#else
29-
ioHasBlockIO hfs params = Async.asyncHasBlockIO hSetNoCache hAdvise hAllocate (FS.tryLockFileIO hfs) hfs params
41+
ioHasBlockIO hfs params =
42+
Async.asyncHasBlockIO
43+
hSetNoCache
44+
hAdvise
45+
hAllocate
46+
(FS.tryLockFileIO hfs)
47+
hSynchronise
48+
(synchroniseDirectory hfs)
49+
(FS.createHardLinkIO hfs Unix.createLink)
50+
hfs
51+
params
3052
#endif
3153

3254
hSetNoCache :: Handle HandleIO -> Bool -> IO ()
@@ -48,3 +70,11 @@ hAdvise h off len advice = FS.withOpenHandle "hAdvise" (handleRaw h) $ \fd ->
4870
hAllocate :: Handle HandleIO -> FileOffset -> FileOffset -> IO ()
4971
hAllocate h off len = FS.withOpenHandle "hAllocate" (handleRaw h) $ \fd ->
5072
Fcntl.fileAllocate fd off len
73+
74+
hSynchronise :: Handle HandleIO -> IO ()
75+
hSynchronise h = FS.withOpenHandle "hSynchronise" (handleRaw h) $ \fd ->
76+
Unix.fileSynchronise fd
77+
78+
synchroniseDirectory :: HasFS IO HandleIO -> FsPath -> IO ()
79+
synchroniseDirectory hfs path =
80+
FS.withFile hfs path FS.ReadMode $ hSynchronise

blockio-api/src-macos/System/FS/BlockIO/Internal.hs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,17 @@ module System.FS.BlockIO.Internal (
22
ioHasBlockIO
33
) where
44

5-
import System.FS.API (Handle (..), HasFS)
5+
import qualified System.FS.API as FS
6+
import System.FS.API (FsPath, Handle (..), HasFS)
67
import qualified System.FS.BlockIO.API as FS
78
import System.FS.BlockIO.API (Advice (..), FileOffset, HasBlockIO,
89
IOCtxParams)
910
import qualified System.FS.BlockIO.Serial as Serial
1011
import System.FS.IO (HandleIO)
1112
import qualified System.FS.IO.Handle as FS
1213
import qualified System.Posix.Fcntl.NoCache as Unix
14+
import qualified System.Posix.Files as Unix
15+
import qualified System.Posix.Unistd as Unix
1316

1417
-- | For now we use the portable serial implementation of HasBlockIO. If you
1518
-- want to provide a proper async I/O implementation for OSX, then this is where
@@ -20,7 +23,16 @@ ioHasBlockIO ::
2023
HasFS IO HandleIO
2124
-> IOCtxParams
2225
-> IO (HasBlockIO IO HandleIO)
23-
ioHasBlockIO hfs _params = Serial.serialHasBlockIO hSetNoCache hAdvise hAllocate (FS.tryLockFileIO hfs) hfs
26+
ioHasBlockIO hfs _params =
27+
Serial.serialHasBlockIO
28+
hSetNoCache
29+
hAdvise
30+
hAllocate
31+
(FS.tryLockFileIO hfs)
32+
hSynchronise
33+
(synchroniseDirectory hfs)
34+
(FS.createHardLinkIO hfs Unix.createLink)
35+
hfs
2436

2537
hSetNoCache :: Handle HandleIO -> Bool -> IO ()
2638
hSetNoCache h b =
@@ -34,3 +46,11 @@ hAdvise _h _off _len _advice = pure ()
3446

3547
hAllocate :: Handle HandleIO -> FileOffset -> FileOffset -> IO ()
3648
hAllocate _h _off _len = pure ()
49+
50+
hSynchronise :: Handle HandleIO -> IO ()
51+
hSynchronise h = FS.withOpenHandle "hSynchronise" (handleRaw h) $ \fd ->
52+
Unix.fileSynchronise fd
53+
54+
synchroniseDirectory :: HasFS IO HandleIO -> FsPath -> IO ()
55+
synchroniseDirectory hfs path =
56+
FS.withFile hfs path FS.ReadMode $ hSynchronise

blockio-api/src-windows/System/FS/BlockIO/Internal.hs

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,20 @@ module System.FS.BlockIO.Internal (
22
ioHasBlockIO
33
) where
44

5-
import System.FS.API (Handle (..), HasFS)
5+
import Control.Exception (throwIO)
6+
import Control.Monad (unless)
7+
import qualified System.FS.API as FS
8+
import System.FS.API (FsPath, Handle (..), HasFS)
69
import qualified System.FS.BlockIO.API as FS
710
import System.FS.BlockIO.API (Advice (..), FileOffset, HasBlockIO,
811
IOCtxParams)
912
import qualified System.FS.BlockIO.Serial as Serial
1013
import System.FS.IO (HandleIO)
14+
import qualified System.FS.IO.Handle as FS
15+
import System.IO.Error (doesNotExistErrorType, ioeSetErrorString,
16+
mkIOError)
17+
import qualified System.Win32.File as Windows
18+
import qualified System.Win32.HardLink as Windows
1119

1220
-- | For now we use the portable serial implementation of HasBlockIO. If you
1321
-- want to provide a proper async I/O implementation for Windows, then this is
@@ -18,7 +26,16 @@ ioHasBlockIO ::
1826
HasFS IO HandleIO
1927
-> IOCtxParams
2028
-> IO (HasBlockIO IO HandleIO)
21-
ioHasBlockIO hfs _params = Serial.serialHasBlockIO hSetNoCache hAdvise hAllocate (FS.tryLockFileIO hfs) hfs
29+
ioHasBlockIO hfs _params =
30+
Serial.serialHasBlockIO
31+
hSetNoCache
32+
hAdvise
33+
hAllocate
34+
(FS.tryLockFileIO hfs)
35+
hSynchronise
36+
(synchroniseDirectory hfs)
37+
(FS.createHardLinkIO hfs Windows.createHardLink)
38+
hfs
2239

2340
hSetNoCache :: Handle HandleIO -> Bool -> IO ()
2441
hSetNoCache _h _b = pure ()
@@ -28,3 +45,18 @@ hAdvise _h _off _len _advice = pure ()
2845

2946
hAllocate :: Handle HandleIO -> FileOffset -> FileOffset -> IO ()
3047
hAllocate _h _off _len = pure ()
48+
49+
hSynchronise :: Handle HandleIO -> IO ()
50+
hSynchronise h = FS.withOpenHandle "hAdvise" (handleRaw h) $ \fd ->
51+
Windows.flushFileBuffers fd
52+
53+
synchroniseDirectory :: HasFS IO HandleIO -> FsPath -> IO ()
54+
synchroniseDirectory hfs path = do
55+
b <- FS.doesDirectoryExist hfs path
56+
unless b $
57+
throwIO $ FS.ioToFsError (FS.mkFsErrorPath hfs (FS.mkFsPath [])) ioerr
58+
where
59+
ioerr =
60+
ioeSetErrorString
61+
(mkIOError doesNotExistErrorType "synchroniseDirectory" Nothing Nothing)
62+
("synchroniseDirectory: directory does not exist")

blockio-api/src/System/FS/BlockIO/API.hs

Lines changed: 88 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
{-# LANGUAGE UnboxedTuples #-}
99

1010
module System.FS.BlockIO.API (
11+
-- * HasBlockIO
1112
HasBlockIO (..)
1213
, IOCtxParams (..)
1314
, defaultIOCtxParams
@@ -19,15 +20,19 @@ module System.FS.BlockIO.API (
1920
, ioopBufferOffset
2021
, ioopByteCount
2122
, IOResult (..)
22-
-- * Advice
23+
-- ** Advice
2324
, Advice (..)
2425
, hAdviseAll
2526
, hDropCacheAll
26-
-- * File locks
27+
-- ** File locks
2728
, GHC.LockMode (..)
2829
, GHC.FileLockingNotSupported (..)
2930
, LockFileHandle (..)
31+
-- ** Storage synchronisation
32+
, synchroniseFile
33+
-- * Defaults for the real file system
3034
, tryLockFileIO
35+
, createHardLinkIO
3136
-- * Re-exports
3237
, ByteCount
3338
, FileOffset
@@ -52,7 +57,8 @@ import System.FS.API (BufferOffset, FsError (..), FsPath, Handle (..),
5257
HasFS, SomeHasFS (..))
5358
import System.FS.IO (HandleIO)
5459
import qualified System.IO as GHC
55-
import System.IO.Error (ioeSetErrorString, mkIOError)
60+
import System.IO.Error (doesNotExistErrorType, ioeSetErrorString,
61+
mkIOError)
5662
import System.Posix.Types (ByteCount, FileOffset)
5763

5864
-- | Abstract interface for submitting large batches of I\/O operations.
@@ -125,12 +131,42 @@ data HasBlockIO m h = HasBlockIO {
125131
-- limited scope. That is, it has to fit the style of @withHandleToHANDLE ::
126132
-- Handle -> (HANDLE -> IO a) -> IO a@ from the @Win32@ package.
127133
, tryLockFile :: FsPath -> GHC.LockMode -> m (Maybe (LockFileHandle m))
134+
135+
-- | Synchronise file contents with the storage device.
136+
--
137+
-- Ensure that all change to the file handle's contents which exist only in
138+
-- memory (as buffered system cache pages) are transfered/flushed to disk.
139+
-- This will also update the file handle's associated metadata.
140+
--
141+
-- This uses different system calls on different distributions.
142+
-- * [Linux]: @fsync(2)@
143+
-- * [MacOS]: @fsync(2)@
144+
-- * [Windows]: @flushFileBuffers@
145+
, hSynchronise :: Handle h -> m ()
146+
147+
-- | Synchronise a directory with the storage device.
148+
--
149+
-- This uses different system calls on different distributions.
150+
-- * [Linux]: @fsync(2)@
151+
-- * [MacOS]: @fsync(2)@
152+
-- * [Windows]: no-op
153+
, synchroniseDirectory :: FsPath -> m ()
154+
155+
-- | Create a hard link for an existing file at the source path and a new
156+
-- file at the target path.
157+
--
158+
-- This uses different system calls on different distributions.
159+
-- * [Linux]: @link@
160+
-- * [MacOS]: @link@
161+
-- * [Windows]: @CreateHardLinkW@
162+
, createHardLink :: FsPath -> FsPath -> m ()
128163
}
129164

130165
instance NFData (HasBlockIO m h) where
131-
rnf (HasBlockIO a b c d e f) =
166+
rnf (HasBlockIO a b c d e f g h i) =
132167
rwhnf a `seq` rwhnf b `seq` rnf c `seq`
133-
rwhnf d `seq` rwhnf e `seq` rwhnf f
168+
rwhnf d `seq` rwhnf e `seq` rwhnf f `seq`
169+
rwhnf g `seq` rwhnf h `seq` rwhnf i
134170

135171
-- | Concurrency parameters for initialising a 'HasBlockIO. Can be ignored by
136172
-- serial implementations.
@@ -195,6 +231,10 @@ deriving via (VU.UnboxViaPrim IOResult) instance VG.Vector VU.Vector IOResult
195231

196232
instance VUM.Unbox IOResult
197233

234+
{-------------------------------------------------------------------------------
235+
Advice
236+
-------------------------------------------------------------------------------}
237+
198238
-- | Basically "System.Posix.Fcntl.Advice" from the @unix@ package
199239
data Advice =
200240
AdviceNormal
@@ -214,6 +254,36 @@ hAdviseAll hbio h advice = hAdvise hbio h 0 0 advice -- len=0 implies until the
214254
hDropCacheAll :: HasBlockIO m h -> Handle h -> m ()
215255
hDropCacheAll hbio h = hAdviseAll hbio h AdviceDontNeed
216256

257+
{-------------------------------------------------------------------------------
258+
Storage synchronisation
259+
-------------------------------------------------------------------------------}
260+
261+
-- TODO: currently, we perform an explicit check to see if the file exists and
262+
-- throw an error when it does not exist. We would prefer to be able to rely on
263+
-- withFile to throw an error for us that we could rethrow with an upated
264+
-- description/location. Unfortunately, we have to open te file in ReadWriteMode
265+
-- on Windows, and withFile currently does not support such errors. The only
266+
-- options are:
267+
--
268+
-- * AllowExisting: silently create a file if it does not exist
269+
-- * MustBeNew: throw an error if the file exists
270+
--
271+
-- We would need to add a third option to fs-api:
272+
--
273+
-- * MustExist: throw an error if the file *does not* exist
274+
synchroniseFile :: MonadThrow m => HasFS m h -> HasBlockIO m h -> FsPath -> m ()
275+
synchroniseFile hfs hbio path = do
276+
b <- FS.doesFileExist hfs path
277+
if b then
278+
FS.withFile hfs path (FS.ReadWriteMode FS.AllowExisting) $ hSynchronise hbio
279+
else
280+
throwIO $ FS.ioToFsError (FS.mkFsErrorPath hfs (FS.mkFsPath [])) ioerr
281+
where
282+
ioerr =
283+
ioeSetErrorString
284+
(mkIOError doesNotExistErrorType "synchroniseFile" Nothing Nothing)
285+
("synchroniseFile: file does not exist")
286+
217287
{-------------------------------------------------------------------------------
218288
File locks
219289
-------------------------------------------------------------------------------}
@@ -249,3 +319,16 @@ rethrowFsErrorIO hfs fp action = do
249319
handleError :: HasCallStack => IOError -> IO a
250320
handleError ioErr =
251321
throwIO $ FS.ioToFsError (FS.mkFsErrorPath hfs fp) ioErr
322+
323+
{-------------------------------------------------------------------------------
324+
Hard links
325+
-------------------------------------------------------------------------------}
326+
327+
createHardLinkIO ::
328+
HasFS IO HandleIO
329+
-> (FilePath -> FilePath -> IO ())
330+
-> (FsPath -> FsPath -> IO ())
331+
createHardLinkIO hfs f = \source target -> do
332+
source' <- FS.unsafeToFilePath hfs source -- shouldn't fail because we are in IO
333+
target' <- FS.unsafeToFilePath hfs target -- shouldn't fail because we are in IO
334+
f source' target'

0 commit comments

Comments
 (0)