diff --git a/lsm-tree.cabal b/lsm-tree.cabal index 7e42f11fd..887a171b5 100644 --- a/lsm-tree.cabal +++ b/lsm-tree.cabal @@ -131,6 +131,7 @@ library Database.LSMTree.Internal.CRC32C Database.LSMTree.Internal.Cursor Database.LSMTree.Internal.Entry + Database.LSMTree.Internal.FS.File Database.LSMTree.Internal.Index Database.LSMTree.Internal.Index.Compact Database.LSMTree.Internal.Index.CompactAcc @@ -362,6 +363,7 @@ test-suite lsm-tree-test Test.Database.LSMTree.Internal.Chunk Test.Database.LSMTree.Internal.CRC32C Test.Database.LSMTree.Internal.Entry + Test.Database.LSMTree.Internal.FS.File Test.Database.LSMTree.Internal.Index.Compact Test.Database.LSMTree.Internal.Index.Ordinary Test.Database.LSMTree.Internal.Lookup diff --git a/src-extras/Database/LSMTree/Extras/NoThunks.hs b/src-extras/Database/LSMTree/Extras/NoThunks.hs index 2661aa16f..17c272a53 100644 --- a/src-extras/Database/LSMTree/Extras/NoThunks.hs +++ b/src-extras/Database/LSMTree/Extras/NoThunks.hs @@ -41,6 +41,7 @@ import Database.LSMTree.Internal.Chunk import Database.LSMTree.Internal.Config import Database.LSMTree.Internal.CRC32C import Database.LSMTree.Internal.Entry +import Database.LSMTree.Internal.FS.File import Database.LSMTree.Internal.Index import Database.LSMTree.Internal.Index.Compact import Database.LSMTree.Internal.Index.CompactAcc @@ -267,7 +268,7 @@ instance NoThunks WriteBuffer where !y = toMap x {------------------------------------------------------------------------------- - BlobFile + WriteBufferBlobs -------------------------------------------------------------------------------} deriving stock instance Generic (WriteBufferBlobs m h) @@ -278,6 +279,24 @@ deriving stock instance Generic (FilePointer m) deriving anyclass instance Typeable (PrimState m) => NoThunks (FilePointer m) +{------------------------------------------------------------------------------- + BlobFile +-------------------------------------------------------------------------------} + +deriving stock instance Generic (BlobFile m h) +deriving anyclass instance (Typeable h, Typeable (PrimState m), Typeable m) + => NoThunks (BlobFile m h) + +deriving stock instance Generic BlobSpan +deriving anyclass instance NoThunks BlobSpan + +{------------------------------------------------------------------------------- + File +-------------------------------------------------------------------------------} + +deriving stock instance Generic (File m) +deriving anyclass instance Typeable (PrimState m) => NoThunks (File m) + {------------------------------------------------------------------------------- Index -------------------------------------------------------------------------------} @@ -533,15 +552,8 @@ deriving anyclass instance NoThunks RawOverflowPage BlobRef -------------------------------------------------------------------------------} -deriving stock instance Generic BlobSpan -deriving anyclass instance NoThunks BlobSpan - -deriving stock instance Generic (BlobFile m h) -deriving anyclass instance (Typeable h, Typeable (PrimState m)) - => NoThunks (BlobFile m h) - deriving stock instance Generic (RawBlobRef m h) -deriving anyclass instance (Typeable h, Typeable (PrimState m)) +deriving anyclass instance (Typeable h, Typeable (PrimState m), Typeable m) => NoThunks (RawBlobRef m h) deriving stock instance Generic (WeakBlobRef m h) diff --git a/src/Database/LSMTree/Internal/BlobFile.hs b/src/Database/LSMTree/Internal/BlobFile.hs index 02c3edbd7..765bf4ae8 100644 --- a/src/Database/LSMTree/Internal/BlobFile.hs +++ b/src/Database/LSMTree/Internal/BlobFile.hs @@ -2,6 +2,8 @@ module Database.LSMTree.Internal.BlobFile ( BlobFile (..) , BlobSpan (..) + , unsafeOpenBlobFile + , newBlobFile , openBlobFile , readBlob , readBlobRaw @@ -10,12 +12,13 @@ module Database.LSMTree.Internal.BlobFile ( import Control.DeepSeq (NFData (..)) import Control.Monad.Class.MonadThrow (MonadCatch (bracketOnError), - MonadThrow (..)) + MonadMask, MonadThrow (..)) import Control.Monad.Primitive (PrimMonad) import Control.RefCount import qualified Data.Primitive.ByteArray as P import qualified Data.Vector.Primitive as VP import Data.Word (Word32, Word64) +import Database.LSMTree.Internal.FS.File import qualified Database.LSMTree.Internal.RawBytes as RB import Database.LSMTree.Internal.Serialise (SerialisedBlob (..)) import qualified System.FS.API as FS @@ -29,16 +32,19 @@ import System.FS.CallStack (HasCallStack) -- and deleted. -- data BlobFile m h = BlobFile { - blobFileHandle :: {-# UNPACK #-} !(FS.Handle h), - blobFileRefCounter :: {-# UNPACK #-} !(RefCounter m) - } + blobFileHandle :: {-# UNPACK #-} !(FS.Handle h) + -- | TODO: once 'unsafeOpenBlobFile' is removed, replace this by just a + -- @Ref (File m)@. + , blobFileFile :: {-# UNPACK #-} !(Maybe (Ref (File m))) + , blobFileRefCounter :: {-# UNPACK #-} !(RefCounter m) + } deriving stock (Show) instance RefCounted m (BlobFile m h) where getRefCounter = blobFileRefCounter instance NFData h => NFData (BlobFile m h) where - rnf (BlobFile a b) = rnf a `seq` rnf b + rnf (BlobFile a b c) = rnf a `seq` rnf b `seq` rnf c -- | The location of a blob inside a blob file. data BlobSpan = BlobSpan { @@ -50,22 +56,17 @@ data BlobSpan = BlobSpan { instance NFData BlobSpan where rnf (BlobSpan a b) = rnf a `seq` rnf b --- | Open the given file to make a 'BlobFile'. The finaliser will close and --- delete the file. --- --- REF: the resulting reference must be released once it is no longer used. --- --- ASYNC: this should be called with asynchronous exceptions masked because it --- allocates/creates resources. -{-# SPECIALISE openBlobFile :: HasCallStack => HasFS IO h -> FS.FsPath -> FS.OpenMode -> IO (Ref (BlobFile IO h)) #-} -openBlobFile :: +{-# SPECIALISE unsafeOpenBlobFile :: HasCallStack => HasFS IO h -> FS.FsPath -> FS.OpenMode -> IO (Ref (BlobFile IO h)) #-} +-- | TODO: replace at use sites by 'newBlobFile' or 'openBlobFile', and then +-- remove this function. +unsafeOpenBlobFile :: (PrimMonad m, MonadCatch m) => HasCallStack => HasFS m h -> FS.FsPath -> FS.OpenMode -> m (Ref (BlobFile m h)) -openBlobFile fs path mode = +unsafeOpenBlobFile fs path mode = bracketOnError (FS.hOpen fs path mode) (FS.hClose fs) $ \blobFileHandle -> do let finaliser = FS.hClose fs blobFileHandle `finally` @@ -77,9 +78,68 @@ openBlobFile fs path mode = newRef finaliser $ \blobFileRefCounter -> BlobFile { blobFileHandle, + blobFileFile = Nothing, blobFileRefCounter } +{-# SPECIALISE fromFile :: HasFS IO h -> Ref (File IO) -> Mode -> IO (Ref (BlobFile IO h)) #-} +-- | Create a 'BlobFile' from a 'File'. +-- +-- REF: the resulting reference must be released once it is no longer used. +-- +-- ASYNC: this should be called with asynchronous exceptions masked because it +-- allocates/creates resources. +fromFile :: + (PrimMonad m, MonadMask m) + => HasFS m h + -> Ref (File m) + -> Mode + -> m (Ref (BlobFile m h)) +fromFile hfs blobFileFile mode = + bracketOnError (openHandle hfs blobFileFile mode) (FS.hClose hfs) $ \blobFileHandle -> do + let finaliser = FS.hClose hfs blobFileHandle `finally` releaseRef blobFileFile + newRef finaliser $ \blobFileRefCounter -> BlobFile { + blobFileHandle + , blobFileFile = Just $! blobFileFile + , blobFileRefCounter + } + +{-# SPECIALISE newBlobFile :: HasCallStack => HasFS IO h -> FS.FsPath -> Mode -> IO (Ref (BlobFile IO h)) #-} +-- | Create a new 'BlobFile'. +-- +-- REF: the resulting reference must be released once it is no longer used. +-- +-- ASYNC: this should be called with asynchronous exceptions masked because it +-- allocates/creates resources. +newBlobFile :: + (PrimMonad m, MonadMask m) + => HasCallStack + => HasFS m h + -> FS.FsPath + -> Mode + -> m (Ref (BlobFile m h)) +newBlobFile hfs path mode = + bracketOnError (newFile hfs path) releaseRef $ + \file -> fromFile hfs file mode + +{-# SPECIALISE openBlobFile :: HasCallStack => HasFS IO h -> Ref (File IO) -> Mode -> IO (Ref (BlobFile IO h)) #-} +-- | Open an existing 'File' to make a 'BlobFile'. +-- +-- REF: the resulting reference must be released once it is no longer used. +-- +-- ASYNC: this should be called with asynchronous exceptions masked because it +-- allocates/creates resources. +openBlobFile :: + (PrimMonad m, MonadMask m) + => HasCallStack + => HasFS m h + -> Ref (File m) + -> Mode + -> m (Ref (BlobFile m h)) +openBlobFile hfs file mode = + bracketOnError (dupRef file) releaseRef $ + \file' -> fromFile hfs file' mode + {-# INLINE readBlob #-} readBlob :: (MonadThrow m, PrimMonad m) diff --git a/src/Database/LSMTree/Internal/FS/File.hs b/src/Database/LSMTree/Internal/FS/File.hs new file mode 100644 index 000000000..e7363f0fc --- /dev/null +++ b/src/Database/LSMTree/Internal/FS/File.hs @@ -0,0 +1,141 @@ +module Database.LSMTree.Internal.FS.File ( + File (..) + -- * Construction + , newFile + , copyFile + -- * Opening handles + , Mode (..) + , openHandle + ) where + +import Control.DeepSeq +import Control.Monad +import Control.Monad.Class.MonadThrow +import Control.Monad.Primitive (PrimMonad) +import Control.RefCount +import qualified System.FS.API as FS +import System.FS.API (AllowExisting (..), FsPath, Handle, HasFS) +import qualified System.FS.API.Lazy as FSL + +-- | A reference counted file. +-- +-- When all references are released, the file will be deleted from disk. +-- +-- INVARIANT: 'filePath' /must/ exist on disk. +data File m = File { + filePath :: {-# UNPACK #-} !FsPath, + fileRefCounter :: {-# UNPACK #-} !(RefCounter m) + } + deriving stock Show + +instance RefCounted m (File m) where + getRefCounter = fileRefCounter + +instance NFData (File m) where + rnf (File a b) = rnf a `seq` rnf b + +-- | Create a 'File' from a file path. This functions assumes the file path +-- exists. +-- +-- REF: the resulting reference must be released once it is no longer used. +-- +-- ASYNC: this should be called with asynchronous exceptions masked because it +-- allocates/creates resources. +fromFsPath :: PrimMonad m => HasFS m h -> FsPath -> m (Ref (File m)) +fromFsPath hfs path = do + let finaliser = FS.removeFile hfs path + newRef finaliser $ \fileRefCounter -> + File { + filePath = path + , fileRefCounter + } + +{------------------------------------------------------------------------------- + Construction +-------------------------------------------------------------------------------} + +{-# SPECIALISE newFile :: HasFS IO h -> FsPath -> IO (Ref (File IO)) #-} +{-# INLINABLE newFile #-} +-- | Create a new file. +-- +-- REF: the resulting reference must be released once it is no longer used. +-- +-- ASYNC: this should be called with asynchronous exceptions masked because it +-- allocates/creates resources. +newFile :: (MonadCatch m, PrimMonad m) => HasFS m h -> FsPath -> m (Ref (File m)) +newFile hfs path = withCreateFile hfs path $ \_h -> fromFsPath hfs path + +{-# SPECIALISE copyFile :: HasFS IO h -> FsPath -> FsPath -> IO (Ref (File IO)) #-} +{-# INLINABLE copyFile #-} +-- | @'copyFile' hfs source target@ copies the @source@ path to the /new/ +-- @target@ path. +-- +-- REF: the resulting reference must be released once it is no longer used. +-- +-- ASYNC: this should be called with asynchronous exceptions masked because it +-- allocates/creates resources. +copyFile :: + (MonadCatch m, PrimMonad m) + => HasFS m h + -> FsPath + -> FsPath + -> m (Ref (File m)) +copyFile hfs sourcePath targetPath = + FS.withFile hfs sourcePath FS.ReadMode $ \sourceHandle -> do + withCreateFile hfs targetPath $ \targetHandle -> do + bs <- FSL.hGetAll hfs sourceHandle + void $ FSL.hPutAll hfs targetHandle bs + fromFsPath hfs targetPath + +{-# SPECIALISE withCreateFile :: HasFS IO h -> FsPath -> (Handle h -> IO a) -> IO a #-} +{-# INLINABLE withCreateFile #-} +-- | Run an action on a handle to a newly created file. +-- +-- The handle is closed automatically when the action is finished, even if an +-- exception is thrown in the action. +-- +-- If an exception happens in the action, the file is removed. +-- +-- ASYNC: this should be called with asynchronous exceptions masked because it +-- allocates/creates resources. +withCreateFile :: MonadCatch m => HasFS m h -> FsPath -> (Handle h -> m a) -> m a +withCreateFile hfs path k = fst <$> generalBracket acquire release k + where + acquire = FS.hOpen hfs path (FS.WriteMode MustBeNew) + release h = \case + ExitCaseSuccess _ -> FS.hClose hfs h + ExitCaseException e -> do + FS.hClose hfs h + `finally` + FS.removeFile hfs path + `finally` + throwIO e + ExitCaseAbort -> do + FS.hClose hfs h + `finally` + FS.removeFile hfs path + +{------------------------------------------------------------------------------- + Opening handles +-------------------------------------------------------------------------------} + +-- | File open mode. The file is assumed to exist already on disk. +data Mode = Read | ReadWrite | Write | Append + deriving stock (Show, Eq) + +modeOpenMode :: Mode -> FS.OpenMode +modeOpenMode = \case + Read -> FS.ReadMode + ReadWrite -> FS.ReadWriteMode MustExist + Write -> FS.WriteMode MustExist + Append -> FS.AppendMode MustExist + +{-# INLINABLE openHandle #-} +-- | Open a handle to a 'File'. +-- +-- REF: the resulting handle should be closed when it is no longer used. +-- +-- ASYNC: this should be called with asynchronous exceptions masked because it +-- allocates/creates resources. +openHandle :: HasFS m h -> Ref (File m) -> Mode -> m (Handle h) +openHandle hfs (DeRef file) mode = FS.hOpen hfs (filePath file) (modeOpenMode mode) diff --git a/src/Database/LSMTree/Internal/Snapshot.hs b/src/Database/LSMTree/Internal/Snapshot.hs index f0ee78680..89801a421 100644 --- a/src/Database/LSMTree/Internal/Snapshot.hs +++ b/src/Database/LSMTree/Internal/Snapshot.hs @@ -29,7 +29,6 @@ import Control.Concurrent.Class.MonadMVar.Strict import Control.Concurrent.Class.MonadSTM (MonadSTM) import Control.DeepSeq (NFData (..)) import Control.Exception (assert) -import Control.Monad (void) import Control.Monad.Class.MonadST (MonadST) import Control.Monad.Class.MonadThrow (MonadMask) import Control.Monad.Primitive (PrimMonad) @@ -42,6 +41,7 @@ import Database.LSMTree.Internal.Config import Database.LSMTree.Internal.CRC32C (checkCRC) import qualified Database.LSMTree.Internal.CRC32C as CRC import Database.LSMTree.Internal.Entry +import Database.LSMTree.Internal.FS.File import Database.LSMTree.Internal.Lookup (ResolveSerialisedValue) import qualified Database.LSMTree.Internal.Merge as Merge import Database.LSMTree.Internal.MergeSchedule @@ -65,7 +65,6 @@ import qualified Database.LSMTree.Internal.WriteBufferReader as WBR import qualified Database.LSMTree.Internal.WriteBufferWriter as WBW import qualified System.FS.API as FS import System.FS.API (HasFS, (<.>), ()) -import qualified System.FS.API.Lazy as FSL import qualified System.FS.BlockIO.API as FS import System.FS.BlockIO.API (HasBlockIO) @@ -334,11 +333,15 @@ openWriteBuffer reg resolve hfs hbio uc activeDir snapWriteBufferPaths = do activeWriteBufferNumber <- uniqueToInt <$> incrUniqCounter uc let activeWriteBufferBlobPath = getActiveDir activeDir FS.mkFsPath [show activeWriteBufferNumber] <.> "wbblobs" - copyFile reg hfs hbio (writeBufferBlobPath snapWriteBufferPaths) activeWriteBufferBlobPath + writeBufferBlobFile <- + withRollback reg + (copyFile hfs (writeBufferBlobPath snapWriteBufferPaths) activeWriteBufferBlobPath) + releaseRef writeBufferBlobs <- withRollback reg - (WBB.open hfs activeWriteBufferBlobPath FS.AllowExisting) + (WBB.open hfs writeBufferBlobFile) releaseRef + delayedCommit reg (releaseRef writeBufferBlobFile) -- Read write buffer key/ops let kOpsPath = ForKOps (writeBufferKOpsPath snapWriteBufferPaths) writeBuffer <- @@ -553,32 +556,3 @@ hardLink reg hfs hbio sourcePath targetPath = do withRollback_ reg (FS.createHardLink hbio sourcePath targetPath) (FS.removeFile hfs targetPath) - -{------------------------------------------------------------------------------- - Copy file --------------------------------------------------------------------------------} - -{-# SPECIALISE - copyFile :: - ActionRegistry IO - -> HasFS IO h - -> HasBlockIO IO h - -> FS.FsPath - -> FS.FsPath - -> IO () - #-} --- | @'copyFile' reg hfs hbio source target@ copies the @source@ path to the @target@ path. -copyFile :: - (MonadMask m, PrimMonad m) - => ActionRegistry m - -> HasFS m h - -> HasBlockIO m h - -> FS.FsPath - -> FS.FsPath - -> m () -copyFile reg hfs _hbio sourcePath targetPath = - flip (withRollback_ reg) (FS.removeFile hfs targetPath) $ - FS.withFile hfs sourcePath FS.ReadMode $ \sourceHandle -> - FS.withFile hfs targetPath (FS.WriteMode FS.MustBeNew) $ \targetHandle -> do - bs <- FSL.hGetAll hfs sourceHandle - void $ FSL.hPutAll hfs targetHandle bs diff --git a/src/Database/LSMTree/Internal/WriteBufferBlobs.hs b/src/Database/LSMTree/Internal/WriteBufferBlobs.hs index d3e1ec271..0a9c4c3a5 100644 --- a/src/Database/LSMTree/Internal/WriteBufferBlobs.hs +++ b/src/Database/LSMTree/Internal/WriteBufferBlobs.hs @@ -24,6 +24,8 @@ -- module Database.LSMTree.Internal.WriteBufferBlobs ( WriteBufferBlobs (..), + unsafeNew, + unsafeOpen, new, open, addBlob, @@ -44,6 +46,7 @@ import Database.LSMTree.Internal.BlobFile import qualified Database.LSMTree.Internal.BlobFile as BlobFile import Database.LSMTree.Internal.BlobRef (RawBlobRef (..), WeakBlobRef (..)) +import Database.LSMTree.Internal.FS.File import Database.LSMTree.Internal.Serialise import qualified System.FS.API as FS import System.FS.API (HasFS) @@ -126,6 +129,29 @@ instance NFData h => NFData (WriteBufferBlobs m h) where instance RefCounted m (WriteBufferBlobs m h) where getRefCounter = writeBufRefCounter +{-# SPECIALISE unsafeNew :: HasFS IO h -> FS.FsPath -> IO (Ref (WriteBufferBlobs IO h)) #-} +-- | TODO: replace at use sites by 'new', and then remove this function. +unsafeNew :: + (PrimMonad m, MonadMask m) + => HasFS m h + -> FS.FsPath + -> m (Ref (WriteBufferBlobs m h)) +unsafeNew fs blobFileName = unsafeOpen fs blobFileName FS.MustBeNew + +{-# SPECIALISE unsafeOpen :: HasFS IO h -> FS.FsPath -> FS.AllowExisting -> IO (Ref (WriteBufferBlobs IO h)) #-} +-- | TODO: replace at use sites by 'open', and then remove this function. +unsafeOpen :: + (PrimMonad m, MonadMask m) + => HasFS m h + -> FS.FsPath + -> FS.AllowExisting + -> m (Ref (WriteBufferBlobs m h)) +unsafeOpen fs blobFileName blobFileAllowExisting = do + bracketOnError + (unsafeOpenBlobFile fs blobFileName (FS.ReadWriteMode blobFileAllowExisting)) + releaseRef + (fromBlobFile fs) + {-# SPECIALISE new :: HasFS IO h -> FS.FsPath -> IO (Ref (WriteBufferBlobs IO h)) #-} -- | Create a new 'WriteBufferBlobs' with a new file. -- @@ -138,10 +164,15 @@ new :: => HasFS m h -> FS.FsPath -> m (Ref (WriteBufferBlobs m h)) -new fs blobFileName = open fs blobFileName FS.MustBeNew +new fs blobFileName = + bracketOnError + (newBlobFile fs blobFileName ReadWrite) + releaseRef + (fromBlobFile fs) -{-# SPECIALISE open :: HasFS IO h -> FS.FsPath -> FS.AllowExisting -> IO (Ref (WriteBufferBlobs IO h)) #-} --- | Open a `WriteBufferBlobs` file and sets the file pointer to the end of the file. +{-# SPECIALISE open :: HasFS IO h -> Ref (File IO) -> IO (Ref (WriteBufferBlobs IO h)) #-} +-- | Open a 'WriteBufferBlobs' file from an existing file and set the file +-- pointer to the end of the file. -- -- REF: the resulting reference must be released once it is no longer used. -- @@ -150,19 +181,18 @@ new fs blobFileName = open fs blobFileName FS.MustBeNew open :: (PrimMonad m, MonadMask m) => HasFS m h - -> FS.FsPath - -> FS.AllowExisting + -> Ref (File m) -> m (Ref (WriteBufferBlobs m h)) -open fs blobFileName blobFileAllowExisting = do +open fs file = do -- Must use read/write mode because we write blobs when adding, but -- we can also be asked to retrieve blobs at any time. bracketOnError - (openBlobFile fs blobFileName (FS.ReadWriteMode blobFileAllowExisting)) + (openBlobFile fs file ReadWrite) releaseRef (fromBlobFile fs) {-# SPECIALISE fromBlobFile :: HasFS IO h -> Ref (BlobFile IO h) -> IO (Ref (WriteBufferBlobs IO h)) #-} --- | Make a `WriteBufferBlobs` from a `BlobFile` and set the file pointer to the +-- | Make a 'WriteBufferBlobs' from a 'BlobFile' and set the file pointer to the -- end of the file. -- -- REF: the resulting reference must be released once it is no longer used. diff --git a/test/Main.hs b/test/Main.hs index 5ee6851e8..2348f14b7 100644 --- a/test/Main.hs +++ b/test/Main.hs @@ -13,6 +13,7 @@ import qualified Test.Database.LSMTree.Internal.BloomFilter import qualified Test.Database.LSMTree.Internal.Chunk import qualified Test.Database.LSMTree.Internal.CRC32C import qualified Test.Database.LSMTree.Internal.Entry +import qualified Test.Database.LSMTree.Internal.FS.File import qualified Test.Database.LSMTree.Internal.Index.Compact import qualified Test.Database.LSMTree.Internal.Index.Ordinary import qualified Test.Database.LSMTree.Internal.Lookup @@ -59,6 +60,7 @@ main = do , Test.Database.LSMTree.Internal.Chunk.tests , Test.Database.LSMTree.Internal.CRC32C.tests , Test.Database.LSMTree.Internal.Entry.tests + , Test.Database.LSMTree.Internal.FS.File.tests , Test.Database.LSMTree.Internal.Index.Compact.tests , Test.Database.LSMTree.Internal.Index.Ordinary.tests , Test.Database.LSMTree.Internal.Lookup.tests