packages feed

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

{-# OPTIONS_HADDOCK not-home #-}

-- | A run that is being read incrementally.
--
module Database.LSMTree.Internal.RunReader (
    RunReader (..)
  , OffsetKey (..)
  , new
  , next
  , close
  , Result (..)
  , Entry (..)
  , toFullEntry
  , appendOverflow
    -- * Exported for WriteBufferReader
  , mkEntryOverflow
  , readDiskPage
  , readOverflowPages
  ) where

import           Control.Exception (assert)
import           Control.Monad (guard, when)
import           Control.Monad.Class.MonadST (MonadST (..))
import           Control.Monad.Class.MonadSTM (MonadSTM (..))
import           Control.Monad.Class.MonadThrow (MonadCatch (..),
                     MonadMask (..), MonadThrow (..))
import           Control.Monad.Primitive (PrimMonad (..))
import           Control.RefCount
import           Data.Bifunctor (first)
import           Data.Maybe (isNothing)
import           Data.Primitive.ByteArray (newPinnedByteArray,
                     unsafeFreezeByteArray)
import           Data.Primitive.MutVar (MutVar, newMutVar, readMutVar,
                     writeMutVar)
import           Data.Primitive.PrimVar
import           Data.Word (Word16, Word32)
import           Database.LSMTree.Internal.BitMath (ceilDivPageSize,
                     mulPageSize, roundUpToPageSize)
import           Database.LSMTree.Internal.BlobFile as BlobFile
import           Database.LSMTree.Internal.BlobRef as BlobRef
import qualified Database.LSMTree.Internal.Entry as E
import qualified Database.LSMTree.Internal.Index as Index (search)
import           Database.LSMTree.Internal.Page (PageNo (..), PageSpan (..),
                     getNumPages, nextPageNo)
import           Database.LSMTree.Internal.Paths
import qualified Database.LSMTree.Internal.RawBytes as RB
import           Database.LSMTree.Internal.RawOverflowPage (RawOverflowPage,
                     pinnedByteArrayToOverflowPages, rawOverflowPageRawBytes)
import           Database.LSMTree.Internal.RawPage
import qualified Database.LSMTree.Internal.Run as Run
import           Database.LSMTree.Internal.Serialise
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)

-- | Allows reading the k\/ops of a run incrementally, using its own read-only
-- file handle and in-memory cache of the current disk page.
--
-- Creating a 'RunReader' does not retain a reference to the 'Run', but does
-- retain an independent reference on the run's blob file. It is not necessary
-- to separately retain the 'Run' for correct use of the 'RunReader'. There is
-- one important caveat however: the 'RunReader' maintains the validity of
-- 'BlobRef's only up until the point where the reader is drained (or
-- explicitly closed). In particular this means 'BlobRefs' can be invalidated
-- as soon as the 'next' returns 'Empty'. If this is not sufficient then it is
-- necessary to separately retain a reference to the 'Run' or its 'BlobFile' to
-- preserve the validity of 'BlobRefs'.
--
-- New pages are loaded when trying to read their first entry.
--
-- TODO(optimise): Reuse page buffers using some kind of allocator. However,
-- deciding how long a page needs to stay around is not trivial.
data RunReader m h = RunReader {
      -- | The disk page currently being read. If it is 'Nothing', the reader
      -- is considered closed.
      readerCurrentPage    :: !(MutVar (PrimState m) (Maybe RawPage))
      -- | The index of the entry to be returned by the next call to 'next'.
    , readerCurrentEntryNo :: !(PrimVar (PrimState m) Word16)
      -- | Read mode file handle into the run's k\/ops file. We rely on it to
      -- track the position of the next disk page to read, instead of keeping
      -- a counter ourselves. Also, the run's handle is supposed to be opened
      -- with @O_DIRECT@, which is counterproductive here.
    , readerKOpsHandle     :: !(FS.Handle h)
      -- | The blob file from the run this reader is reading from.
    , readerBlobFile       :: !(Ref (BlobFile m h))
    , readerRunDataCaching :: !Run.RunDataCaching
    , readerHasFS          :: !(HasFS m h)
    , readerHasBlockIO     :: !(HasBlockIO m h)
    }

data OffsetKey = NoOffsetKey | OffsetKey !SerialisedKey
  deriving stock Show

{-# SPECIALISE new ::
     OffsetKey
  -> Ref (Run.Run IO h)
  -> IO (RunReader IO h) #-}
new :: forall m h.
     (MonadMask m, MonadSTM m, PrimMonad m)
  => OffsetKey
  -> Ref (Run.Run m h)
  -> m  (RunReader m h)
new !offsetKey
    readerRun@(DeRef Run.Run {
      runBlobFile,
      runRunDataCaching = readerRunDataCaching,
      runHasFS          = readerHasFS,
      runHasBlockIO     = readerHasBlockIO,
      runIndex          = index
    }) = do
    (readerKOpsHandle :: FS.Handle h) <-
      FS.hOpen readerHasFS (runKOpsPath (Run.runFsPaths readerRun)) FS.ReadMode >>= \h -> do
        fileSize <- FS.hGetSize readerHasFS h
        let fileSizeInPages = fileSize `div` toEnum pageSize
        let indexedPages = getNumPages $ Run.sizeInPages readerRun
        assert (indexedPages == fileSizeInPages) $ pure h
    -- Advise the OS that this file is being read sequentially, which will
    -- double the readahead window in response (only for this file descriptor)
    FS.hAdviseAll readerHasBlockIO readerKOpsHandle FS.AdviceSequential

    (page, entryNo) <- seekFirstEntry readerKOpsHandle

    readerBlobFile <- dupRef runBlobFile
    readerCurrentEntryNo <- newPrimVar entryNo
    readerCurrentPage <- newMutVar page
    let reader = RunReader {..}

    when (isNothing page) $
      close reader
    pure reader
  where
    seekFirstEntry readerKOpsHandle =
        case offsetKey of
          NoOffsetKey -> do
            -- Load first page from disk, if it exists.
            firstPage <- readDiskPage readerHasFS readerKOpsHandle
            pure (firstPage, 0)
          OffsetKey offset -> do
            -- Use the index to find the page number for the key (if it exists).
            let PageSpan pageNo pageEnd = Index.search offset index
            seekToDiskPage readerHasFS pageNo readerKOpsHandle
            readDiskPage readerHasFS readerKOpsHandle >>= \case
              Nothing ->
                pure (Nothing, 0)
              Just foundPage -> do
                case rawPageFindKey foundPage offset of
                  Just n ->
                    -- Found an appropriate index within the index's page.
                    pure (Just foundPage, n)

                  _ -> do
                    -- The index said that the key, if it were to exist, would
                    -- live on pageNo, but then rawPageFindKey tells us that in
                    -- fact there is no key greater than or equal to the given
                    -- offset on this page.
                    -- This tells us that the key does not exist, but that if it
                    -- were to exist, it would be between the last key in this
                    -- page and the first key in the next page.
                    -- Thus the reader should be initialised to return keys
                    -- starting from the next (non-overflow) page.
                    seekToDiskPage readerHasFS (nextPageNo pageEnd) readerKOpsHandle
                    nextPage <- readDiskPage readerHasFS readerKOpsHandle
                    pure (nextPage, 0)

{-# SPECIALISE close ::
     RunReader IO h
  -> IO () #-}
-- | This function should be called when discarding a 'RunReader' before it
-- was done (i.e. returned 'Empty'). This avoids leaking file handles.
-- Once it has been called, do not use the reader any more!
close ::
     (MonadSTM m, MonadMask m, PrimMonad m)
  => RunReader m h
  -> m ()
close RunReader{..} = do
    when (readerRunDataCaching == Run.NoCacheRunData) $
      -- drop the file from the OS page cache
      FS.hDropCacheAll readerHasBlockIO readerKOpsHandle
    FS.hClose readerHasFS readerKOpsHandle
    releaseRef readerBlobFile
    --TODO: arguably we should have distinct finish and close and require that
    -- readers are _always_ closed, even after they have been drained.
    -- This would allow BlobRefs to remain valid until the reader is closed.
    -- Currently they are invalidated as soon as the cursor is drained.

-- | The 'SerialisedKey' and 'SerialisedValue' point into the in-memory disk
-- page. Keeping them alive will also prevent garbage collection of the 4k byte
-- array, so if they're long-lived, consider making a copy!
data Result m h
  = Empty
  | ReadEntry !SerialisedKey !(Entry m h)

data Entry m h =
    Entry
      !(E.Entry SerialisedValue (RawBlobRef m h))
  | -- | A large entry. The caller might be interested in various different
    -- (redundant) representation, so we return all of them.
    EntryOverflow
      -- | The value is just a prefix, with the remainder in the overflow pages.
      !(E.Entry SerialisedValue (RawBlobRef m h))
      -- | A page containing the single entry (or rather its prefix).
      !RawPage
      -- | Non-zero length of the overflow in bytes.
      !Word32
      -- | The overflow pages containing the suffix of the value (so at least
      -- the number of bytes specified above).
      --
      -- TODO(optimise): Sometimes, reading the overflow pages is not necessary.
      -- We could just return the page index and offer a separate function to do
      -- the disk I/O once needed.
      ![RawOverflowPage]

mkEntryOverflow ::
     E.Entry SerialisedValue (RawBlobRef m h)
  -> RawPage
  -> Word32
  -> [RawOverflowPage]
  -> Entry m h
mkEntryOverflow entryPrefix page len overflowPages =
    assert (len > 0) $
    assert (rawPageOverflowPages page == ceilDivPageSize (fromIntegral len)) $
    assert (rawPageOverflowPages page == length overflowPages) $
      EntryOverflow entryPrefix page len overflowPages

{-# INLINE toFullEntry #-}
toFullEntry :: Entry m h -> E.Entry SerialisedValue (RawBlobRef m h)
toFullEntry = \case
    Entry e ->
      e
    EntryOverflow prefix _ len overflowPages ->
      first (appendOverflow len overflowPages) prefix

{-# INLINE appendOverflow #-}
appendOverflow :: Word32 -> [RawOverflowPage] -> SerialisedValue -> SerialisedValue
appendOverflow len overflowPages (SerialisedValue prefix) =
    SerialisedValue $
      RB.take (RB.size prefix + fromIntegral len) $
        mconcat (prefix : map rawOverflowPageRawBytes overflowPages)

{-# SPECIALISE next ::
     RunReader IO h
  -> IO (Result IO h) #-}
-- | Stop using the 'RunReader' after getting 'Empty', because the 'Reader' is
-- automatically closed!
next :: forall m h.
     (MonadMask m, MonadSTM m, MonadST m)
  => RunReader m h
  -> m (Result m h)
next reader@RunReader {..} = do
    readMutVar readerCurrentPage >>= \case
      Nothing ->
        pure Empty
      Just page -> do
        entryNo <- readPrimVar readerCurrentEntryNo
        go entryNo page
  where
    go :: Word16 -> RawPage -> m (Result m h)
    go !entryNo !page =
        -- take entry from current page (resolve blob if necessary)
        case rawPageIndex page entryNo of
          IndexNotPresent -> do
            -- if it is past the last one, load a new page from disk, try again
            newPage <- readDiskPage readerHasFS readerKOpsHandle
            stToIO $ writeMutVar readerCurrentPage newPage
            case newPage of
              Nothing -> do
                close reader
                pure Empty
              Just p -> do
                writePrimVar readerCurrentEntryNo 0
                go 0 p  -- try again on the new page
          IndexEntry key entry -> do
            modifyPrimVar readerCurrentEntryNo (+1)
            let entry' = fmap (BlobRef.mkRawBlobRef readerBlobFile) entry
            let rawEntry = Entry entry'
            pure (ReadEntry key rawEntry)
          IndexEntryOverflow key entry lenSuffix -> do
            -- TODO: we know that we need the next page, could already load?
            modifyPrimVar readerCurrentEntryNo (+1)
            let entry' :: E.Entry SerialisedValue (RawBlobRef m h)
                entry' = fmap (BlobRef.mkRawBlobRef readerBlobFile) entry
            overflowPages <- readOverflowPages readerHasFS readerKOpsHandle lenSuffix
            let rawEntry = mkEntryOverflow entry' page lenSuffix overflowPages
            pure (ReadEntry key rawEntry)

{-------------------------------------------------------------------------------
  Utilities
-------------------------------------------------------------------------------}

seekToDiskPage :: HasFS m h -> PageNo -> FS.Handle h -> m ()
seekToDiskPage fs pageNo h = do
    FS.hSeek fs h FS.AbsoluteSeek (pageNoToByteOffset pageNo)
  where
    pageNoToByteOffset (PageNo n) =
        assert (n >= 0) $
          mulPageSize (fromIntegral n)

{-# SPECIALISE readDiskPage ::
     HasFS IO h
  -> FS.Handle h
  -> IO (Maybe RawPage) #-}
-- | Returns 'Nothing' on EOF.
readDiskPage ::
     (MonadCatch m, PrimMonad m)
  => HasFS m h
  -> FS.Handle h
  -> m (Maybe RawPage)
readDiskPage fs h = do
    mba <- newPinnedByteArray pageSize
    -- TODO: make sure no other exception type can be thrown
    --
    -- TODO: if FS.FsReachEOF is thrown as an injected disk fault, then we
    -- incorrectly deduce that the file has no more contents. We should probably
    -- use an explicit file pointer instead in the style of 'FilePointer'.
    handleJust (guard . FS.isFsErrorType FS.FsReachedEOF) (\_ -> pure Nothing) $ do
      bytesRead <- FS.hGetBufExactly fs h mba 0 (fromIntegral pageSize)
      assert (fromIntegral bytesRead == pageSize) $ pure ()
      ba <- unsafeFreezeByteArray mba
      let !rawPage = unsafeMakeRawPage ba 0
      pure (Just rawPage)

pageSize :: Int
pageSize = 4096

{-# SPECIALISE readOverflowPages ::
     HasFS IO h
  -> FS.Handle h
  -> Word32
  -> IO [RawOverflowPage] #-}
-- | Throws exception on EOF. If a suffix was expected, the file should have it.
-- Reads full pages, despite the suffix only using part of the last page.
readOverflowPages ::
     (MonadSTM m, MonadThrow m, PrimMonad m)
   => HasFS m h
   -> FS.Handle h
   -> Word32
   -> m [RawOverflowPage]
readOverflowPages fs h len = do
    let lenPages = fromIntegral (roundUpToPageSize len)  -- always read whole pages
    mba <- newPinnedByteArray lenPages
    _ <- FS.hGetBufExactly fs h mba 0 (fromIntegral lenPages)
    ba <- unsafeFreezeByteArray mba
    -- should not copy since 'ba' is pinned and its length is a multiple of 4k.
    pure $ pinnedByteArrayToOverflowPages 0 lenPages ba