packages feed

lsm-tree-1.0.0.0: src-extras/Database/LSMTree/Extras/MergingTreeData.hs

-- | Utilities for generating 'MergingTree's. Tests and benchmarks should
-- preferably use these utilities instead of (re-)defining their own.
module Database.LSMTree.Extras.MergingTreeData (
    -- * Create merging trees
    withMergingTree
  , unsafeCreateMergingTree
    -- * MergingTreeData
  , MergingTreeData (..)
  , PreExistingRunData (..)
  , mergingTreeDataInvariant
  , mapMergingTreeData
  , SerialisedMergingTreeData
  , serialiseMergingTreeData
    -- * QuickCheck
  , labelMergingTreeData
  , genMergingTreeData
  , shrinkMergingTreeData
  ) where

import           Control.Exception (assert, bracket)
import           Control.RefCount
import           Data.Foldable (for_, toList)
import           Database.LSMTree.Extras (showPowersOf)
import           Database.LSMTree.Extras.Generators ()
import           Database.LSMTree.Extras.MergingRunData
import           Database.LSMTree.Extras.RunData
import qualified Database.LSMTree.Internal.BloomFilter as Bloom
import qualified Database.LSMTree.Internal.MergingRun as MR
import           Database.LSMTree.Internal.MergingTree (MergingTree)
import qualified Database.LSMTree.Internal.MergingTree as MT
import           Database.LSMTree.Internal.RunBuilder (RunParams)
import           Database.LSMTree.Internal.Serialise
import           Database.LSMTree.Internal.UniqCounter
import qualified System.FS.API as FS
import           System.FS.API (HasFS)
import           System.FS.BlockIO.API (HasBlockIO)
import           Test.QuickCheck as QC

{-------------------------------------------------------------------------------
  Create merging tree
-------------------------------------------------------------------------------}

-- | Create a temporary 'MergingTree' using 'unsafeCreateMergingTree'.
withMergingTree ::
     HasFS IO h
  -> HasBlockIO IO h
  -> ResolveSerialisedValue
  -> Bloom.Salt
  -> RunParams
  -> FS.FsPath
  -> UniqCounter IO
  -> SerialisedMergingTreeData
  -> (Ref (MergingTree IO h) -> IO a)
  -> IO a
withMergingTree hfs hbio resolve salt runParams path counter mrd = do
    bracket
      (unsafeCreateMergingTree hfs hbio resolve salt runParams path counter mrd)
      releaseRef

-- | Flush serialised merging tree data to disk.
--
-- This might leak resources if not run with asynchronous exceptions masked.
-- Consider using 'withMergingTree' instead.
--
-- Use of this function should be paired with a 'releaseRef'.
unsafeCreateMergingTree ::
     HasFS IO h
  -> HasBlockIO IO h
  -> ResolveSerialisedValue
  -> Bloom.Salt
  -> RunParams
  -> FS.FsPath
  -> UniqCounter IO
  -> SerialisedMergingTreeData
  -> IO (Ref (MergingTree IO h))
unsafeCreateMergingTree hfs hbio resolve salt runParams path counter = go
  where
    go = \case
      CompletedTreeMergeData rd ->
        withRun hfs hbio salt runParams path counter rd $ \run ->
          MT.newCompletedMerge run
      OngoingTreeMergeData mrd ->
        withMergingRun hfs hbio resolve salt runParams path counter mrd $ \mr ->
          MT.newOngoingMerge mr
      PendingLevelMergeData prds mtd ->
        withPreExistingRuns prds $ \prs ->
          withMaybeTree mtd $ \mt ->
            MT.newPendingLevelMerge prs mt
      PendingUnionMergeData mtds ->
        withTrees mtds $ \mts ->
          MT.newPendingUnionMerge mts

    withTrees []         act = act []
    withTrees (mtd:rest) act =
        bracket (go mtd) releaseRef $ \t ->
          withTrees rest $ \ts ->
            act (t:ts)

    withMaybeTree Nothing    act = act Nothing
    withMaybeTree (Just mtd) act =
        bracket (go mtd) releaseRef $ \t ->
          act (Just t)

    withPreExistingRuns [] act = act []
    withPreExistingRuns (PreExistingRunData rd : rest) act =
        withRun hfs hbio salt runParams path counter rd $ \r ->
          withPreExistingRuns rest $ \prs ->
            act (MT.PreExistingRun r : prs)
    withPreExistingRuns (PreExistingMergingRunData mrd : rest) act =
        withMergingRun hfs hbio resolve salt runParams path counter mrd $ \mr ->
          withPreExistingRuns rest $ \prs ->
            act (MT.PreExistingMergingRun mr : prs)

{-------------------------------------------------------------------------------
  MergingTreeData
-------------------------------------------------------------------------------}

-- TODO: This module has quite a lot duplication with the prototype's
-- ScheduledMergesTest module. Maybe we can share some code?

-- | A data structure suitable for creating arbitrary 'MergingTree's.
--
-- Note: 'b ~ Void' should rule out blobs.
data MergingTreeData k v b =
    CompletedTreeMergeData (RunData k v b)
  | OngoingTreeMergeData (MergingRunData MR.TreeMergeType k v b)
  | PendingLevelMergeData
      [PreExistingRunData k v b]
      (Maybe (MergingTreeData k v b))  -- ^ not both empty!
  | PendingUnionMergeData [MergingTreeData k v b]  -- ^ at least 2 children
  deriving stock (Show, Eq)

data PreExistingRunData k v b =
    PreExistingRunData (RunData k v b)
  | PreExistingMergingRunData (MergingRunData MR.LevelMergeType k v b)
  deriving stock (Show, Eq)

mergingTreeIsStructurallyEmpty :: MergingTreeData k v b -> Bool
mergingTreeIsStructurallyEmpty = \case
    CompletedTreeMergeData _   -> False  -- could be, but we match MT
    OngoingTreeMergeData _     -> False
    PendingLevelMergeData ps t -> null ps && null t
    PendingUnionMergeData ts   -> null ts

-- | See @treeInvariant@ in prototype.
mergingTreeDataInvariant :: MergingTreeData k v b -> Either String ()
mergingTreeDataInvariant mtd
  | mergingTreeIsStructurallyEmpty mtd = Right ()
  | otherwise = case mtd of
      CompletedTreeMergeData _rd ->
        Right ()
      OngoingTreeMergeData mr ->
        mergingRunDataInvariant mr
      PendingLevelMergeData prs t -> do
        assertI "pending level merges have at least one input" $
          length prs + length t > 0
        for_ prs $ \case
          PreExistingRunData        _r -> Right ()
          PreExistingMergingRunData mr -> mergingRunDataInvariant mr
        for_ (drop 1 (reverse prs)) $ \case
          PreExistingRunData        _r -> Right ()
          PreExistingMergingRunData mr ->
            assertI "only the last pre-existing run can be a last level merge" $
              mergingRunDataMergeType mr == MR.MergeMidLevel
        for_ t mergingTreeDataInvariant
      PendingUnionMergeData ts -> do
        assertI "pending union merges are non-trivial (at least two inputs)" $
          length ts >= 2
        for_ ts mergingTreeDataInvariant
  where
    assertI msg False = Left msg
    assertI _   True  = Right ()

mapMergingTreeData ::
     Ord k'
  => (k -> k') -> (v -> v') -> (b -> b')
  -> MergingTreeData k v b -> MergingTreeData k' v' b'
mapMergingTreeData f g h = \case
    CompletedTreeMergeData r ->
      CompletedTreeMergeData $ mapRunData f g h r
    OngoingTreeMergeData mr ->
      OngoingTreeMergeData $ mapMergingRunData f g h mr
    PendingLevelMergeData prs t ->
      PendingLevelMergeData
        (map (mapPreExistingRunData f g h) prs)
        (fmap (mapMergingTreeData f g h) t)
    PendingUnionMergeData ts ->
      PendingUnionMergeData $ fmap (mapMergingTreeData f g h) ts

mapPreExistingRunData ::
     Ord k'
  => (k -> k') -> (v -> v') -> (b -> b')
  -> PreExistingRunData k v b -> PreExistingRunData k' v' b'
mapPreExistingRunData f g h = \case
    PreExistingRunData r ->
      PreExistingRunData (mapRunData f g h r)
    PreExistingMergingRunData mr ->
      PreExistingMergingRunData (mapMergingRunData f g h mr)

type SerialisedMergingTreeData =
    MergingTreeData SerialisedKey SerialisedValue SerialisedBlob

type SerialisedPreExistingRunData =
    PreExistingRunData SerialisedKey SerialisedValue SerialisedBlob

serialiseMergingTreeData ::
     (SerialiseKey k, SerialiseValue v, SerialiseValue b)
  => MergingTreeData k v b -> SerialisedMergingTreeData
serialiseMergingTreeData =
    mapMergingTreeData serialiseKey serialiseValue serialiseBlob

{-------------------------------------------------------------------------------
  QuickCheck
-------------------------------------------------------------------------------}

labelMergingTreeData :: SerialisedMergingTreeData -> Property -> Property
labelMergingTreeData = \rd ->
    tabulate "tree depth" [showPowersOf 2 (depthTree rd)] . go rd
  where
    go (CompletedTreeMergeData rd) =
          tabulate "merging tree state" ["CompletedTreeMerge"]
        . labelRunData rd
    go (OngoingTreeMergeData mrd) =
          tabulate "merging tree state" ["OngoingTreeMerge"]
        . labelMergingRunData mrd
    go (PendingLevelMergeData prds mtd) =
          tabulate "merging tree state" ["PendingLevelMerge"]
        . foldr ((.) . labelPreExistingRunData) id prds
        . maybe id go mtd
    go (PendingUnionMergeData mtds) =
          tabulate "merging tree state" ["PendingUnionMerge"]
        . foldr ((.) . go) id mtds

    -- the longest path from the root to a run
    depthTree = (+1) . \case  -- maximum depth of children
        CompletedTreeMergeData _ -> 0
        OngoingTreeMergeData _   -> 0
        PendingLevelMergeData prds mtds ->
          maximum (0 : fmap (const 1) prds ++ map depthTree (toList mtds))
        PendingUnionMergeData mtds ->
          maximum (0 : map depthTree mtds)


labelPreExistingRunData :: SerialisedPreExistingRunData -> Property -> Property
labelPreExistingRunData (PreExistingRunData rd)         = labelRunData rd
labelPreExistingRunData (PreExistingMergingRunData mrd) = labelMergingRunData mrd

instance ( Ord k, Arbitrary k, Arbitrary v, Arbitrary b
         ) => Arbitrary (MergingTreeData k v b) where
  arbitrary = genMergingTreeData arbitrary arbitrary arbitrary
  shrink = shrinkMergingTreeData shrink shrink shrink

genMergingTreeData ::
     Ord k => Gen k -> Gen v -> Gen b -> Gen (MergingTreeData k v b)
genMergingTreeData genKey genVal genBlob =
    QC.frequency
      -- Only at the root, we can have pending merges with no children, see
      -- 'MR.newPendingLevelMerge' and 'MR.newPendingUnionMerge'.
      [ ( 1, pure $ PendingLevelMergeData [] Nothing)
      , ( 1, pure $ PendingUnionMergeData [])
      , (50, QC.sized $ \s -> do
          treeSize <- QC.chooseInt (1, 1 + (s `div` 4)) -- up to 26
          genMergingTreeDataOfSize genKey genVal genBlob treeSize)
      ]

-- | Minimal returned size will be 1. Doesn't generate structurally empty trees!
--
-- The size is measured by the number of MergingTreeData constructors.
genMergingTreeDataOfSize ::
     forall k v b. Ord k
  => Gen k -> Gen v -> Gen b -> Int -> Gen (MergingTreeData k v b)
genMergingTreeDataOfSize genKey genVal genBlob = \n0 -> do
    tree <- genMergingTree n0
    assert (mergingTreeDataSize tree == n0) $
      pure tree
  where
    genMergingTree n
      | n < 1
      = error ("arbitrary T: n == " <> show n)

      | n == 1
      = QC.oneof
          [ CompletedTreeMergeData <$> genRun
          , OngoingTreeMergeData <$> genMergingRun arbitrary
          , genPendingLevelMergeNoChild
          ]

      | n == 2
      = genPendingLevelMergeWithChild n

      | otherwise
      = QC.oneof [genPendingLevelMergeWithChild n, genPendingUnionMerge n]

    -- n == 1
    genPendingLevelMergeNoChild = do
        numPreExisting <- chooseIntSkewed (0, 6)
        initPreExisting <- QC.vectorOf numPreExisting $
          -- these can't be last level. we generate the last input below.
          genPreExistingRun (pure MR.MergeMidLevel)
        -- there must be at least one (last) input to the pending merge.
        lastPreExisting <- genPreExistingRun arbitrary
        let preExisting = initPreExisting ++ [lastPreExisting]
        pure (PendingLevelMergeData preExisting Nothing)

    -- n >= 2
    genPendingLevelMergeWithChild n = do
        numPreExisting <- chooseIntSkewed (0, 6)
        preExisting <- QC.vectorOf numPreExisting $
          -- there can't be a last level merge, child is last
          genPreExistingRun (pure MR.MergeMidLevel)
        tree <- genMergingTree (n - 1)
        pure (PendingLevelMergeData preExisting (Just tree))

    -- n >= 3, needs 1 constructor + 2 children
    genPendingUnionMerge n = do
        ns <- QC.shuffle =<< arbitraryPartition2 (n - 1)
        PendingUnionMergeData <$> traverse genMergingTree ns

    genRun                    = genScaled genRunData
    genMergingRun genType     = genScaled (genMergingRunData genType)
    genPreExistingRun genType = genScaled (genPreExistingRunData genType)

    -- To avoid generating too large test cases, we reduce the number of
    -- entries for each run. The size of the individual entries is unaffected.
    genScaled :: forall r. (Gen k -> Gen v -> Gen b -> Gen r) -> Gen r
    genScaled gen =
        QC.sized $ \s ->
          QC.scale (`div` 2) $
            gen (QC.resize s genKey) (QC.resize s genVal) (QC.resize s genBlob)

    -- skewed towards smaller values
    chooseIntSkewed (lb, ub) = do
        ub' <- QC.chooseInt (lb, ub)
        QC.chooseInt (lb, ub')

mergingTreeDataSize :: MergingTreeData k v b -> Int
mergingTreeDataSize = \case
    CompletedTreeMergeData _ -> 1
    OngoingTreeMergeData _ -> 1
    PendingLevelMergeData _ tree -> 1 + maybe 0 mergingTreeDataSize tree
    PendingUnionMergeData trees -> 1 + sum (map mergingTreeDataSize trees)

-- Split into at least two smaller positive numbers. The input needs to be
-- greater than or equal to 2.
arbitraryPartition2 :: Int -> QC.Gen [Int]
arbitraryPartition2 n = assert (n >= 2) $ do
    first <- QC.chooseInt (1, n-1)
    (first :) <$> arbitraryPartition (n - first)

-- Split into smaller positive numbers.
arbitraryPartition :: Int -> QC.Gen [Int]
arbitraryPartition n
      | n <  1 = pure []
      | n == 1 = pure [1]
      | otherwise = do
        first <- QC.chooseInt (1, n)
        (first :) <$> arbitraryPartition (n - first)

-- TODO: Would it be useful to shrink by merging subtrees into a single run?
-- This would simplify the tree while preserving many errors that depend on the
-- specific content of the tree. See prototype tests.
shrinkMergingTreeData ::
     Ord k
  => (k -> [k])
  -> (v -> [v])
  -> (b -> [b])
  -> MergingTreeData k v b
  -> [MergingTreeData k v b]
shrinkMergingTreeData shrinkKey shrinkVal shrinkBlob = \case
  CompletedTreeMergeData r ->
    [ CompletedTreeMergeData r'
    | r' <- shrinkRunData shrinkKey shrinkVal shrinkBlob r
    ]
  OngoingTreeMergeData mr ->
    [ OngoingTreeMergeData mr'
    | mr' <- shrinkMergingRunData shrinkKey shrinkVal shrinkBlob mr
    ]
  PendingLevelMergeData prs t ->
    -- just use the child tree, if present
    [ t' | Just t' <- [t] ]
    <>
    -- move completed child tree into regular levels
    [ PendingLevelMergeData (prs ++ [PreExistingRunData r]) Nothing
    | Just (CompletedTreeMergeData r) <- [t]
    ]
    <>
    [ PendingLevelMergeData prs' t'
    | (prs', t') <-
        liftShrink2
          (liftShrink (shrinkPreExistingRunData shrinkKey shrinkVal shrinkBlob))
          (liftShrink (shrinkMergingTreeData shrinkKey shrinkVal shrinkBlob))
          (prs, t)
    , length prs' + length t' > 0
    ]
  PendingUnionMergeData ts ->
    ts
    <>
    [ PendingUnionMergeData ts'
    | ts' <- liftShrink (shrinkMergingTreeData shrinkKey shrinkVal shrinkBlob) ts
    , length ts' >= 2
    ]

genPreExistingRunData ::
     Ord k
  => Gen MR.LevelMergeType
  -> Gen k
  -> Gen v
  -> Gen b
  -> Gen (PreExistingRunData k v b)
genPreExistingRunData genMergeType genKey genVal genBlob =
    QC.oneof
      [ PreExistingRunData <$> genRunData genKey genVal genBlob
      , PreExistingMergingRunData <$> genMergingRunData genMergeType genKey genVal genBlob
      ]

shrinkPreExistingRunData ::
     Ord k
  => (k -> [k])
  -> (v -> [v])
  -> (b -> [b])
  -> PreExistingRunData k v b
  -> [PreExistingRunData k v b]
shrinkPreExistingRunData shrinkKey shrinkVal shrinkBlob = \case
    PreExistingRunData r ->
      [ PreExistingRunData r'
      | r' <- shrinkRunData shrinkKey shrinkVal shrinkBlob r
      ]
    PreExistingMergingRunData mr ->
      [ PreExistingMergingRunData mr'
      | mr' <- shrinkMergingRunData shrinkKey shrinkVal shrinkBlob mr
      ]