packages feed

lsm-tree-1.0.0.0: src-core/Database/LSMTree/Internal/WriteBufferWriter.hs

{-# OPTIONS_HADDOCK not-home #-}

module Database.LSMTree.Internal.WriteBufferWriter
  ( writeWriteBuffer
  ) where

import           Control.Exception (assert)
import           Control.Monad (void, when)
import           Control.Monad.Class.MonadST (MonadST (..))
import qualified Control.Monad.Class.MonadST as ST
import           Control.Monad.Class.MonadSTM (MonadSTM (..))
import           Control.Monad.Class.MonadThrow (MonadThrow)
import           Control.Monad.Primitive (PrimMonad (..))
import           Control.Monad.ST (ST)
import           Control.RefCount (Ref)
import           Data.Foldable (for_)
import           Data.Maybe (maybeToList)
import           Data.Primitive.PrimVar (PrimVar, newPrimVar)
import           Data.Word (Word64)
import           Database.LSMTree.Internal.BlobFile (BlobSpan)
import           Database.LSMTree.Internal.BlobRef (RawBlobRef)
import           Database.LSMTree.Internal.ChecksumHandle (ChecksumHandle,
                     closeHandle, copyBlob, dropCache, makeHandle, readChecksum,
                     writeRawOverflowPages, writeRawPage)
import qualified Database.LSMTree.Internal.CRC32C as CRC
import           Database.LSMTree.Internal.Entry (Entry)
import           Database.LSMTree.Internal.PageAcc (PageAcc)
import qualified Database.LSMTree.Internal.PageAcc as PageAcc
import qualified Database.LSMTree.Internal.PageAcc1 as PageAcc
import           Database.LSMTree.Internal.Paths (ForBlob (..), ForKOps (..),
                     WriteBufferFsPaths, toChecksumsFileForWriteBufferFiles,
                     writeBufferBlobPath, writeBufferChecksumsPath,
                     writeBufferKOpsPath)
import           Database.LSMTree.Internal.RawOverflowPage (RawOverflowPage)
import           Database.LSMTree.Internal.RawPage (RawPage)
import           Database.LSMTree.Internal.Serialise (SerialisedKey,
                     SerialisedValue)
import           Database.LSMTree.Internal.WriteBuffer (WriteBuffer)
import qualified Database.LSMTree.Internal.WriteBuffer as WB
import           Database.LSMTree.Internal.WriteBufferBlobs (WriteBufferBlobs)
import qualified Database.LSMTree.Internal.WriteBufferBlobs as WBB
import qualified System.FS.API as FS
import           System.FS.API (HasFS)
import qualified System.FS.BlockIO.API as FS
import           System.FS.BlockIO.API (HasBlockIO)


{-# SPECIALISE
  writeWriteBuffer ::
       HasFS IO h
    -> HasBlockIO IO h
    -> WriteBufferFsPaths
    -> WriteBuffer
    -> Ref (WriteBufferBlobs IO h)
    -> IO ()
  #-}
-- | Write a 'WriteBuffer' to disk.
writeWriteBuffer ::
     (MonadSTM m, MonadST m, MonadThrow m)
  => HasFS m h
  -> HasBlockIO m h
  -> WriteBufferFsPaths
  -> WriteBuffer
  -> Ref (WriteBufferBlobs m h)
  -> m ()
writeWriteBuffer hfs hbio fsPaths buffer blobs = do
  writer <- new hfs hbio fsPaths
  for_ (WB.toList buffer) $ \(key, op) ->
    -- TODO: The fmap entry here reallocates even when there are no blobs.
    addKeyOp writer key (WBB.mkRawBlobRef blobs <$> op)
  void $ unsafeFinalise True writer

-- | The in-memory representation of an LSM 'WriteBuffer' that is in the process of being serialised to disk.
data WriteBufferWriter m h = WriteBufferWriter
  { -- | The file system paths for all the files used by the serialised write buffer.
    writerFsPaths    :: !WriteBufferFsPaths,
    -- | The page accumulator.
    writerPageAcc    :: !(PageAcc (PrimState m)),
    -- | The byte offset within the blob file for the next blob to be written.
    writerBlobOffset :: !(PrimVar (PrimState m) Word64),
    -- | The (write mode) file handles.
    writerKOpsHandle :: !(ForKOps (ChecksumHandle (PrimState m) h)),
    writerBlobHandle :: !(ForBlob (ChecksumHandle (PrimState m) h)),
    writerHasFS      :: !(HasFS m h),
    writerHasBlockIO :: !(HasBlockIO m h)
  }

{-# SPECIALISE
  new ::
       HasFS IO h
    -> HasBlockIO IO h
    -> WriteBufferFsPaths
    -> IO (WriteBufferWriter IO h)
  #-}
-- | Create a 'WriteBufferWriter' to start serialising a 'WriteBuffer'.
--
-- See 'Database.LSMTree.Internal.RunBuilder.new'.
--
-- NOTE: 'new' assumes that the directory passed via 'WriteBufferFsPaths' exists.
new ::
     (MonadST m, MonadSTM m)
  => HasFS m h
  -> HasBlockIO m h
  -> WriteBufferFsPaths
  -> m (WriteBufferWriter m h)
new hfs hbio fsPaths = do
  writerPageAcc <- ST.stToIO PageAcc.newPageAcc
  writerBlobOffset <- newPrimVar 0
  writerKOpsHandle <- ForKOps <$> makeHandle hfs (writeBufferKOpsPath fsPaths)
  writerBlobHandle <- ForBlob <$> makeHandle hfs (writeBufferBlobPath fsPaths)
  pure WriteBufferWriter
    { writerFsPaths    = fsPaths,
      writerHasFS      = hfs,
      writerHasBlockIO = hbio,
      ..
    }

{-# SPECIALISE
  unsafeFinalise ::
       Bool
    -> WriteBufferWriter IO h
    -> IO (HasFS IO h, HasBlockIO IO h, WriteBufferFsPaths)
  #-}
-- | Finalise an incremental 'WriteBufferWriter'.
--
-- Do /not/ use a 'WriteBufferWriter' after finalising it.
--
-- See 'Database.LSMTree.Internal.RunBuilder.unsafeFinalise'.
--
-- TODO: Ensure proper cleanup even in presence of exceptions.
unsafeFinalise ::
     (MonadST m, MonadSTM m, MonadThrow m)
  => Bool -- ^ drop caches
  -> WriteBufferWriter m h
  -> m (HasFS m h, HasBlockIO m h, WriteBufferFsPaths)
unsafeFinalise dropCaches WriteBufferWriter {..} = do
  -- write final bits
  mPage <- ST.stToIO $ flushPageIfNonEmpty writerPageAcc
  for_ mPage $ writeRawPage writerHasFS writerKOpsHandle
  kOpsChecksum <- traverse readChecksum writerKOpsHandle
  blobChecksum <- traverse readChecksum writerBlobHandle
  let checksums = toChecksumsFileForWriteBufferFiles (kOpsChecksum, blobChecksum)
  FS.withFile writerHasFS (writeBufferChecksumsPath writerFsPaths) (FS.WriteMode FS.MustBeNew) $ \h -> do
    CRC.writeChecksumsFile' writerHasFS h checksums
    FS.hDropCacheAll writerHasBlockIO h
  -- drop the KOps and blobs files from the cache if asked for
  when dropCaches $ do
    dropCache writerHasBlockIO (unForKOps writerKOpsHandle)
    dropCache writerHasBlockIO (unForBlob writerBlobHandle)
  closeHandle writerHasFS (unForKOps writerKOpsHandle)
  closeHandle writerHasFS (unForBlob writerBlobHandle)
  pure (writerHasFS, writerHasBlockIO, writerFsPaths)


{-# SPECIALISE
  addKeyOp ::
       WriteBufferWriter IO h
    -> SerialisedKey
    -> Entry SerialisedValue (RawBlobRef IO h)
    -> IO ()
  #-}
-- | See 'Database.LSMTree.Internal.RunBuilder.addKeyOp'.
addKeyOp ::
     (MonadST m, MonadSTM m, MonadThrow m)
  => WriteBufferWriter m h
  -> SerialisedKey
  -> Entry SerialisedValue (RawBlobRef m h)
  -> m ()
addKeyOp WriteBufferWriter{..} key op = do
  -- TODO: consider optimisation described in 'Database.LSMTree.Internal.RunBuilder.addKeyOp'.
  op' <- traverse (copyBlob writerHasFS writerBlobOffset writerBlobHandle) op
  if PageAcc.entryWouldFitInPage key op
    then do
      mPage <- ST.stToIO $ addSmallKeyOp writerPageAcc key op'
      for_ mPage $ writeRawPage writerHasFS writerKOpsHandle
    else do
      (pages, overflowPages) <- ST.stToIO $ addLargeKeyOp writerPageAcc key op'
      -- TODO: consider optimisation described in 'Database.LSMTree.Internal.RunBuilder.addKeyOp'.
      for_ pages $ writeRawPage writerHasFS writerKOpsHandle
      writeRawOverflowPages writerHasFS writerKOpsHandle overflowPages

-- | See 'Database.LSMTree.Internal.RunAcc.addSmallKeyOp'.
addSmallKeyOp ::
     PageAcc s
  -> SerialisedKey
  -> Entry SerialisedValue BlobSpan
  -> ST s (Maybe RawPage)
addSmallKeyOp pageAcc key op =
  assert (PageAcc.entryWouldFitInPage key op) $ do
    pageBoundaryNeeded <-
        -- Try adding the key/op to the page accumulator to see if it fits. If
        -- it does not fit, a page boundary is needed.
        not <$> PageAcc.pageAccAddElem pageAcc key op
    if pageBoundaryNeeded
      then do
        -- We need a page boundary. If the current page is empty then we have
        -- a boundary already, otherwise we need to flush the current page.
        mPage <- flushPageIfNonEmpty pageAcc
        -- The current page is now empty, either because it was already empty
        -- or because we just flushed it. Adding the new key/op to an empty
        -- page must now succeed, because we know it fits in a page.
        added <- PageAcc.pageAccAddElem pageAcc key op
        assert added $ pure mPage
      else do
        pure Nothing

-- | See 'Database.LSMTree.Internal.RunAcc.addLargeKeyOp'.
addLargeKeyOp ::
     PageAcc s
  -> SerialisedKey
  -> Entry SerialisedValue BlobSpan -- ^ the full value, not just a prefix
  -> ST s ([RawPage], [RawOverflowPage])
addLargeKeyOp pageAcc key op =
  assert (not (PageAcc.entryWouldFitInPage key op)) $ do
    -- If the existing page accumulator is non-empty, we flush it, since the
    -- new large key/op will need more than one page to itself.
    mPagePre <- flushPageIfNonEmpty pageAcc
    -- Make the new page and overflow pages. Add the span of pages to the index.
    let (page, overflowPages) = PageAcc.singletonPage key op
    -- Combine the results with anything we flushed before
    let !pages = selectPages mPagePre page
    pure (pages, overflowPages)

-- | Internal helper. See 'Database.LSMTree.Internal.RunAcc.flushPageIfNonEmpty'.
flushPageIfNonEmpty :: PageAcc s -> ST s (Maybe RawPage)
flushPageIfNonEmpty pageAcc = do
    keysCount <- PageAcc.keysCountPageAcc pageAcc
    if keysCount > 0
      then do
        -- Serialise the page and reset the accumulator
        page <- PageAcc.serialisePageAcc pageAcc
        PageAcc.resetPageAcc pageAcc
        pure $ Just page
      else pure Nothing

-- | Internal helper. See 'Database.LSMTree.Internal.RunAcc.selectPagesAndChunks'.
selectPages ::
     Maybe RawPage
  -> RawPage
  -> [RawPage]
selectPages mPagePre page =
  maybeToList mPagePre ++ [page]