packages feed

lsm-tree-1.0.0.1: src-core/Database/LSMTree/Internal/Merge.hs

{-# OPTIONS_HADDOCK not-home #-}

-- | The 'Merge' type and its functions are not intended for concurrent use.
-- Concurrent access should therefore be sequentialised using a suitable
-- concurrency primitive, such as an 'MVar'.
--
module Database.LSMTree.Internal.Merge (
    Merge (..)
  , MergeType (..)
  , IsMergeType (..)
  , LevelMergeType (..)
  , TreeMergeType (..)
  , MergeState (..)
  , RunParams (..)
  , new
  , abort
  , complete
  , stepsToCompletion
  , stepsToCompletionCounted
  , StepResult (..)
  , steps
  , mergeRunParams
  ) where

import           Control.DeepSeq (NFData (..))
import           Control.Exception (assert)
import           Control.Monad (when)
import           Control.Monad.Class.MonadST (MonadST)
import           Control.Monad.Class.MonadSTM (MonadSTM (..))
import           Control.Monad.Class.MonadThrow (MonadMask, MonadThrow)
import           Control.Monad.Primitive (PrimState)
import           Control.RefCount
import           Data.Primitive.MutVar
import           Data.Traversable (for)
import qualified Data.Vector as V
import           Database.LSMTree.Internal.BlobRef (RawBlobRef)
import qualified Database.LSMTree.Internal.BloomFilter as Bloom
import           Database.LSMTree.Internal.Entry
import           Database.LSMTree.Internal.Readers (Readers)
import qualified Database.LSMTree.Internal.Readers as Readers
import           Database.LSMTree.Internal.Run (Run)
import qualified Database.LSMTree.Internal.Run as Run
import           Database.LSMTree.Internal.RunBuilder (RunBuilder, RunParams)
import qualified Database.LSMTree.Internal.RunBuilder as Builder
import qualified Database.LSMTree.Internal.RunReader as Reader
import           Database.LSMTree.Internal.Serialise
import qualified System.FS.API as FS
import           System.FS.API (HasFS)
import           System.FS.BlockIO.API (HasBlockIO)

-- | An in-progress incremental k-way merge of 'Run's.
--
-- Since we always resolve all entries of the same key in one go, there is no
-- need to store incompletely-resolved entries.
data Merge t m h = Merge {
      mergeType        :: !t
      -- | We also store @isLastLevel mergeType@ and @isUnion mergeType@ here to
      -- avoid recomputing them it again and again for each call to 'steps',
      -- which would also add an 'IsMergeType' constraint to large parts of the
      -- interface.
    , mergeIsLastLevel :: !Bool
    , mergeIsUnion     :: !Bool
    , mergeResolve     :: !ResolveSerialisedValue
    , mergeReaders     :: {-# UNPACK #-} !(Readers m h)
    , mergeBuilder     :: !(RunBuilder m h)
      -- | The result of the latest call to 'steps'. This is used to determine
      -- whether a merge can be 'complete'd.
    , mergeState       :: !(MutVar (PrimState m) MergeState)
    , mergeHasFS       :: !(HasFS m h)
    , mergeHasBlockIO  :: !(HasBlockIO m h)
    }

mergeRunParams :: Merge t m h -> RunParams
mergeRunParams = Builder.runBuilderParams . mergeBuilder

-- | The current state of the merge.
data MergeState =
    -- | There is still merging work to be done
    Merging
    -- | There is no more merging work to be done, but the merge still has to be
    -- completed to yield a new run.
  | MergingDone
    -- | A run was yielded as the result of a merge. The merge is implicitly
    -- closed.
  | Completed
    -- | The merge was closed before it was completed.
  | Closed

-- | Merges can either exist on a level of the LSM, or be a union merge of two
-- tables.
class IsMergeType t where
  -- | A last level merge behaves differently from a mid-level merge: last level
  -- merges can actually remove delete operations, whereas mid-level merges must
  -- preserve them.
  isLastLevel :: t -> Bool
  -- | Union merges follow the semantics of @Data.Map.unionWith (<>)@. Since the
  -- input runs are semantically treated like @Data.Map@s, deletes are ignored
  -- and inserts act like mupserts, so they need to be merged monoidally using
  -- 'resolveValue'.
  isUnion :: t -> Bool

-- | Fully general merge type, mainly useful for testing.
data MergeType = MergeTypeMidLevel | MergeTypeLastLevel | MergeTypeUnion
  deriving stock (Eq, Show)

instance NFData MergeType where
  rnf MergeTypeMidLevel  = ()
  rnf MergeTypeLastLevel = ()
  rnf MergeTypeUnion     = ()

instance IsMergeType MergeType where
  isLastLevel = \case
      MergeTypeMidLevel  -> False
      MergeTypeLastLevel -> True
      MergeTypeUnion     -> True
  isUnion = \case
      MergeTypeMidLevel  -> False
      MergeTypeLastLevel -> False
      MergeTypeUnion     -> True

-- | Different types of merges created as part of a regular (non-union) level.
--
-- A last level merge behaves differently from a mid-level merge: last level
-- merges can actually remove delete operations, whereas mid-level merges must
-- preserve them. This is orthogonal to the 'MergePolicy'.
data LevelMergeType = MergeMidLevel | MergeLastLevel
  deriving stock (Eq, Show)

instance NFData LevelMergeType where
  rnf MergeMidLevel  = ()
  rnf MergeLastLevel = ()

instance IsMergeType LevelMergeType where
  isLastLevel = \case
      MergeMidLevel  -> False
      MergeLastLevel -> True
  isUnion = const False

-- | Different types of merges created as part of the merging tree.
data TreeMergeType = MergeLevel | MergeUnion
  deriving stock (Eq, Show)

instance NFData TreeMergeType where
  rnf MergeLevel = ()
  rnf MergeUnion = ()

instance IsMergeType TreeMergeType where
  isLastLevel = const True
  isUnion = \case
      MergeLevel -> False
      MergeUnion -> True

{-# SPECIALISE new ::
     IsMergeType t
  => HasFS IO h
  -> HasBlockIO IO h
  -> Bloom.Salt
  -> RunParams
  -> t
  -> ResolveSerialisedValue
  -> Run.RunFsPaths
  -> V.Vector (Ref (Run IO h))
  -> IO (Maybe (Merge t IO h)) #-}
-- | Returns 'Nothing' if no input 'Run' contains any entries.
-- The list of runs should be sorted from new to old.
new ::
     (IsMergeType t, MonadMask m, MonadSTM m, MonadST m)
  => HasFS m h
  -> HasBlockIO m h
  -> Bloom.Salt
  -> RunParams
  -> t
  -> ResolveSerialisedValue
  -> Run.RunFsPaths
  -> V.Vector (Ref (Run m h))
  -> m (Maybe (Merge t m h))
new hfs hbio salt runParams mergeType mergeResolve targetPaths runs = do
    let sources = Readers.FromRun <$> V.toList runs
    mreaders <- Readers.new mergeResolve Readers.NoOffsetKey sources
    -- TODO: Exception safety! If Readers.new fails after already creating some
    -- run readers, or Builder.new fails, the run readers will stay open,
    -- holding handles of the input runs' files.
    for mreaders $ \mergeReaders -> do
      -- calculate upper bounds based on input runs
      let numEntries = V.foldMap' Run.size runs
      mergeBuilder <- Builder.new hfs hbio salt runParams targetPaths numEntries
      mergeState <- newMutVar $! Merging
      pure Merge {
          mergeIsLastLevel = isLastLevel mergeType
        , mergeIsUnion = isUnion mergeType
        , mergeHasFS = hfs
        , mergeHasBlockIO = hbio
        , ..
        }

{-# SPECIALISE abort :: Merge t IO (FS.Handle h) -> IO () #-}
-- | This function should be called when discarding a 'Merge' before it
-- was done (i.e. returned 'MergeComplete'). This removes the incomplete files
-- created for the new run so far and avoids leaking file handles.
--
-- Once it has been called, do not use the 'Merge' any more!
abort :: (MonadMask m, MonadSTM m, MonadST m) => Merge t m h -> m ()
abort Merge {..} = do
    readMutVar mergeState >>= \case
      Merging -> do
        Readers.close mergeReaders
        Builder.close mergeBuilder
      MergingDone -> do
        -- the readers are already drained, therefore closed
        Builder.close mergeBuilder
      Completed ->
        assert False $ pure ()
      Closed ->
        assert False $ pure ()
    writeMutVar mergeState $! Closed

{-# SPECIALISE complete ::
     RefCtx
  -> Merge t IO h
  -> IO (Ref (Run IO h)) #-}
-- | Complete a 'Merge', returning a new 'Run' as the result of merging the
-- input runs.
--
-- All resources held by the merge are released, so do not use the it any more!
--
-- This function will /not/ do any merging work if there is any remaining. That
-- is, if not enough 'steps' were performed to exhaust the input 'Readers', this
-- function will throw an error.
--
-- Returns an error if the merge was not yet done, if it was already completed
-- before, or if it was already closed.
--
-- Note: this function creates new 'Run' resources, so it is recommended to run
-- this function with async exceptions masked. Otherwise, these resources can
-- leak. And it must eventually be released with 'releaseRef'.
--
complete ::
     (MonadSTM m, MonadST m, MonadMask m)
  => RefCtx
  -> Merge t m h
  -> m (Ref (Run m h))
complete refCtx Merge{..} = do
    readMutVar mergeState >>= \case
      Merging -> error "complete: Merge is not done"
      MergingDone -> do
        -- the readers are already drained, therefore closed
        r <- Run.fromBuilder refCtx mergeBuilder
        writeMutVar mergeState $! Completed
        pure r
      Completed -> error "complete: Merge is already completed"
      Closed -> error "complete: Merge is closed"

{-# SPECIALISE stepsToCompletion ::
     RefCtx
  -> Merge t IO h
  -> Int
  -> IO (Ref (Run IO h)) #-}
-- | Like 'steps', but calling 'complete' once the merge is finished.
--
-- Note: run with async exceptions masked. See 'complete'.
stepsToCompletion ::
      (MonadMask m, MonadSTM m, MonadST m)
   => RefCtx
   -> Merge t m h
   -> Int
   -> m (Ref (Run m h))
stepsToCompletion refCtx m stepBatchSize = go
  where
    go = do
      steps m stepBatchSize >>= \case
        (_, MergeInProgress) -> go
        (_, MergeDone)       -> complete refCtx m

{-# SPECIALISE stepsToCompletionCounted ::
     RefCtx
  -> Merge t IO h
  -> Int
  -> IO (Int, Ref (Run IO h)) #-}
-- | Like 'steps', but calling 'complete' once the merge is finished.
--
-- Note: run with async exceptions masked. See 'complete'.
stepsToCompletionCounted ::
     (MonadMask m, MonadSTM m, MonadST m)
  => RefCtx
  -> Merge t m h
  -> Int
  -> m (Int, Ref (Run m h))
stepsToCompletionCounted refCtx m stepBatchSize = go 0
  where
    go !stepsSum = do
      steps m stepBatchSize >>= \case
        (n, MergeInProgress) -> go (stepsSum + n)
        (n, MergeDone)       -> let !stepsSum' = stepsSum + n
                                in (stepsSum',) <$> complete refCtx m

data StepResult = MergeInProgress | MergeDone
  deriving stock Eq

stepsInvariant :: Int -> (Int, StepResult) -> Bool
stepsInvariant requestedSteps = \case
    (n, MergeInProgress) -> n >= requestedSteps
    _                    -> True

{-# SPECIALISE steps ::
     Merge t IO h
  -> Int
  -> IO (Int, StepResult) #-}
-- | Do at least a given number of steps of merging. Each step reads a single
-- entry, then either resolves the previous entry with the new one or writes it
-- out to the run being created. Since we always finish resolving a key we
-- started, we might do slightly more work than requested.
--
-- Returns the number of input entries read, which is guaranteed to be at least
-- as many as requested (unless the merge is complete).
--
-- Returns an error if the merge was already completed or closed.
steps ::
     (MonadMask m, MonadSTM m, MonadST m)
  => Merge t m h
  -> Int  -- ^ How many input entries to consume (at least)
  -> m (Int, StepResult)
steps m@Merge {..} requestedSteps = assertStepsInvariant <$> do
    -- TODO: ideally, we would not check whether the merge was already done on
    -- every call to @steps@. It is important for correctness, however, that we
    -- do not call @steps@ on a merge when it was already done. It is not yet
    -- clear whether our (upcoming) implementation of scheduled merges is going
    -- to satisfy this precondition when it calls @steps@, so for now we do the
    -- check.
    readMutVar mergeState >>= \case
      Merging     -> if mergeIsUnion then doStepsUnion m requestedSteps
                                     else doStepsLevel m requestedSteps
      MergingDone -> pure (0, MergeDone)
      Completed   -> error "steps: Merge is completed"
      Closed      -> error "steps: Merge is closed"
  where
    assertStepsInvariant res = assert (stepsInvariant requestedSteps res) res

{-# SPECIALISE doStepsLevel ::
     Merge t IO h
  -> Int
  -> IO (Int, StepResult) #-}
doStepsLevel ::
     (MonadMask m, MonadSTM m, MonadST m)
  => Merge t m h
  -> Int  -- ^ How many input entries to consume (at least)
  -> m (Int, StepResult)
doStepsLevel m@Merge {..} requestedSteps = go 0
  where
    go !n
      | n >= requestedSteps =
          pure (n, MergeInProgress)
      | otherwise = do
          (key, entry, hasMore) <- Readers.pop mergeResolve mergeReaders
          case hasMore of
            Readers.HasMore ->
              handleEntry (n + 1) key entry
            Readers.Drained -> do
              -- no future entries, no previous entry to resolve, just write!
              writeReaderEntry m key entry
              writeMutVar mergeState $! MergingDone
              pure (n + 1, MergeDone)

    handleEntry !n !key (Reader.Entry (Upsert v)) =
        -- resolve small mupsert vals with the following entries of the same key
        handleMupdate n key v
    handleEntry !n !key (Reader.EntryOverflow (Upsert v) _ len overflowPages) =
        -- resolve large mupsert vals with following entries of the same key
        handleMupdate n key (Reader.appendOverflow len overflowPages v)
    handleEntry !n !key entry = do
        -- otherwise, we can just drop all following entries of same key
        writeReaderEntry m key entry
        dropRemaining n key

    -- the value is from a mupsert, complete (not just a prefix)
    handleMupdate !n !key !v = do
        nextKey <- Readers.peekKey mergeReaders
        if nextKey /= key
          then do
            -- resolved all entries for this key, write it
            writeSerialisedEntry m key (Upsert v)
            go n
          else do
            (_, nextEntry, hasMore) <- Readers.pop mergeResolve mergeReaders
            -- for resolution, we need the full second value to be present
            let resolved = combine mergeResolve
                             (Upsert v)
                             (Reader.toFullEntry nextEntry)
            case hasMore of
              Readers.HasMore -> case resolved of
                Upsert v' ->
                  -- still a mupsert, keep resolving
                  handleMupdate (n + 1) key v'
                _ -> do
                  -- done with this key, now the remaining entries are obsolete
                  writeSerialisedEntry m key resolved
                  dropRemaining (n + 1) key
              Readers.Drained -> do
                writeSerialisedEntry m key resolved
                writeMutVar mergeState $! MergingDone
                pure (n + 1, MergeDone)

    dropRemaining !n !key = do
        (dropped, hasMore) <- Readers.dropWhileKey mergeResolve mergeReaders key
        case hasMore of
          Readers.HasMore -> go (n + dropped)
          Readers.Drained -> do
            writeMutVar mergeState $! MergingDone
            pure (n + dropped, MergeDone)

{-# SPECIALISE doStepsUnion ::
     Merge t IO h
  -> Int
  -> IO (Int, StepResult) #-}
doStepsUnion ::
     (MonadMask m, MonadSTM m, MonadST m)
  => Merge t m h
  -> Int  -- ^ How many input entries to consume (at least)
  -> m (Int, StepResult)
doStepsUnion m@Merge {..} requestedSteps = go 0
  where
    go !n
      | n >= requestedSteps =
          pure (n, MergeInProgress)
      | otherwise = do
          (key, entry, hasMore) <- Readers.pop mergeResolve mergeReaders
          handleEntry (n + 1) key entry hasMore

    -- Similar to 'handleMupdate' in 'stepsLevel', but here we have to combine
    -- all entries monoidally, so there are no obsolete/overwritten entries
    -- that we could skip.
    --
    -- TODO(optimisation): If mergeResolve is const, we could skip all remaining
    -- entries for the key. Unfortunately, we can't inspect the function. This
    -- would require encoding it as something like `Const | Resolve (_ -> _ ->
    -- _)`.
    handleEntry !n !key !entry Readers.Drained = do
        -- no future entries, no previous entry to resolve, just write!
        writeReaderEntry m key entry
        writeMutVar mergeState $! MergingDone
        pure (n, MergeDone)

    handleEntry !n !key !entry Readers.HasMore = do
        nextKey <- Readers.peekKey mergeReaders
        if nextKey /= key
          then do
            -- resolved all entries for this key, write it
            writeReaderEntry m key entry
            go n
          else do
            (_, nextEntry, hasMore) <- Readers.pop mergeResolve mergeReaders
            -- for resolution, we need the full second value to be present
            let resolved = combineUnion mergeResolve
                             (Reader.toFullEntry entry)
                             (Reader.toFullEntry nextEntry)
            handleEntry (n + 1) key (Reader.Entry resolved) hasMore

{-# INLINE writeReaderEntry #-}
writeReaderEntry ::
     (MonadSTM m, MonadST m, MonadThrow m)
  => Merge t m h
  -> SerialisedKey
  -> Reader.Entry m h
  -> m ()
writeReaderEntry m key (Reader.Entry entryFull) =
      -- Small entry.
      -- Note that this small entry could be the only one on the page. We only
      -- care about it being small, not single-entry, since it could still end
      -- up sharing a page with other entries in the merged run.
      -- TODO(optimise): This doesn't fully exploit the case where there is a
      -- single page small entry on the page which again ends up as the only
      -- entry of a page (which would for example happen a lot if most entries
      -- have 2k-4k bytes). In that case we could have copied the RawPage
      -- (but we find out too late to easily exploit it).
      writeSerialisedEntry m key entryFull
writeReaderEntry m key entry@(Reader.EntryOverflow prefix page _ overflowPages)
  | InsertWithBlob {} <- prefix =
      assert (shouldWriteEntry m prefix) $ do -- large, can't be delete
        -- has blob, we can't just copy the first page, fall back
        -- we simply append the overflow pages to the value
        Builder.addKeyOp (mergeBuilder m) key (Reader.toFullEntry entry)
        -- TODO(optimise): This copies the overflow pages unnecessarily.
        -- We could extend the RunBuilder API to allow to either:
        -- 1. write an Entry (containing the value prefix) + [RawOverflowPage]
        -- 2. write a RawPage + SerialisedBlob + [RawOverflowPage], rewriting
        --      the raw page's blob offset (slightly faster, but a bit hacky)
  | otherwise =
      assert (shouldWriteEntry m prefix) $  -- large, can't be delete
        -- no blob, directly copy all pages as they are
        Builder.addLargeSerialisedKeyOp (mergeBuilder m) key page overflowPages

{-# INLINE writeSerialisedEntry #-}
writeSerialisedEntry ::
     (MonadSTM m, MonadST m, MonadThrow m)
  => Merge t m h
  -> SerialisedKey
  -> Entry SerialisedValue (RawBlobRef m h)
  -> m ()
writeSerialisedEntry m key entry =
    when (shouldWriteEntry m entry) $
      Builder.addKeyOp (mergeBuilder m) key entry

-- On the last level we could also turn Upsert into Insert, but no need to
-- complicate things.
shouldWriteEntry :: Merge t m h -> Entry v b -> Bool
shouldWriteEntry m Delete = not (mergeIsLastLevel m)
shouldWriteEntry _ _      = True