lsm-tree-1.0.0.0: src-core/Database/LSMTree/Internal/Readers.hs
{-# OPTIONS_HADDOCK not-home #-}
-- | Multiple inputs (write buffers, runs) that are being read incrementally.
module Database.LSMTree.Internal.Readers (
Readers (..)
, OffsetKey (..)
, ReaderSource (..)
, ReadersMergeType (..)
, new
, close
, peekKey
, HasMore (..)
, pop
, dropWhileKey
-- * Internals
, Reader (..)
, ReaderNumber (..)
, ReadCtx (..)
) where
import Control.Monad (zipWithM)
import Control.Monad.Class.MonadST (MonadST)
import Control.Monad.Class.MonadSTM (MonadSTM (..))
import Control.Monad.Class.MonadThrow (MonadMask)
import Control.Monad.Primitive
import Control.RefCount
import Data.Function (on)
import Data.Functor ((<&>))
import Data.List.NonEmpty (nonEmpty)
import qualified Data.Map.Strict as Map
import Data.Maybe (catMaybes)
import Data.Primitive.MutVar
import Data.Traversable (for)
import Database.LSMTree.Internal.BlobRef (BlobSpan, RawBlobRef)
import Database.LSMTree.Internal.Entry (Entry (..))
import qualified Database.LSMTree.Internal.Entry as Entry
import Database.LSMTree.Internal.Index.CompactAcc (SMaybe (..),
smaybe)
import Database.LSMTree.Internal.Run (Run)
import Database.LSMTree.Internal.RunReader (OffsetKey (..),
RunReader (..))
import qualified Database.LSMTree.Internal.RunReader as RunReader
import Database.LSMTree.Internal.Serialise
import qualified Database.LSMTree.Internal.WriteBuffer as WB
import qualified Database.LSMTree.Internal.WriteBufferBlobs as WB
import qualified KMerge.Heap as Heap
import qualified System.FS.API as FS
-- | A collection of runs and write buffers being read from, yielding elements
-- in order. More precisely, that means first ordered by their key, then by the
-- input run they came from. This is important for resolving multiple entries
-- with the same key into one.
--
-- Construct with 'new', then keep calling 'pop'.
-- If aborting early, remember to call 'close'!
--
-- Creating a 'Readers' does not retain a reference to the input 'Run's or the
-- 'WriteBufferBlobs', but does retain an independent reference on their blob
-- files. It is not necessary to separately retain the 'Run's or the
-- 'WriteBufferBlobs' for correct use of the 'Readers'. There is one important
-- caveat however: to preserve the validity of 'BlobRef's then it is necessary
-- to separately retain a reference to the 'Run' or its 'BlobFile' to preserve
-- the validity of 'BlobRefs'.
--
-- TODO: do this more nicely by changing 'Reader' to preserve the 'BlobFile'
-- ref until it is explicitly closed, and also retain the 'BlobFile' from the
-- WBB and release all of these 'BlobFiles' once the 'Readers' is itself closed.
--
data Readers m h = Readers {
readersHeap :: !(Heap.MutableHeap (PrimState m) (ReadCtx m h))
-- | Since there is always one reader outside of the heap, we need to
-- store it separately. This also contains the next k\/op to yield, unless
-- all readers are drained, i.e. both:
-- 1. the reader inside the 'ReadCtx' is empty
-- 2. the heap is empty
, readersNext :: !(MutVar (PrimState m) (ReadCtx m h))
}
newtype ReaderNumber = ReaderNumber Int
deriving stock (Eq, Ord)
-- | Each heap element needs some more context than just the reader.
-- E.g. the 'Eq' instance we need to be able to access the first key to be read
-- in a pure way.
--
-- TODO(optimisation): We allocate this record for each k/op. This might be
-- avoidable, see ideas below.
data ReadCtx m h = ReadCtx {
-- We could avoid this using a more specialised mutable heap with separate
-- arrays for keys and values (or even each of their components).
-- Using an 'STRef' could avoid reallocating the record for every entry,
-- but that might not be straightforward to integrate with the heap.
readCtxHeadKey :: !SerialisedKey
, readCtxHeadEntry :: !(RunReader.Entry m h)
-- We could get rid of this by making 'LoserTree' stable (for which there
-- is a prototype already).
-- Alternatively, if we decide to have an invariant that the number in
-- 'RunFsPaths' is always higher for newer runs, then we could use that
-- in the 'Ord' instance.
, readCtxNumber :: !ReaderNumber
, readCtxReader :: !(Reader m h)
}
instance Eq (ReadCtx m h) where
(==) = (==) `on` (\r -> (readCtxHeadKey r, readCtxNumber r))
-- | Makes sure we resolve entries in the right order.
instance Ord (ReadCtx m h) where
compare = compare `on` (\r -> (readCtxHeadKey r, readCtxNumber r))
-- | An individual reader must be able to produce a sequence of pairs of
-- 'SerialisedKey' and 'RunReader.Entry', with ordered und unique keys.
--
-- TODO: This is slightly inelegant. This module could work generally for
-- anything that can produce elements, but currently is very specific to having
-- write buffer and run readers. Also, for run merging, no write buffer is
-- involved, but we still need to branch on this sum type.
-- A more general version is possible, but despite SPECIALISE-ing everything
-- showed ~100 bytes of extra allocations per entry that is read (which might be
-- avoidable with some tinkering).
data Reader m h =
-- | The list allows to incrementally read from the write buffer without
-- having to find the next entry in the Map again (requiring key
-- comparisons) or having to copy out all entries.
--
-- TODO: more efficient representation? benchmark!
ReadBuffer !(MutVar (PrimState m) [KOp m h])
| ReadRun !(RunReader m h)
-- | Recursively read from another reader. This requires keeping track of
-- its 'HasMore' status, since we should not try to read another entry from
-- it once it is drained.
--
-- We represent the recursive reader and 'HasMore' status together as a
-- 'Maybe' 'Readers'. The reason is subtle: once a 'Readers' becomes drained
-- it is immediately closed, after which the structure should not be used
-- anymore or you'd be using resources after they have been closed already.
--
-- TODO: maybe it's a slightly more ergonomic alternative to no close the
-- 'Readers' automatically.
| ReadReaders !ReadersMergeType !(SMaybe (Readers m h))
type KOp m h = (SerialisedKey, Entry SerialisedValue (RawBlobRef m h))
data ReaderSource m h =
FromWriteBuffer !WB.WriteBuffer !(Ref (WB.WriteBufferBlobs m h))
| FromRun !(Ref (Run m h))
-- | Recursive case, allowing to build a tree of readers for a merging tree.
| FromReaders !ReadersMergeType ![ReaderSource m h]
{-# SPECIALISE new ::
ResolveSerialisedValue
-> OffsetKey
-> [ReaderSource IO h]
-> IO (Maybe (Readers IO h)) #-}
new :: forall m h.
(MonadMask m, MonadST m, MonadSTM m)
=> ResolveSerialisedValue
-> OffsetKey
-> [ReaderSource m h]
-> m (Maybe (Readers m h))
new resolve !offsetKey sources = do
readers <- zipWithM (fromSource . ReaderNumber) [1..] sources
for (nonEmpty (catMaybes readers)) $ \xs -> do
(readersHeap, readCtx) <- Heap.newMutableHeap xs
readersNext <- newMutVar readCtx
pure Readers {..}
where
fromSource :: ReaderNumber -> ReaderSource m h -> m (Maybe (ReadCtx m h))
fromSource n src =
case src of
FromWriteBuffer wb wbblobs -> do
rs <- fromWB wb wbblobs
nextReadCtx resolve n rs
FromRun r -> do
rs <- ReadRun <$> RunReader.new offsetKey r
nextReadCtx resolve n rs
FromReaders mergeType nestedSources -> do
new resolve offsetKey nestedSources >>= \case
Nothing -> pure Nothing
Just rs -> nextReadCtx resolve n (ReadReaders mergeType (SJust rs))
fromWB :: WB.WriteBuffer -> Ref (WB.WriteBufferBlobs m h) -> m (Reader m h)
fromWB wb wbblobs = do
let kops = Map.toList $ filterWB $ WB.toMap wb
ReadBuffer <$> newMutVar (map convertBlobs kops)
where
-- TODO: this conversion involves quite a lot of allocation
convertBlobs :: (k, Entry v BlobSpan) -> (k, Entry v (RawBlobRef m h))
convertBlobs = fmap (fmap (WB.mkRawBlobRef wbblobs))
filterWB = case offsetKey of
NoOffsetKey -> id
OffsetKey k -> Map.dropWhileAntitone (< k)
{-# SPECIALISE close :: Readers IO (FS.Handle h) -> IO () #-}
-- | Clean up the resources held by the readers.
--
-- Only call this function when aborting before all readers have been drained!
close ::
(MonadMask m, MonadSTM m, PrimMonad m)
=> Readers m h
-> m ()
close Readers {..} = do
ReadCtx {readCtxReader} <- readMutVar readersNext
closeReader readCtxReader
closeHeap
where
closeReader = \case
ReadBuffer _ -> pure ()
ReadRun r -> RunReader.close r
ReadReaders _ readersMay -> smaybe (pure ()) close readersMay
closeHeap =
Heap.extract readersHeap >>= \case
Nothing -> pure ()
Just ReadCtx {readCtxReader} -> do
closeReader readCtxReader
closeHeap
{-# SPECIALISE peekKey :: Readers IO h -> IO SerialisedKey #-}
-- | Return the smallest key present in the readers, without consuming any
-- entries.
peekKey ::
PrimMonad m
=> Readers m h
-> m SerialisedKey
peekKey Readers {..} = do
readCtxHeadKey <$> readMutVar readersNext
-- | Once a function returned 'Drained', do not use the 'Readers' any more!
data HasMore = HasMore | Drained
deriving stock (Eq, Show)
{-# SPECIALISE pop ::
ResolveSerialisedValue
-> Readers IO h
-> IO (SerialisedKey, RunReader.Entry IO h, HasMore) #-}
-- | Remove the entry with the smallest key and return it. If there are multiple
-- entries with that key, it removes the one from the source that came first
-- in list supplied to 'new'. No resolution of multiple entries takes place.
pop ::
(MonadMask m, MonadSTM m, MonadST m)
=> ResolveSerialisedValue
-> Readers m h
-> m (SerialisedKey, RunReader.Entry m h, HasMore)
pop resolve r@Readers {..} = do
ReadCtx {..} <- readMutVar readersNext
hasMore <- dropOne resolve r readCtxNumber readCtxReader
pure (readCtxHeadKey, readCtxHeadEntry, hasMore)
-- TODO: avoid duplication with Merge.TreeMergeType?
data ReadersMergeType = MergeLevel | MergeUnion
deriving stock (Eq, Show)
{-# SPECIALISE popResolved ::
ResolveSerialisedValue
-> ReadersMergeType
-> Readers IO h
-> IO (SerialisedKey, RunReader.Entry IO h, HasMore) #-}
-- | Produces an entry with the smallest key, resolving all input entries if
-- there are multiple. Therefore, the next call to 'peekKey' will return a
-- larger key than the one returned here.
--
-- General notes on the code below:
-- * It is quite similar to the one in Internal.Cursor and Internal.Merge. Maybe
-- we can avoid some duplication.
-- * Any function that doesn't take a 'hasMore' argument assumes that the
-- readers have not been drained yet, so we must check before calling them.
-- * There is probably opportunity for optimisations.
--
-- TODO: use this function in Internal.Cursor? Measure performance impact.
popResolved ::
forall h m.
(MonadMask m, MonadST m, MonadSTM m)
=> ResolveSerialisedValue
-> ReadersMergeType
-> Readers m h
-> m (SerialisedKey, RunReader.Entry m h, HasMore)
popResolved resolve mergeType readers = readEntry
where
readEntry :: m (SerialisedKey, RunReader.Entry m h, HasMore)
readEntry = do
(key, entry, hasMore) <- pop resolve readers
case hasMore of
Drained -> do
pure (key, entry, Drained)
HasMore -> do
case mergeType of
MergeLevel -> handleLevel key (RunReader.toFullEntry entry)
MergeUnion -> handleUnion key (RunReader.toFullEntry entry)
handleUnion :: SerialisedKey
-> Entry SerialisedValue (RawBlobRef m h)
-> m (SerialisedKey, RunReader.Entry m h, HasMore)
handleUnion key entry = do
nextKey <- peekKey readers
if nextKey /= key
then
-- No more entries for same key, done.
pure (key, RunReader.Entry entry, HasMore)
else do
(_, nextEntry, hasMore) <- pop resolve readers
let resolved = Entry.combineUnion resolve entry
(RunReader.toFullEntry nextEntry)
case hasMore of
HasMore -> handleUnion key resolved
Drained -> pure (key, RunReader.Entry resolved, Drained)
handleLevel :: SerialisedKey
-> Entry SerialisedValue (RawBlobRef m h)
-> m (SerialisedKey, RunReader.Entry m h, HasMore)
handleLevel key entry =
case entry of
Upsert v ->
handleMupdate key v
_ -> do
-- Anything but Upsert supersedes all previous entries of
-- the same key, so we can simply drop them and are done.
hasMore' <- dropRemaining key
pure (key, RunReader.Entry entry, hasMore')
-- Resolve a 'Mupsert' value with the other entries of the same key.
handleMupdate :: SerialisedKey
-> SerialisedValue
-> m (SerialisedKey, RunReader.Entry m h, HasMore)
handleMupdate key v = do
nextKey <- peekKey readers
if nextKey /= key
then
-- No more entries for same key, done.
pure (key, RunReader.Entry (Upsert v), HasMore)
else do
(_, nextEntry, hasMore) <- pop resolve readers
let resolved = Entry.combine resolve (Upsert v)
(RunReader.toFullEntry nextEntry)
case hasMore of
HasMore -> handleLevel key resolved
Drained -> pure (key, RunReader.Entry resolved, Drained)
dropRemaining :: SerialisedKey -> m HasMore
dropRemaining key = do
(_, hasMore) <- dropWhileKey resolve readers key
pure hasMore
{-# SPECIALISE dropWhileKey ::
ResolveSerialisedValue
-> Readers IO h
-> SerialisedKey
-> IO (Int, HasMore) #-}
-- | Drop all entries with a key that is smaller or equal to the supplied one.
dropWhileKey ::
(MonadMask m, MonadSTM m, MonadST m)
=> ResolveSerialisedValue
-> Readers m h
-> SerialisedKey
-> m (Int, HasMore) -- ^ How many were dropped?
dropWhileKey resolve Readers {..} key = do
cur <- readMutVar readersNext
if readCtxHeadKey cur <= key
then go 0 cur
else pure (0, HasMore) -- nothing to do
where
-- invariant: @readCtxHeadKey <= key@
go !n ReadCtx {readCtxNumber, readCtxReader} = do
mNext <- nextReadCtx resolve readCtxNumber readCtxReader >>= \case
Nothing -> Heap.extract readersHeap
Just ctx -> Just <$> Heap.replaceRoot readersHeap ctx
let !n' = n + 1
case mNext of
Nothing -> do
pure (n', Drained)
Just next -> do
-- hasMore
if readCtxHeadKey next <= key
then
go n' next
else do
writeMutVar readersNext next
pure (n', HasMore)
{-# SPECIALISE dropOne ::
ResolveSerialisedValue
-> Readers IO h
-> ReaderNumber
-> Reader IO h
-> IO HasMore #-}
dropOne ::
(MonadMask m, MonadSTM m, MonadST m)
=> ResolveSerialisedValue
-> Readers m h
-> ReaderNumber
-> Reader m h
-> m HasMore
dropOne resolve Readers {..} number reader = do
mNext <- nextReadCtx resolve number reader >>= \case
Nothing -> Heap.extract readersHeap
Just ctx -> Just <$> Heap.replaceRoot readersHeap ctx
case mNext of
Nothing ->
pure Drained
Just next -> do
writeMutVar readersNext next
pure HasMore
{-# SPECIALISE nextReadCtx ::
ResolveSerialisedValue
-> ReaderNumber
-> Reader IO h
-> IO (Maybe (ReadCtx IO h)) #-}
nextReadCtx ::
(MonadMask m, MonadSTM m, MonadST m)
=> ResolveSerialisedValue
-> ReaderNumber
-> Reader m h
-> m (Maybe (ReadCtx m h))
nextReadCtx resolve readCtxNumber readCtxReader =
case readCtxReader of
ReadBuffer r -> atomicModifyMutVar r $ \case
[] ->
([], Nothing)
((readCtxHeadKey, e) : rest) ->
let readCtxHeadEntry = RunReader.Entry e
in (rest, Just ReadCtx {..})
ReadRun r -> RunReader.next r <&> \case
RunReader.Empty ->
Nothing
RunReader.ReadEntry readCtxHeadKey readCtxHeadEntry ->
Just ReadCtx {..}
ReadReaders mergeType readersMay -> case readersMay of
SNothing ->
pure Nothing
SJust readers -> do
(readCtxHeadKey, readCtxHeadEntry, hasMore) <-
popResolved resolve mergeType readers
let readersMay' = case hasMore of
Drained -> SNothing
HasMore -> SJust readers
pure $ Just ReadCtx {
-- TODO: reduce allocations?
readCtxReader = ReadReaders mergeType readersMay'
, ..
}