-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Abstraction for blockio-style file operations
Changes include: * Add blockio-uring dependency * Add an abstract API that captures the file operations from `blockio-uring`. * Implementations of this for three different operating systems: Linux, MacOS, or Windows. The Linux implementation uses `blockio-uring` and benefits from async IO. MacOS and Windows use a simple implementation that performs file I/O sequentially instead of in asynchronous batches. * Implement some basic tests for the API.
- Loading branch information
Showing
12 changed files
with
511 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
{-# LANGUAGE NamedFieldPuns #-} | ||
|
||
module System.FS.BlockIO.Async ( | ||
asyncHasBlockIO | ||
) where | ||
|
||
import Control.Exception | ||
import qualified Control.Exception as E | ||
import Control.Monad | ||
import Foreign.C.Error | ||
import GHC.IO.Exception | ||
import GHC.Stack | ||
import System.FS.API (BufferOffset (..), FsErrorPath, Handle (..), | ||
HasFS (..), SomeHasFS (..), ioToFsError) | ||
import qualified System.FS.BlockIO.API as API | ||
import System.FS.BlockIO.API (IOOp (..), IOResult (..), ioopHandle) | ||
import System.FS.IO (HandleIO) | ||
import System.FS.IO.Internal.Handle | ||
import qualified System.IO.BlockIO as I | ||
import System.IO.Error (ioeSetErrorString, isResourceVanishedError) | ||
import System.Posix.Types | ||
|
||
-- | IO instantiation of 'HasBlockIO', using @blockio-uring@. | ||
asyncHasBlockIO :: HasFS IO HandleIO -> Maybe API.IOCtxParams -> IO (API.HasBlockIO IO HandleIO) | ||
asyncHasBlockIO hasFS ctxParams = do | ||
ctx <- I.initIOCtx (maybe I.defaultIOCtxParams ctxParamsConv ctxParams) | ||
pure $ API.HasBlockIO { | ||
API.close = I.closeIOCtx ctx | ||
, API.submitIO = submitIO hasFS ctx | ||
} | ||
|
||
ctxParamsConv :: API.IOCtxParams -> I.IOCtxParams | ||
ctxParamsConv API.IOCtxParams{API.ioctxBatchSizeLimit, API.ioctxConcurrencyLimit} = | ||
I.IOCtxParams { | ||
I.ioctxBatchSizeLimit = ioctxBatchSizeLimit | ||
, I.ioctxConcurrencyLimit = ioctxConcurrencyLimit | ||
} | ||
|
||
submitIO :: | ||
HasFS IO HandleIO | ||
-> I.IOCtx | ||
-> [IOOp IO HandleIO] | ||
-> IO [IOResult] | ||
submitIO hasFS ioctx ioops = do | ||
ioops' <- mapM ioopConv ioops | ||
ress <- I.submitIO ioctx ioops' `catch` rethrowClosedError | ||
zipWithM rethrowErrno ioops ress | ||
where | ||
rethrowClosedError :: IOError -> IO a | ||
rethrowClosedError e@IOError{} = | ||
-- Pattern matching on the error is brittle, because the structure of | ||
-- the exception might change between versions of @blockio-uring@. | ||
-- Nonetheless, it's better than nothing. | ||
if isResourceVanishedError e && ioe_location e == "IOCtx closed" | ||
then throwIO (API.mkClosedError (SomeHasFS hasFS) "submitIO") | ||
else throwIO e | ||
|
||
rethrowErrno :: | ||
HasCallStack | ||
=> IOOp IO HandleIO | ||
-> I.IOResult | ||
-> IO IOResult | ||
rethrowErrno ioop res = do | ||
case res of | ||
I.IOResult c -> pure (IOResult c) | ||
I.IOError e -> throwAsFsError e | ||
where | ||
throwAsFsError :: HasCallStack => Errno -> IO a | ||
throwAsFsError errno = E.throwIO $ ioToFsError fep (fromErrno errno) | ||
|
||
fep :: FsErrorPath | ||
fep = mkFsErrorPath hasFS (handlePath (ioopHandle ioop)) | ||
|
||
fromErrno :: Errno -> IOError | ||
fromErrno errno = ioeSetErrorString | ||
(errnoToIOError "submitIO" errno Nothing Nothing) | ||
("submitIO failed: " <> ioopType) | ||
|
||
ioopType :: String | ||
ioopType = case ioop of | ||
IOOpRead{} -> "IOOpRead" | ||
IOOpWrite{} -> "IOOpWrite" | ||
|
||
ioopConv :: IOOp IO HandleIO -> IO (I.IOOp IO) | ||
ioopConv (IOOpRead h off buf bufOff c) = handleFd h >>= \fd -> | ||
pure (I.IOOpRead fd off buf (unBufferOffset bufOff) c) | ||
ioopConv (IOOpWrite h off buf bufOff c) = handleFd h >>= \fd -> | ||
pure (I.IOOpWrite fd off buf (unBufferOffset bufOff) c) | ||
|
||
-- This only checks whether the handle is open when we convert to an Fd. After | ||
-- that, the handle could be closed when we're still performing blockio | ||
-- operations. | ||
handleFd :: Handle HandleIO -> IO Fd | ||
handleFd h = withOpenHandle "submitIO" (handleRaw h) pure |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
module System.FS.BlockIO.Internal ( | ||
ioHasBlockIO | ||
) where | ||
|
||
import System.FS.API (HasBufFS, HasFS) | ||
import System.FS.BlockIO.API (HasBlockIO, IOCtxParams) | ||
import qualified System.FS.BlockIO.Async as I | ||
import System.FS.IO (HandleIO) | ||
|
||
ioHasBlockIO :: | ||
HasFS IO HandleIO | ||
-> HasBufFS IO HandleIO | ||
-> Maybe IOCtxParams | ||
-> IO (HasBlockIO IO HandleIO) | ||
ioHasBlockIO hfs _bhfs = I.asyncHasBlockIO hfs |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
module System.FS.BlockIO.Internal ( | ||
ioHasBlockIO | ||
) where | ||
|
||
import System.FS.API (HasBufFS, HasFS) | ||
import System.FS.BlockIO.API (HasBlockIO, IOCtxParams) | ||
import qualified System.FS.BlockIO.Serial as Serial | ||
import System.FS.IO (HandleIO) | ||
|
||
ioHasBlockIO :: | ||
HasFS IO HandleIO | ||
-> HasBufFS IO HandleIO | ||
-> Maybe IOCtxParams | ||
-> IO (HasBlockIO IO HandleIO) | ||
ioHasBlockIO hasFS hasBufFS _ = Serial.serialHasBlockIO hasFS hasBufFS |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
module System.FS.BlockIO.Internal ( | ||
ioHasBlockIO | ||
) where | ||
|
||
import System.FS.API (HasBufFS, HasFS) | ||
import System.FS.BlockIO.API (HasBlockIO, IOCtxParams) | ||
import qualified System.FS.BlockIO.Serial as Serial | ||
import System.FS.IO (HandleIO) | ||
|
||
ioHasBlockIO :: | ||
HasFS IO HandleIO | ||
-> HasBufFS IO HandleIO | ||
-> Maybe IOCtxParams | ||
-> IO (HasBlockIO IO HandleIO) | ||
ioHasBlockIO hasFS hasBufFS _ = Serial.serialHasBlockIO hasFS hasBufFS |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
{-# LANGUAGE GADTs #-} | ||
{-# LANGUAGE RankNTypes #-} | ||
|
||
module System.FS.BlockIO.API ( | ||
HasBlockIO (..) | ||
, IOCtxParams (..) | ||
, mkClosedError | ||
, IOOp (..) | ||
, ioopHandle | ||
, IOResult (..) | ||
-- * Re-exports | ||
, ByteCount | ||
, FileOffset | ||
) where | ||
|
||
import Control.Monad.Primitive (PrimMonad (PrimState)) | ||
import Data.Primitive.ByteArray (MutableByteArray) | ||
import GHC.IO.Exception (IOErrorType (ResourceVanished)) | ||
import System.FS.API | ||
import System.IO.Error (ioeSetErrorString, mkIOError) | ||
import System.Posix.Types (ByteCount, FileOffset) | ||
import Util.CallStack | ||
|
||
-- | Abstract interface for submitting large batches of I\/O operations. | ||
data HasBlockIO m h = HasBlockIO { | ||
-- | (Idempotent) close the interface. | ||
-- | ||
-- Using 'submitIO' after 'close' should thrown an 'FsError' exception. See | ||
-- 'mkClosedError'. | ||
close :: HasCallStack => m () | ||
-- | Submit a batch of I\/O operations and wait for the result. | ||
-- | ||
-- Results correspond to input 'IOOp's in a pair-wise manner, i.e., one can | ||
-- match 'IOOp's with 'IOResult's by zipping the input and output list. | ||
-- | ||
-- If any of the I\/O operations fails, an 'FsError' exception will be thrown. | ||
, submitIO :: HasCallStack => [IOOp m h] -> m [IOResult] | ||
} | ||
|
||
-- | Concurrency parameters for initialising a 'HasBlockIO. Can be ignored by | ||
-- serial implementations. | ||
data IOCtxParams = IOCtxParams { | ||
ioctxBatchSizeLimit :: !Int, | ||
ioctxConcurrencyLimit :: !Int | ||
} | ||
|
||
mkClosedError :: HasCallStack => SomeHasFS m -> String -> FsError | ||
mkClosedError (SomeHasFS hasFS) loc = ioToFsError (mkFsErrorPath hasFS (mkFsPath [])) ioerr | ||
where ioerr = | ||
ioeSetErrorString | ||
(mkIOError ResourceVanished loc Nothing Nothing) | ||
("HasBlockIO closed: " <> loc) | ||
|
||
|
||
data IOOp m h = | ||
IOOpRead !(Handle h) !FileOffset !(MutableByteArray (PrimState m)) !BufferOffset !ByteCount | ||
| IOOpWrite !(Handle h) !FileOffset !(MutableByteArray (PrimState m)) !BufferOffset !ByteCount | ||
|
||
ioopHandle :: IOOp m h -> Handle h | ||
ioopHandle (IOOpRead h _ _ _ _) = h | ||
ioopHandle (IOOpWrite h _ _ _ _) = h | ||
|
||
-- | Number of read/written bytes. | ||
newtype IOResult = IOResult ByteCount |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
module System.FS.BlockIO.IO ( | ||
ioHasBlockIO | ||
) where | ||
|
||
import System.FS.API (HasBufFS, HasFS) | ||
import System.FS.BlockIO.API (HasBlockIO, IOCtxParams) | ||
import qualified System.FS.BlockIO.Internal as I | ||
import System.FS.IO (HandleIO) | ||
|
||
-- | Platform-dependent IO instantiation of 'HasBlockIO'. | ||
ioHasBlockIO :: | ||
HasFS IO HandleIO | ||
-> HasBufFS IO HandleIO | ||
-> Maybe IOCtxParams | ||
-> IO (HasBlockIO IO HandleIO) | ||
ioHasBlockIO = I.ioHasBlockIO |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
{-# LANGUAGE LambdaCase #-} | ||
|
||
module System.FS.BlockIO.Serial ( | ||
serialHasBlockIO | ||
) where | ||
|
||
import Control.Concurrent.Class.MonadMVar | ||
import Control.Monad.Class.MonadThrow | ||
import System.FS.API | ||
import qualified System.FS.BlockIO.API as API | ||
import System.FS.BlockIO.API (IOOp (..), IOResult (..)) | ||
|
||
-- | IO instantiation of 'HasBlockIO', using serialised I\/O. | ||
serialHasBlockIO :: | ||
(MonadThrow m, MonadMVar m, Eq h) | ||
=> HasFS m h | ||
-> HasBufFS m h | ||
-> m (API.HasBlockIO m h) | ||
serialHasBlockIO hfs hbfs = do | ||
ctx <- initIOCtx (SomeHasFS hfs) | ||
pure $ API.HasBlockIO { | ||
API.close = close ctx | ||
, API.submitIO = submitIO hfs hbfs ctx | ||
} | ||
|
||
data IOCtx m = IOCtx { ctxFS :: SomeHasFS m, openVar :: MVar m CtxState } | ||
|
||
data CtxState = Open Word | Closing Word | Closed | ||
|
||
addSubmitter :: (MonadMVar m, MonadThrow m) => IOCtx m -> m () | ||
addSubmitter ctx = modifyMVar_ (openVar ctx) $ \case | ||
Open n -> pure (Open (n+1)) | ||
Closing _ -> throwIO (API.mkClosedError (ctxFS ctx) "submitIO") | ||
Closed -> throwIO (API.mkClosedError (ctxFS ctx) "submitIO") | ||
|
||
removeSubmitter :: (MonadMVar m, MonadThrow m) => IOCtx m -> m () | ||
removeSubmitter ctx = modifyMVar_ (openVar ctx) $ \case | ||
Open n -> pure (Open (n-1)) | ||
Closing n | ||
| n - 1 == 0 -> pure Closed | ||
| otherwise -> pure (Closing (n-1)) | ||
Closed -> throwIO (API.mkClosedError (ctxFS ctx) "submitIO") | ||
|
||
initIOCtx :: MonadMVar m => SomeHasFS m -> m (IOCtx m) | ||
initIOCtx someHasFS = IOCtx someHasFS <$> newMVar (Open 0) | ||
|
||
close :: MonadMVar m => IOCtx m -> m () | ||
close ctx = modifyMVar_ (openVar ctx) $ \case | ||
Open n -> pure (Closing n) | ||
Closing n -> pure (Closing n) | ||
Closed -> pure Closed | ||
|
||
submitIO :: | ||
(MonadMVar m, MonadThrow m) | ||
=> HasFS m h | ||
-> HasBufFS m h | ||
-> IOCtx m | ||
-> [IOOp m h] | ||
-> m [IOResult] | ||
submitIO hfs hbfs ctx ioops = do | ||
addSubmitter ctx | ||
ress <- mapM (ioop hfs hbfs) ioops | ||
removeSubmitter ctx | ||
pure ress | ||
|
||
-- | Perform the IOOp using synchronous I\/O. | ||
ioop :: | ||
MonadThrow m | ||
=> HasFS m h | ||
-> HasBufFS m h | ||
-> IOOp m h | ||
-> m IOResult | ||
ioop hfs hbfs (IOOpRead h off buf bufOff c) = | ||
IOResult <$> hGetBufExactlyAt hfs hbfs h buf bufOff c (fromIntegral off) | ||
ioop _hfs hbfs (IOOpWrite h off buf bufOff c) = | ||
IOResult <$> hPutBufExactlyAt hbfs h buf bufOff c (fromIntegral off) |
Oops, something went wrong.