Skip to content

Commit a5ce645

Browse files
committed
blockio: refactor and document the IO module
1 parent 246568f commit a5ce645

File tree

12 files changed

+208
-112
lines changed

12 files changed

+208
-112
lines changed

bench/macro/lsm-tree-bench-lookups.hs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -307,11 +307,10 @@ totalNumEntriesSanityCheck l1 runSizes =
307307
withFS ::
308308
(FS.HasFS IO FS.HandleIO -> FS.HasBlockIO IO FS.HandleIO -> IO a)
309309
-> IO a
310-
withFS action = do
311-
let hfs = FS.ioHasFS (FS.MountPoint "_bench_lookups")
312-
exists <- FS.doesDirectoryExist hfs (FS.mkFsPath [""])
313-
unless exists $ error ("_bench_lookups directory does not exist")
314-
FS.withIOHasBlockIO hfs FS.defaultIOCtxParams $ \hbio ->
310+
withFS action =
311+
FS.withIOHasBlockIO (FS.MountPoint "_bench_lookups") FS.defaultIOCtxParams $ \hfs hbio -> do
312+
exists <- FS.doesDirectoryExist hfs (FS.mkFsPath [""])
313+
unless exists $ error ("_bench_lookups directory does not exist")
315314
action hfs hbio
316315

317316
-- | Input environment for benchmarking lookup functions.

bench/macro/lsm-tree-bench-wp8.hs

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,6 @@ import qualified MCG
6262
import qualified Options.Applicative as O
6363
import Prelude hiding (lookup)
6464
import qualified System.Clock as Clock
65-
import qualified System.FS.API as FS
66-
import qualified System.FS.BlockIO.IO as FsIO
67-
import qualified System.FS.IO as FsIO
6865
import System.IO
6966
import System.Mem (performMajorGC)
7067
import qualified System.Random as Random
@@ -402,17 +399,8 @@ doSetup gopts opts = do
402399

403400
doSetup' :: GlobalOpts -> SetupOpts -> IO ()
404401
doSetup' gopts opts = do
405-
let mountPoint :: FS.MountPoint
406-
mountPoint = FS.MountPoint (rootDir gopts)
407-
408-
let hasFS :: FS.HasFS IO FsIO.HandleIO
409-
hasFS = FsIO.ioHasFS mountPoint
410-
411-
hasBlockIO <- FsIO.ioHasBlockIO hasFS FsIO.defaultIOCtxParams
412-
413402
let name = LSM.toSnapshotName "bench"
414-
415-
LSM.withSession (mkTracer gopts) hasFS hasBlockIO (FS.mkFsPath []) $ \session -> do
403+
LSM.withSessionIO (mkTracer gopts) (rootDir gopts) $ \session -> do
416404
tbl <- LSM.newTableWith @IO @K @V @B (mkTableConfigSetup gopts opts benchTableConfig) session
417405

418406
forM_ (groupsOfN 256 [ 0 .. initialSize gopts ]) $ \batch -> do
@@ -564,17 +552,9 @@ toOperations lookups inserts = (batch1, batch2)
564552

565553
doRun :: GlobalOpts -> RunOpts -> IO ()
566554
doRun gopts opts = do
567-
let mountPoint :: FS.MountPoint
568-
mountPoint = FS.MountPoint (rootDir gopts)
569-
570-
let hasFS :: FS.HasFS IO FsIO.HandleIO
571-
hasFS = FsIO.ioHasFS mountPoint
572-
573-
hasBlockIO <- FsIO.ioHasBlockIO hasFS FsIO.defaultIOCtxParams
574-
575555
let name = LSM.toSnapshotName "bench"
576556

577-
LSM.withSession (mkTracer gopts) hasFS hasBlockIO (FS.mkFsPath []) $ \session ->
557+
LSM.withSessionIO (mkTracer gopts) (rootDir gopts) $ \session ->
578558
withLatencyHandle $ \h -> do
579559
-- open snapshot
580560
-- In checking mode we start with an empty table, since our pure

bench/micro/Bench/Database/LSMTree.hs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -429,8 +429,7 @@ mkFiles ::
429429
mkFiles = do
430430
sysTmpDir <- getCanonicalTemporaryDirectory
431431
benchTmpDir <- createTempDirectory sysTmpDir "full"
432-
let hfs = FS.ioHasFS (FS.MountPoint benchTmpDir)
433-
hbio <- FS.ioHasBlockIO hfs FS.defaultIOCtxParams
432+
(hfs, hbio) <- FS.ioHasBlockIO (FS.MountPoint benchTmpDir) FS.defaultIOCtxParams
434433
pure (benchTmpDir, hfs, hbio)
435434

436435
cleanupFiles ::

bench/micro/Bench/Database/LSMTree/Internal/Lookup.hs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,8 +187,7 @@ lookupsInBatchesEnv Config {..} = do
187187
sysTmpDir <- getCanonicalTemporaryDirectory
188188
benchTmpDir <- createTempDirectory sysTmpDir "lookupsInBatchesEnv"
189189
(storedKeys, lookupKeys) <- lookupsEnv (mkStdGen 17) nentries npos nneg
190-
let hasFS = FS.ioHasFS (FS.MountPoint benchTmpDir)
191-
hasBlockIO <- FS.ioHasBlockIO hasFS (fromMaybe FS.defaultIOCtxParams ioctxps)
190+
(hasFS, hasBlockIO) <- FS.ioHasBlockIO (FS.MountPoint benchTmpDir) (fromMaybe FS.defaultIOCtxParams ioctxps)
192191
wbblobs <- WBB.new hasFS (FS.mkFsPath ["0.wbblobs"])
193192
wb <- WB.fromMap <$> traverse (traverse (WBB.addBlob hasFS wbblobs)) storedKeys
194193
let fsps = RunFsPaths (FS.mkFsPath []) (RunNumber 0)

bench/micro/Bench/Database/LSMTree/Internal/Merge.hs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -369,8 +369,7 @@ mergeEnv ::
369369
mergeEnv config = do
370370
sysTmpDir <- getCanonicalTemporaryDirectory
371371
benchTmpDir <- createTempDirectory sysTmpDir "mergeEnv"
372-
let hasFS = FS.ioHasFS (FS.MountPoint benchTmpDir)
373-
hasBlockIO <- FS.ioHasBlockIO hasFS FS.defaultIOCtxParams
372+
(hasFS, hasBlockIO) <- FS.ioHasBlockIO (FS.MountPoint benchTmpDir) FS.defaultIOCtxParams
374373
runs <- randomRuns hasFS hasBlockIO config (mkStdGen 17)
375374
pure (benchTmpDir, hasFS, hasBlockIO, runs)
376375

Lines changed: 154 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,174 @@
1+
-- | Implementations using the real file system.
2+
--
3+
-- The implementation of the 'HasBlockIO' interface provided in this module is
4+
-- platform-dependent. Most importantly, on Linux, the implementation of
5+
-- 'submitIO' is backed by @blockio-uring@: a library for asynchronous I/O. On
6+
-- Windows and MacOS, the implementation of 'submitIO' only supports serial I/O.
17
module System.FS.BlockIO.IO (
8+
-- * Implementation details #impl#
9+
-- $impl
10+
11+
-- * Initialisation
212
ioHasBlockIO
313
, withIOHasBlockIO
14+
-- ** Parameters
415
, IOI.IOCtxParams (..)
516
, IOI.defaultIOCtxParams
17+
-- ** Unsafe
18+
, unsafeFromHasFS
19+
, withUnsafeFromHasFS
620
) where
721

822
import Control.Exception (bracket)
9-
import System.FS.API (HasFS)
23+
import System.FS.API (HasFS, MountPoint)
1024
import System.FS.BlockIO.API (HasBlockIO (..))
1125
import qualified System.FS.BlockIO.Internal as I
1226
import qualified System.FS.BlockIO.IO.Internal as IOI
13-
import System.FS.IO (HandleIO)
27+
import System.FS.IO (HandleIO, ioHasFS)
1428

15-
-- | Platform-dependent IO instantiation of 'HasBlockIO'.
16-
ioHasBlockIO ::
29+
{- $impl
30+
31+
Though the 'HasBlockIO' interface tries to capture uniform behaviour, each
32+
function in this implementation for the real file system can have subtly
33+
different effects depending on the underlying patform. For example, some
34+
features are not provided by some operating systems, and in some cases the
35+
features behave subtly differently for different operating systems. For this
36+
reason, we include below some documentation about the effects of calling the
37+
interface functions on different platforms.
38+
39+
Note: if the @serialblockio@ Cabal flag is enabled, then the Linux implementation
40+
uses a mocked context and serial I/O for 'close' and 'submitIO', just like the
41+
MacOS and Windows implementations do.
42+
43+
[IO context]: When an instance of the 'HasBlockIO' interface for Linux
44+
systems is initialised, an @io_uring@ context is created using the
45+
@blockio-uring@ package and stored in the 'HasBlockIO' closure. For uniform
46+
behaviour, each other platform creates and stores a mocked IO context that
47+
has the same open/closed behaviour as an @io_uring@ context. In summary,
48+
each platform creates:
49+
50+
* Linux: an @io_uring@ context provided by the @blockio-uring@ package
51+
* MacOS: a mocked context using an @MVar@
52+
* Windows: a mocked conext using an @MVar@
53+
54+
['close']:
55+
56+
* Linux: close the @io_uring@ context through the @blockio-uring@ package
57+
* MacOS: close the mocked context
58+
* Windows: close the mocked context
59+
60+
['submitIO']: Submit a batch of I/O operations using:
61+
62+
* Linux: the @submitIO@ function from the @blockio-uring@ package
63+
* MacOS: serial I/O using a 'HasFS'
64+
* Windows: serial I/O using a 'HasFS'
65+
66+
['hSetNoCache']:
67+
68+
* Linux: set the @O_DIRECT@ flag
69+
* MacOS: set the @F_NOCACHE@ flag
70+
* Windows: no-op
71+
72+
['hAdvise']:
73+
74+
* Linux: perform @posix_fadvise(2)@
75+
* MacOS: no-op
76+
* Windows: no-op
77+
78+
['hAllocate']:
79+
80+
* Linux: perform @posix_fallocate(2)@
81+
* MacOS: no-op
82+
* Windows: no-op
83+
84+
['tryLockFile']: This uses different locking methods depending on the OS.
85+
86+
* Linux: Open file descriptor (OFD)
87+
* MacOS: @flock@
88+
* Windows: @LockFileEx@
89+
90+
['hSynchronise']:
91+
92+
* Linux: perform @fsync(2)@
93+
* MacOS: perform @fsync(2)@
94+
* Windows: perform @flushFileBuffers@
95+
96+
['synchroniseDirectory']:
97+
98+
* Linux: perform @fsync(2)@
99+
* MacOS: perform @fsync(2)@
100+
* Windows: no-op
101+
102+
['createHardLink']:
103+
104+
* Linux: perform @link@
105+
* MacOS: perform @link@
106+
* Windows: perform @CreateHardLinkW@
107+
-}
108+
109+
-- | An implementation of the 'HasBlockIO' interface using the real file system.
110+
--
111+
-- Make sure to use 'close' the resulting 'HasBlockIO' when it is no longer
112+
-- used. 'withUnsafeFromHasFS' does this automatically.
113+
--
114+
-- === Unsafe
115+
--
116+
-- You will probably want to use 'ioHasBlockIO' or 'withIOHasBlockIO' instead.
117+
--
118+
-- Only a 'HasFS' for the real file system, like 'ioHasFS', should be passed to
119+
-- 'unsafeFromHasFS'. Technically, one could pass a 'HasFS' for a simulated file
120+
-- system, but then the resulting 'HasBlockIO' would contain a mix of simulated
121+
-- and real functions, which is probably not what you want.
122+
unsafeFromHasFS ::
17123
HasFS IO HandleIO
18124
-> IOI.IOCtxParams
19125
-> IO (HasBlockIO IO HandleIO)
20-
ioHasBlockIO = I.ioHasBlockIO
126+
unsafeFromHasFS = I.ioHasBlockIO
21127

22-
withIOHasBlockIO ::
128+
-- | Perform an action using a 'HasBlockIO' instance that is only open for the
129+
-- duration of the action.
130+
--
131+
-- The 'HasBlockIO' is initialised using 'unsafeFromHasFS'.
132+
--
133+
-- === Unsafe
134+
--
135+
-- You will probably want to use 'ioHasBlockIO' or 'withIOHasBlockIO' instead.
136+
--
137+
-- Only a 'HasFS' for the real file system, like 'ioHasFS', should be passed to
138+
-- 'withUnsafeFromHasFS'. Technically, one could pass a 'HasFS' for a simulated
139+
-- file system, but then the resulting 'HasBlockIO' would contain a mix of
140+
-- simulated and real functions, which is probably not what you want.
141+
withUnsafeFromHasFS ::
23142
HasFS IO HandleIO
24143
-> IOI.IOCtxParams
25144
-> (HasBlockIO IO HandleIO -> IO a)
26145
-> IO a
27-
withIOHasBlockIO hfs params action =
28-
bracket (ioHasBlockIO hfs params) (\HasBlockIO{close} -> close) action
146+
withUnsafeFromHasFS hfs params =
147+
bracket (unsafeFromHasFS hfs params) (\HasBlockIO{close} -> close)
148+
149+
-- | An implementation of the 'HasBlockIO' interface using the real file system.
150+
--
151+
-- Make sure to use 'close' the resulting 'HasBlockIO' when it is no longer
152+
-- used. 'withIOHasBlockIO' does this automatically.
153+
--
154+
-- The 'HasFS' interface is instantiated using 'ioHasFS'.
155+
ioHasBlockIO ::
156+
MountPoint
157+
-> IOI.IOCtxParams
158+
-> IO (HasFS IO HandleIO, HasBlockIO IO HandleIO)
159+
ioHasBlockIO mount params = do
160+
let hfs = ioHasFS mount
161+
hbio <- unsafeFromHasFS hfs params
162+
pure (hfs, hbio)
163+
164+
-- | Perform an action using a 'HasFS' and a 'HasBlockIO' instance. The latter
165+
-- is only open for the duration of the action.
166+
--
167+
-- The 'HasFS' and 'HasBlockIO' interfaces are initialised using 'ioHasBlockIO'.
168+
withIOHasBlockIO ::
169+
MountPoint
170+
-> IOI.IOCtxParams
171+
-> (HasFS IO HandleIO -> HasBlockIO IO HandleIO -> IO a)
172+
-> IO a
173+
withIOHasBlockIO mount params action =
174+
bracket (ioHasBlockIO mount params) (\(_, HasBlockIO{close}) -> close) (uncurry action)

blockio/test/Main.hs

Lines changed: 34 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@ import System.FS.API.Strict (hPutAllStrict)
2424
import qualified System.FS.BlockIO.API as FS
2525
import System.FS.BlockIO.API
2626
import qualified System.FS.BlockIO.IO as IO
27-
import System.FS.BlockIO.IO
28-
import qualified System.FS.IO as IO
2927
import System.FS.IO
3028
import System.IO.Temp
3129
import Test.QuickCheck
@@ -68,15 +66,12 @@ toByteString n mba = do
6866
example_initClose :: Assertion
6967
example_initClose = withSystemTempDirectory "example_initClose" $ \dirPath -> do
7068
let mount = FS.MountPoint dirPath
71-
hfs = IO.ioHasFS mount
72-
hbio <- IO.ioHasBlockIO hfs IO.defaultIOCtxParams
73-
close hbio
69+
IO.withIOHasBlockIO mount IO.defaultIOCtxParams $ \_ _ -> pure ()
7470

7571
example_closeIsIdempotent :: Assertion
7672
example_closeIsIdempotent = withSystemTempDirectory "example_closeIsIdempotent" $ \dirPath -> do
7773
let mount = FS.MountPoint dirPath
78-
hfs = IO.ioHasFS mount
79-
hbio <- IO.ioHasBlockIO hfs IO.defaultIOCtxParams
74+
hbio <- IO.withIOHasBlockIO mount IO.defaultIOCtxParams $ \_ hbio -> pure hbio
8075
close hbio
8176
eith <- try @SomeException (close hbio)
8277
case eith of
@@ -88,61 +83,48 @@ example_closeIsIdempotent = withSystemTempDirectory "example_closeIsIdempotent"
8883
prop_readWrite :: ByteString -> Property
8984
prop_readWrite bs = ioProperty $ withSystemTempDirectory "prop_readWrite" $ \dirPath -> do
9085
let mount = FS.MountPoint dirPath
91-
hfs = IO.ioHasFS mount
92-
hbio <- IO.ioHasBlockIO hfs IO.defaultIOCtxParams
93-
prop <- FS.withFile hfs (FS.mkFsPath ["temp"]) (FS.WriteMode FS.MustBeNew) $ \h -> do
94-
let n = BS.length bs
95-
writeBuf <- fromByteStringPinned bs
96-
[IOResult m] <- VU.toList <$> submitIO hbio (V.singleton (IOOpWrite h 0 writeBuf 0 (fromIntegral n)))
97-
let writeTest = n === fromIntegral m
98-
readBuf <- newPinnedByteArray n
99-
[IOResult o] <- VU.toList <$> submitIO hbio (V.singleton (IOOpRead h 0 readBuf 0 (fromIntegral n)))
100-
let readTest = o === m
101-
bs' <- toByteString n readBuf
102-
let cmpTest = bs === bs'
103-
pure $ writeTest .&&. readTest .&&. cmpTest
104-
close hbio
105-
pure prop
86+
IO.withIOHasBlockIO mount IO.defaultIOCtxParams $ \hfs hbio -> do
87+
FS.withFile hfs (FS.mkFsPath ["temp"]) (FS.WriteMode FS.MustBeNew) $ \h -> do
88+
let n = BS.length bs
89+
writeBuf <- fromByteStringPinned bs
90+
[IOResult m] <- VU.toList <$> submitIO hbio (V.singleton (IOOpWrite h 0 writeBuf 0 (fromIntegral n)))
91+
let writeTest = n === fromIntegral m
92+
readBuf <- newPinnedByteArray n
93+
[IOResult o] <- VU.toList <$> submitIO hbio (V.singleton (IOOpRead h 0 readBuf 0 (fromIntegral n)))
94+
let readTest = o === m
95+
bs' <- toByteString n readBuf
96+
let cmpTest = bs === bs'
97+
pure $ writeTest .&&. readTest .&&. cmpTest
10698

10799
prop_submitToClosedCtx :: ByteString -> Property
108100
prop_submitToClosedCtx bs = ioProperty $ withSystemTempDirectory "prop_a" $ \dir -> do
109101
let mount = FS.MountPoint dir
110-
hfs = IO.ioHasFS mount
111-
hbio <- IO.ioHasBlockIO hfs IO.defaultIOCtxParams
112-
113-
props <- FS.withFile hfs (FS.mkFsPath ["temp"]) (FS.WriteMode FS.MustBeNew) $ \h -> do
114-
void $ hPutAllStrict hfs h bs
115-
syncVar <- newMVar False
116-
forConcurrently [0 .. BS.length bs - 1] $ \i ->
117-
if i == 0 then do
118-
threadDelay 15
119-
modifyMVar_ syncVar $ \_ -> do
120-
close hbio
121-
pure True
122-
pure Nothing
123-
else do
124-
readBuf <- newPinnedByteArray (BS.length bs)
125-
withMVar syncVar $ \b -> do
126-
eith <- try @SomeException $ submitIO hbio (V.singleton (IOOpRead h 0 readBuf (fromIntegral i) 1))
127-
pure $ case eith of
128-
Left _ -> Just $ tabulate "submitIO successful" [show False] $ counterexample "expected failure, but got success" (b === True)
129-
Right _ -> Just $ tabulate "submitIO successful" [show True] $ counterexample "expected success, but got failure" (b === False)
130-
pure $ conjoin (catMaybes props)
131-
102+
IO.withIOHasBlockIO mount IO.defaultIOCtxParams $ \hfs hbio -> do
103+
FS.withFile hfs (FS.mkFsPath ["temp"]) (FS.WriteMode FS.MustBeNew) $ \h -> do
104+
void $ hPutAllStrict hfs h bs
105+
syncVar <- newMVar False
106+
fmap (conjoin . catMaybes) $ forConcurrently [0 .. BS.length bs - 1] $ \i ->
107+
if i == 0 then do
108+
threadDelay 15
109+
modifyMVar_ syncVar $ \_ -> do
110+
close hbio
111+
pure True
112+
pure Nothing
113+
else do
114+
readBuf <- newPinnedByteArray (BS.length bs)
115+
withMVar syncVar $ \b -> do
116+
eith <- try @SomeException $ submitIO hbio (V.singleton (IOOpRead h 0 readBuf (fromIntegral i) 1))
117+
pure $ case eith of
118+
Left _ -> Just $ tabulate "submitIO successful" [show False] $ counterexample "expected failure, but got success" (b === True)
119+
Right _ -> Just $ tabulate "submitIO successful" [show True] $ counterexample "expected success, but got failure" (b === False)
132120

133121
{-------------------------------------------------------------------------------
134122
File locks
135123
-------------------------------------------------------------------------------}
136124

137-
withTempIOHasFS :: FilePath -> (HasFS IO HandleIO -> IO a) -> IO a
138-
withTempIOHasFS path action = withSystemTempDirectory path $ \dir -> do
139-
let hfs = ioHasFS (MountPoint dir)
140-
action hfs
141-
142125
withTempIOHasBlockIO :: FilePath -> (HasFS IO HandleIO -> HasBlockIO IO HandleIO -> IO a) -> IO a
143-
withTempIOHasBlockIO path action =
144-
withTempIOHasFS path $ \hfs -> do
145-
withIOHasBlockIO hfs defaultIOCtxParams (action hfs)
126+
withTempIOHasBlockIO path action = withSystemTempDirectory path $ \dir -> do
127+
IO.withIOHasBlockIO (MountPoint dir) IO.defaultIOCtxParams action
146128

147129
showLeft :: Show a => String -> Either a b -> String
148130
showLeft x = \case

0 commit comments

Comments
 (0)