lsm-tree 1.0.0.1 → 1.0.0.2
raw patch · 14 files changed
+702/−280 lines, 14 filesdep ~basedep ~containersdep ~io-classes
Dependency ranges changed: base, containers, io-classes, io-sim, tasty
Files
- CHANGELOG.md +25/−0
- bench/macro/utxo-bench.hs +12/−8
- lsm-tree.cabal +17/−15
- src-core/Database/LSMTree/Internal/Arena.hs +21/−19
- src-core/Database/LSMTree/Internal/BloomFilter.hs +1/−1
- src-core/Database/LSMTree/Internal/Index/Compact.hs +8/−8
- src-prototypes/ScheduledMerges.hs +274/−128
- test-prototypes/Main.hs +2/−0
- test-prototypes/Test/ScheduledMerges.hs +108/−22
- test-prototypes/Test/ScheduledMergesDL.hs +47/−0
- test-prototypes/Test/ScheduledMergesQLS.hs +7/−2
- test/Database/LSMTree/Model/Session.hs +1/−1
- test/Test/Database/LSMTree/Internal/BloomFilter.hs +49/−0
- test/Test/Database/LSMTree/Internal/Snapshot/Codec/Golden.hs +130/−76
CHANGELOG.md view
@@ -1,5 +1,30 @@ # Revision history for `lsm-tree` +## 1.0.0.2 -- 2026-04-24++### Breaking changes++None++### New features++None++### Minor changes++* Support `io-classes ^>=1.9` and `^>=1.10`. See [PR+ #819](https://github.com/IntersectMBO/lsm-tree/pull/819).+* Support `ghc-9.14`. See [PR+ #836](https://github.com/IntersectMBO/lsm-tree/pull/836).+* Support `containers-0.8`. See [PR+ #836](https://github.com/IntersectMBO/lsm-tree/pull/836).++### Bug fixes++* Fix a bug where `lookups` with a large number of input keys would sometimes+ return incorrect lookup results. See [PR+ #841](https://github.com/IntersectMBO/lsm-tree/pull/841).+ ## 1.0.0.1 -- 2025-12-03 * PATCH: support `filepath-1.4`. See PR
bench/macro/utxo-bench.hs view
@@ -38,21 +38,23 @@ import Control.Applicative ((<**>)) import Control.Concurrent (getNumCapabilities)-import Control.Concurrent.Async-import Control.Concurrent.MVar+import Control.Concurrent.Async (waitBoth, withAsyncOn)+import Control.Concurrent.MVar (MVar, newEmptyMVar, putMVar, takeMVar) import Control.DeepSeq (force)-import Control.Exception+import Control.Exception (evaluate) import Control.Monad (forM_, unless, void, when) import Control.Monad.Trans.State.Strict (runState, state)-import Control.Tracer+import Control.Tracer (Contravariant (contramap), Tracer, nullTracer,+ squelchUnless, stdoutTracer) import qualified Data.ByteString.Short as BS import qualified Data.Foldable as Fold import qualified Data.IntSet as IS-import Data.IORef+import Data.IORef (IORef, modifyIORef, modifyIORef', newIORef,+ readIORef, writeIORef) import qualified Data.List.NonEmpty as NE import Data.Map.Strict (Map) import qualified Data.Map.Strict as Map-import Data.Monoid+import Data.Monoid (Dual (Dual, getDual)) import qualified Data.Primitive as P import qualified Data.Vector as V import Data.Void (Void)@@ -64,11 +66,13 @@ import qualified System.Clock as Clock import qualified System.FS.API as FS import qualified System.FS.BlockIO.IO as FsIO-import System.IO+import System.IO (BufferMode (NoBuffering), Handle,+ IOMode (WriteMode), hPutStr, hPutStrLn, hSetBuffering,+ stdout, withFile) import System.Mem (performMajorGC) import qualified System.Random as Random import Text.Printf (printf)-import Text.Show.Pretty+import Text.Show.Pretty (ppShow) import Database.LSMTree.Extras (groupsOfN)
lsm-tree.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.4 name: lsm-tree-version: 1.0.0.1+version: 1.0.0.2 synopsis: Log-structured merge-trees description: This package contains an efficient implementation of on-disk key–value storage, implemented as a log-structured merge-tree, LSM-tree or LSMT.@@ -502,7 +502,7 @@ type: git location: https://github.com/IntersectMBO/lsm-tree subdir: lsm-tree- tag: lsm-tree-1.0.0.1+ tag: lsm-tree-1.0.0.2 common warnings ghc-options:@@ -528,6 +528,7 @@ ExplicitNamespaces GADTs LambdaCase+ OverloadedRecordDot RecordWildCards RoleAnnotations ViewPatterns@@ -540,12 +541,12 @@ Database.LSMTree.Simple build-depends:- , base >=4.16 && <4.22+ , base >=4.16 && <4.23 , blockio ^>=0.1 , contra-tracer ^>=0.1 || ^>=0.2 , deepseq ^>=1.4 || ^>=1.5 , fs-api ^>=0.4- , io-classes ^>=1.6 || ^>=1.7 || ^>=1.8.0.1+ , io-classes ^>=1.6 || ^>=1.7 || ^>=1.8.0.1 || ^>=1.9 || ^>=1.10 , io-classes:strict-mvar , lsm-tree:control , lsm-tree:core@@ -617,19 +618,19 @@ Database.LSMTree.Internal.WriteBufferWriter build-depends:- , base >=4.16 && <4.22+ , base >=4.16 && <4.23 , bitvec ^>=1.1 , blockio ^>=0.1 , bloomfilter-blocked ^>=0.1 , bytestring ^>=0.11.4.0 || ^>=0.12.1.0 , cborg ^>=0.2.10.0- , containers ^>=0.6 || ^>=0.7+ , containers ^>=0.6 || ^>=0.7 || ^>=0.8 , contra-tracer ^>=0.1 || ^>=0.2 , crc32c ^>=0.2.1 , deepseq ^>=1.4 || ^>=1.5 , filepath ^>=1.4 || ^>=1.5 , fs-api ^>=0.4- , io-classes ^>=1.6 || ^>=1.7 || ^>=1.8.0.1+ , io-classes ^>=1.6 || ^>=1.7 || ^>=1.8.0.1 || ^>=1.9 || ^>=1.10 , io-classes:strict-mvar , lsm-tree:control , lsm-tree:kmerge@@ -663,7 +664,7 @@ Database.LSMTree.Extras.UTxO build-depends:- , base >=4.16 && <4.22+ , base >=4.16 && <4.23 , bitvec , blockio , bytestring@@ -780,7 +781,7 @@ , io-classes , io-classes:strict-mvar , io-classes:strict-stm- , io-sim+ , io-sim <1.10 , lsm-tree , lsm-tree:control , lsm-tree:core@@ -798,7 +799,7 @@ , safe-wild-cards , semialign , split- , tasty+ , tasty <1.5.3.0 , tasty-golden , tasty-hunit , tasty-quickcheck@@ -1026,7 +1027,7 @@ hs-source-dirs: test main-is: kmerge-test.hs build-depends:- , base >=4.16 && <4.22+ , base >=4.16 && <4.23 , deepseq , heaps , lsm-tree:kmerge@@ -1045,7 +1046,7 @@ main-is: kmerge-test.hs cpp-options: -DKMERGE_BENCHMARKS build-depends:- , base >=4.16 && <4.22+ , base >=4.16 && <4.23 , deepseq , heaps , lsm-tree:kmerge@@ -1063,7 +1064,7 @@ hs-source-dirs: test main-is: map-range-test.hs build-depends:- , base >=4.16 && <4.22+ , base >=4.16 && <4.23 , bytestring , containers , lsm-tree:core@@ -1099,6 +1100,7 @@ Test.FormatPage Test.ScheduledMerges Test.ScheduledMerges.RunSizes+ Test.ScheduledMergesDL Test.ScheduledMergesQLS build-depends:@@ -1127,9 +1129,9 @@ Control.RefCount build-depends:- , base >=4.16 && <4.22+ , base >=4.16 && <4.23 , deepseq ^>=1.4 || ^>=1.5- , io-classes ^>=1.6 || ^>=1.7 || ^>=1.8.0.1+ , io-classes ^>=1.6 || ^>=1.7 || ^>=1.8.0.1 || ^>=1.9 || ^>=1.10 , io-classes:strict-stm , primitive ^>=0.9
src-core/Database/LSMTree/Internal/Arena.hs view
@@ -1,6 +1,8 @@-{-# LANGUAGE CPP #-}-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE CPP #-}+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE NoFieldSelectors #-}+{-# LANGUAGE OverloadedRecordDot #-} {-# OPTIONS_HADDOCK not-home #-} module Database.LSMTree.Internal.Arena ( ArenaManager,@@ -105,7 +107,7 @@ curr <- newBlock >>= newMVar free <- newMutVar [] full <- newMutVar []- pure Arena {..}+ pure Arena { curr = curr, free = free, full = full} {-# SPECIALISE closeArena :: ArenaManager s -> Arena s -> ST s ()@@ -132,10 +134,10 @@ #ifndef NO_IGNORE_ASSERTS scrambleArena _ = pure () #else-scrambleArena Arena {..} = do- readMVar curr >>= scrambleBlock- readMutVar full >>= mapM_ scrambleBlock- readMutVar free >>= mapM_ scrambleBlock+scrambleArena arena = do+ readMVar arena.curr >>= scrambleBlock+ readMutVar arena.full >>= mapM_ scrambleBlock+ readMutVar arena.free >>= mapM_ scrambleBlock {-# SPECIALISE scrambleBlock :: Block s -> ST s ()@@ -157,18 +159,18 @@ #-} -- | Reset arena, i.e. return used blocks to free list. resetArena :: PrimMonad m => Arena (PrimState m) -> m ()-resetArena Arena {..} = do- Block off mba <- takeMVar curr+resetArena arena = do+ Block off mba <- takeMVar arena.curr -- reset current block writePrimVar off 0 -- move full block to free blocks. -- block's offset will be reset in 'newBlockWithFree'- full' <- atomicModifyMutVar' full $ \xs -> ([], xs)- atomicModifyMutVar' free $ \xs -> (full' <> xs, ())+ full' <- atomicModifyMutVar' arena.full $ \xs -> ([], xs)+ atomicModifyMutVar' arena.free $ \xs -> (full' <> xs, ()) - putMVar curr $! Block off mba+ putMVar arena.curr $! Block off mba -- | Create unmanaged arena. --@@ -205,9 +207,9 @@ #-} -- TODO!? this is not async exception safe allocateFromArena' :: PrimMonad m => Arena (PrimState m)-> Size -> Alignment -> m (Offset, MutableByteArray (PrimState m))-allocateFromArena' arena@Arena { .. } !size !alignment = do+allocateFromArena' arena !size !alignment = do -- take current block, lock the arena- curr'@(Block off mba) <- takeMVar curr+ curr'@(Block off mba) <- takeMVar arena.curr off' <- readPrimVar off let !ali = alignment - 1@@ -219,18 +221,18 @@ -- * update offset writePrimVar off end -- * release lock- putMVar curr curr'+ putMVar arena.curr curr' -- * return data pure (off'', mba) else do -- doesn't fit into current block: -- * move current block into full- atomicModifyMutVar' full (\xs -> (curr' : xs, ()))+ atomicModifyMutVar' arena.full (\xs -> (curr' : xs, ())) -- * allocate new block- new <- newBlockWithFree free+ new <- newBlockWithFree arena.free -- * set new block as current, release the lock- putMVar curr new+ putMVar arena.curr new -- * go again allocateFromArena' arena size alignment
src-core/Database/LSMTree/Internal/BloomFilter.hs view
@@ -89,7 +89,7 @@ unpackRunIxKeyIx :: RunIxKeyIx -> (Int, Int) unpackRunIxKeyIx (MkRunIxKeyIx c) = ( fromIntegral (c `unsafeShiftR` 16)- , fromIntegral (c .&. 0xfff)+ , fromIntegral (c .&. 0xffff) ) {-# INLINE unpackRunIxKeyIx #-}
src-core/Database/LSMTree/Internal/Index/Compact.hs view
@@ -233,15 +233,15 @@ entries both for the first and subsequent pages of a larger-than-page entry. The natural thing to do is have each of these subsequent primary index entries contain the same key prefix value. This means a binary search will- find the /last/ entry in a run of equal prefix values.+ find the /last/ entry in a sequence of equal prefix values. - What leads to complexity is that we will /also/ get runs of equal values if+ What leads to complexity is that we will /also/ get sequences of equal values if we have clashes between pages (as discussed above). So in the general case- we may have a run of equal values made up of a mixture of clashes and+ we may have a sequence of equal values made up of a mixture of clashes and larger-than-page entries. So the general situation is that after a binary search we have found the- end of what may turn out to be a run of clashes and larger-than-page values+ end of what may turn out to be a sequence of clashes and larger-than-page values and we must disambigutate and return the appropriate single page (for the ordinary case) or an interval of pages (for the LTP case). @@ -251,13 +251,13 @@ counter-intuitive but it leads to a simpler mathematical definition (below). The search algorithm involves searching backwards in the clash bits to find- the beginning of the run of entries that are involved. To establish which- entry within the run is the right one to return, we can consult the tie+ the beginning of the sequence of entries that are involved. To establish which+ entry within the sequence is the right one to return, we can consult the tie breaker map by looking for the biggest entry that is less than or equal to- the full search key. This may then point to an index within the run of+ the full search key. This may then point to an index within the sequence of clashing entries, in which case this is the right entry, but it may also point to an earlier and thus irrelevant entry, in which case the first entry- in the run is the right one.+ in the sequence is the right one. Note that this second case also covers the case of a single non-clashing LTP.
src-prototypes/ScheduledMerges.hs view
@@ -1,8 +1,6 @@-{-# LANGUAGE DataKinds #-} {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE UnboxedTuples #-} -{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} {-# OPTIONS_GHC -Wno-partial-fields #-} -- | A prototype of an LSM with explicitly scheduled incremental merges.@@ -106,7 +104,7 @@ import Data.Functor.Contravariant import Data.Map.Strict (Map) import qualified Data.Map.Strict as Map-import Data.Maybe (catMaybes)+import Data.Maybe (catMaybes, maybeToList) import Data.Primitive.Types import Data.STRef @@ -150,7 +148,7 @@ -- | The levels of the table, from most to least recently inserted. data LSMContent s = LSMContent- Buffer -- ^ write buffer is level 0 of the table, in-memory+ WriteBuffer -- ^ write buffer is level 0 of the table, in-memory (Levels s) -- ^ \"regular\" levels 1+, on disk in real implementation (UnionLevel s) -- ^ a potential last level @@ -163,7 +161,11 @@ -- | A level is a sequence of resident runs at this level, prefixed by an -- incoming run, which is usually multiple runs that are being merged. Once -- completed, the resulting run will become a resident run at this level.+--+-- Levels can also be empty, although we currently never create empty levels.+-- We plan to make use of them when migrating a completed union level. data Level s = Level !(IncomingRun s) ![Run]+ | EmptyLevel -- | We represent single runs specially, rather than putting them in as a -- 'CompletedMerge'. This is for two reasons: to see statically that it's a@@ -173,8 +175,17 @@ data IncomingRun s = Merging !MergePolicyForLevel !NominalDebt !(STRef s NominalCredit) !(MergingRun LevelMergeType s)- | Single !Run+ | Single !SingleRunOrigin !Run +-- | Additional information about the origin of a 'Single' run. This allows us+-- to have stronger invariants.+data SingleRunOrigin = FromWriteBuffer+ | FromLevellingLevel+ -- | A former union level that was completed (merged down+ -- to a single run) and became the last regular level.+ | FromMigratedUnion+ deriving stock (Eq, Show)+ -- | The merge policy for a LSM level can be either tiering or levelling. -- In this design we use levelling for the last level, and tiering for -- all other levels. The first level always uses tiering however, even if@@ -288,18 +299,34 @@ -> PendingMerge s pattern PendingMerge mt prs ts <- (pendingContent -> (mt, prs, ts)) -type Run = Map Key Entry-type Buffer = Map Key Entry--bufferToRun :: Buffer -> Run-bufferToRun = id+newtype Run = Run { runEntries :: Map Key Entry }+ deriving newtype Show runSize :: Run -> Int-runSize = Map.size+runSize = Map.size . runEntries -bufferSize :: Buffer -> Int-bufferSize = Map.size+lookupRun :: Key -> Run -> Maybe Entry+lookupRun k = Map.lookup k . runEntries +newtype WriteBuffer = WriteBuffer { bufferEntries :: Map Key Entry }++emptyWriteBuffer :: WriteBuffer+emptyWriteBuffer = WriteBuffer Map.empty++writeBufferSize :: WriteBuffer -> Int+writeBufferSize = Map.size . bufferEntries++insertWriteBuffer :: Key -> Entry -> WriteBuffer -> WriteBuffer+insertWriteBuffer k e = WriteBuffer . Map.insertWith combine k e . bufferEntries++lookupWriteBuffer :: Key -> WriteBuffer -> Maybe Entry+lookupWriteBuffer k = Map.lookup k . bufferEntries++-- | Flush a write buffer. In the real implementation, this involves IO.+-- Note that we should never flush an empty write buffer.+flushWriteBuffer :: WriteBuffer -> Run+flushWriteBuffer (WriteBuffer m) = assert (not (null m)) (Run m)+ type Entry = Update Value Blob newtype Key = K Int@@ -340,9 +367,12 @@ levelsInvariant :: Int -> Levels s -> ST s () levelsInvariant !_ [] = pure () + levelsInvariant !_ (EmptyLevel : ls) = do+ assertST $ not (null ls) -- last level shouldn't be empty+ levelsInvariant !ln (Level ir rs : ls) = do mrs <- case ir of- Single r ->+ Single _ r -> pure (CompletedMerge r) Merging mp _ _ (MergingRun mt _ ref) -> do assertST $ ln > 1 -- no merges on level 1@@ -356,24 +386,35 @@ levelsInvariant (ln+1) ls - -- All runs within a level "proper" (as opposed to the incoming runs+ -- All regular runs within a level "proper" (as opposed to the incoming runs -- being merged) should be of the correct size for the level. expectedRunLengths :: Int -> [Run] -> [Level s] -> ST s () expectedRunLengths ln rs ls = case mergePolicyForLevel ln ls ul of- -- Levels using levelling have only one (incoming) run, which almost- -- always consists of an ongoing merge. The exception is when a- -- levelling run becomes too large and is promoted, in that case- -- initially there's no merge, but it is still represented as an- -- 'IncomingRun', using 'Single'. Thus there are no other resident runs.- LevelLevelling -> assertST $ null rs && null ls- -- Runs in tiering levels usually fit that size, but they can be one- -- larger, if a run has been held back (creating a (T+1)-way merge).- LevelTiering -> assertST $ all (\r -> runToLevelNumber LevelTiering conf r `elem` [ln, ln+1]) rs- -- (This is actually still not really true, but will hold in practice.- -- In the pathological case, all runs passed to the next level can be- -- factor ((T+1)/T) too large, and there the same holding back can lead to- -- factor ((T+2)/T) etc., until at level 12 a run is two levels too large.+ LevelLevelling -> do+ -- Levelling can only occur on the last level.+ assertST $ null ls+ -- Levels using levelling have only one (incoming) run, which almost+ -- always consists of an ongoing merge. The exception is when a+ -- levelling run becomes too large and is promoted, in that case+ -- initially there's no merge, but it is still represented as an+ -- 'IncomingRun', using 'Single'. Thus there are no other resident+ -- runs.+ assertST $ null rs+ LevelTiering -> do+ -- There are no empty runs in tiering levels, as they are either:+ -- 1. a mid-level, so deletes can't be dropped and merges can't result+ -- in empty runs+ -- 2. the first level, so their runs come directly from flushing the+ -- write buffer.+ assertST $ all (\r -> runSize r > 0) rs+ -- Runs in tiering levels usually fit that level's size, but they can+ -- be slightly larger if a run has been held back (creating a+ -- (T+1)-way merge).+ --+ -- TODO: Holding back runs can theoretically result in runs that are+ -- more than one size too large. See issue #829.+ assertST $ all (\r -> runToLevelNumber LevelTiering conf r `elem` [ln, ln+1]) rs -- Incoming runs being merged also need to be of the right size, but the -- conditions are more complicated.@@ -383,12 +424,23 @@ case mergePolicyForLevel ln ls ul of LevelLevelling -> do case (ir, mrs) of- -- A single incoming run (which thus didn't need merging) must be- -- of the expected size range already- (Single r, m) -> do+ -- A single incoming run (which didn't need merging) in a levelling+ -- level can be created one of two ways:+ -- 1. when a levelling level is full, so the run gets promoted to+ -- the new last level+ -- 2. when the union level gets migrated.+ --+ -- In the latter case, it can be arbitrarily small.+ (Single origin r, m) -> do assertST $ case m of CompletedMerge{} -> True OngoingMerge{} -> False- assertST $ runToLevelNumber LevelLevelling conf r == ln+ case origin of+ FromWriteBuffer ->+ assertST False -- we don't flush to levelling levels+ FromLevellingLevel ->+ assertST $ runToLevelNumber LevelLevelling conf r == ln+ FromMigratedUnion ->+ assertST $ runToLevelNumber LevelLevelling conf r <= ln -- A completed merge for levelling can be of almost any size at all! -- It can be smaller, due to deletions in the last level. But it@@ -415,9 +467,10 @@ case (ir, mrs, mergeTypeForLevel ls ul) of -- A single incoming run (which thus didn't need merging) must be -- of the expected size already- (Single r, m, _) -> do+ (Single origin r, m, _) -> do assertST $ case m of CompletedMerge{} -> True OngoingMerge{} -> False+ assertST $ origin == FromWriteBuffer assertST $ runToLevelNumber LevelTiering conf r == ln -- A completed last level run can be of almost any smaller size due@@ -891,7 +944,7 @@ assertST $ length runs > 1 -- in some cases, no merging is required at all (debt, state) <- case filter (\r -> runSize r > 0) runs of- [] -> let (r:_) = runs -- just reuse the empty input+ [] -> let r = head runs -- just reuse the empty input in pure (runSize r, CompletedMerge r) [r] -> pure (runSize r, CompletedMerge r) rs -> do@@ -904,8 +957,10 @@ mergek :: IsMergeType t => t -> [Run] -> Run mergek t =- (if isLastLevel t then Map.filter (/= Delete) else id)+ Run+ . (if isLastLevel t then Map.filter (/= Delete) else id) . Map.unionsWith (if isUnion t then combineUnion else combine)+ . map runEntries -- | Combines two entries that have been performed after another. Therefore, the -- newer one overwrites the old one (or modifies it for 'Mupsert'). Only take a@@ -992,7 +1047,7 @@ | otherwise = do traceWith tr $ NewTableEvent tid conf c <- newSTRef 0- lsm <- newSTRef (LSMContent Map.empty [] NoUnion)+ lsm <- newSTRef (LSMContent emptyWriteBuffer [] NoUnion) pure (LSMHandle tid c conf lsm) inserts :: Tracer (ST s) Event -> LSM s -> [(Key, Value, Maybe Blob)] -> ST s ()@@ -1026,19 +1081,23 @@ update tr (LSMHandle tid scr conf lsmr) k entry = do traceWith tr $ UpdateEvent tid k entry sc <- readSTRef scr- content@(LSMContent wb ls unionLevel) <- readSTRef lsmr+ content@(LSMContent wb ls ul) <- readSTRef lsmr modifySTRef' scr (+1) supplyCreditsLevels (NominalCredit 1) ls invariant conf content- let wb' = Map.insertWith combine k entry wb- if bufferSize wb' >= maxWriteBufferSize conf+ let wb' = insertWriteBuffer k entry wb+ if writeBufferSize wb' >= maxWriteBufferSize conf then do- ls' <- increment (LevelEvent tid >$< tr) sc conf (bufferToRun wb') ls unionLevel- let content' = LSMContent Map.empty ls' unionLevel+ -- The merging tree might have become completed. If so, we want to+ -- migrate it now, so it can become part of new merges.+ (ls', ul') <- migrateUnionLevel (LevelEvent tid >$< tr) sc conf ls ul+ let r = flushWriteBuffer wb'+ ls'' <- increment (LevelEvent tid >$< tr) sc conf r ls' ul'+ let content' = LSMContent emptyWriteBuffer ls'' ul' invariant conf content' writeSTRef lsmr content' else- writeSTRef lsmr (LSMContent wb' ls unionLevel)+ writeSTRef lsmr (LSMContent wb' ls ul) supplyMergeCredits :: LSM s -> NominalCredit -> ST s () supplyMergeCredits (LSMHandle _ scr conf lsmr) credits = do@@ -1099,7 +1158,7 @@ Just tree -> do debt <- fst <$> remainingDebtMergingTree tree Union tree <$> newSTRef debt- lsmr <- newSTRef (LSMContent Map.empty [] unionLevel)+ lsmr <- newSTRef (LSMContent emptyWriteBuffer [] unionLevel) c <- newSTRef 0 pure (LSMHandle childTid c conf lsmr) @@ -1116,12 +1175,17 @@ -- | Return the current union debt. This debt can be reduced until it is paid -- off using 'supplyUnionCredits'.+--+-- As long as there is a union level, there is a non-zero debt. This makes it+-- clear that 'supplyUnionCredits' should still be called to trigger migration,+-- even if the merging tree itself has been completed. This becomes necessary+-- when the tree has been completed by operations on another table via sharing. remainingUnionDebt :: LSM s -> ST s UnionDebt remainingUnionDebt (LSMHandle _ _ _conf lsmr) = do LSMContent _ _ ul <- readSTRef lsmr UnionDebt <$> case ul of NoUnion -> pure 0- Union tree d -> checkedUnionDebt tree d+ Union tree d -> (+1) <$> checkedUnionDebt tree d -- | Credits are used to pay off 'UnionDebt', completing a 'union' in the -- process.@@ -1142,43 +1206,31 @@ -- This function returns any surplus of union credits as /leftover/ credits when -- a union has finished. In particular, if the returned number of credits is -- non-negative, then the union is finished.-supplyUnionCredits :: LSM s -> UnionCredits -> ST s UnionCredits-supplyUnionCredits (LSMHandle _ scr conf lsmr) (UnionCredits credits)- | credits <= 0 = pure (UnionCredits 0)- | otherwise = do- content@(LSMContent _ _ ul) <- readSTRef lsmr- UnionCredits <$> case ul of- NoUnion ->- pure credits- Union tree debtRef -> do- modifySTRef' scr (+1)- _debt <- checkedUnionDebt tree debtRef -- just to make sure it's checked- c' <- supplyCreditsMergingTree credits tree- debt' <- checkedUnionDebt tree debtRef- when (debt' > 0) $- assertST $ c' == 0 -- should have spent these credits- invariant conf content- pure c'---- TODO: At some point the completed merging tree should to moved into the--- regular levels, so it can be merged with other runs and last level merges can--- happen again to drop deletes. Also, lookups then don't need to handle the--- merging tree any more. There are two possible strategies:------ 1. As soon as the merging tree completes, move the resulting run to the--- regular levels. However, its size does generally not fit the last level,--- which requires relaxing 'invariant' and adjusting 'increment'.------ If the run is much larger than the resident and incoming runs of the last--- level, it should also not be included into a merge yet, as that merge--- would be expensive, but offer very little potential for compaction (the--- run from the merging tree is already compacted after all). So it needs to--- be bumped to the next level instead.------ 2. Initially leave the completed run in the union level. Then every time a--- new last level merge is created in 'increment', check if there is a--- completed run in the union level with a size that fits the new merge. If--- yes, move it over.+supplyUnionCredits :: Tracer (ST s) Event -> LSM s -> UnionCredits -> ST s UnionCredits+supplyUnionCredits tr (LSMHandle tid scr conf lsmr) (UnionCredits credits) = do+ traceWith tr $ SupplyUnionCreditsEvent tid credits+ if credits <= 0+ then+ pure (UnionCredits 0)+ else do+ content@(LSMContent wb ls ul) <- readSTRef lsmr+ UnionCredits <$> case ul of+ NoUnion ->+ pure credits+ Union tree debtRef -> do+ invariant conf content+ sc <- readSTRef scr+ modifySTRef' scr (+1)+ _debt <- checkedUnionDebt tree debtRef -- make sure it's checked+ c' <- supplyCreditsMergingTree credits tree+ debt' <- checkedUnionDebt tree debtRef+ when (debt' > 0) $+ assertST $ c' == 0 -- we should have spent these credits+ (ls', ul') <- migrateUnionLevel (LevelEvent tid >$< tr) sc conf ls ul+ let content' = LSMContent wb ls' ul'+ invariant conf content'+ writeSTRef lsmr content'+ pure c' -- | Like 'remainingDebtMergingTree', but additionally asserts that the debt -- never increases.@@ -1190,6 +1242,53 @@ writeSTRef debtRef debt pure debt +-- | At some point the completed merging tree should become part of the regular+-- levels, so it can be merged with other runs. Otherwise, we could never+-- perform a last level merge, which is especially important for compaction, as+-- it allows us to drop deletes. Also, lookups then don't need to consider the+-- merging tree any more.+--+-- We can do this as soon as the tree is completed by appending a new regular+-- level containing only the run that resulted from the tree merge. If the+-- run is too large to fit into the level directly after the existing ones, we+-- first add empty levels.+migrateUnionLevel :: forall s. Tracer (ST s) (EventAt EventDetail)+ -> Counter -> LSMConfig -> Levels s -> UnionLevel s+ -> ST s (Levels s, UnionLevel s)+migrateUnionLevel _ _ _ ls NoUnion = do+ -- nothing to do+ pure (ls, NoUnion)+migrateUnionLevel tr sc conf ls ul@(Union t _) =+ getCompletedMergingTree t >>= \case+ Nothing ->+ -- Still in progress, leave it.+ pure (ls, ul)+ Just r -> do+ -- Before migration, there is usually a last regular (i.e. non-union)+ -- level, which uses tiering. We could potentially add the completed+ -- union (or rather the run it resulted in) directly to the resident+ -- tiering runs of that last regular level, but that would clash with+ -- some invariants. Instead, we always create a new last level, which+ -- only makes a small difference, but keeps the invariants simpler.+ --+ -- Also note even empty runs get migrated. This doesn't violate any+ -- invariants, as levelling levels can already contain empty runs.+ -- Dropping the run would be more complicated since the previous level+ -- then suddenly becomes the last one while potentially still containing+ -- midlevel merges.+ let levelNo = maximum+ [ 2 -- First level is tiering, don't migrate there.+ , length ls + 1 -- Must come after existing levels.+ , runToLevelNumber LevelLevelling conf r+ -- Run must not be too large for the level.+ ]+ let emptyLevels = replicate (levelNo - 1 - length ls) EmptyLevel+ let level = Level (Single FromMigratedUnion r) []+ let levels = ls ++ emptyLevels ++ [level]+ assertST $ length levels == levelNo+ traceWith tr $ EventAt sc levelNo $ UnionLevelMigratedEvent r+ pure (levels, NoUnion)+ ------------------------------------------------------------------------------- -- Lookups --@@ -1212,9 +1311,9 @@ -- -- In the real implementation, this is done not on an individual 'LookupAcc', -- but one for each key, i.e. @Vector (Maybe Entry)@.-doLookup :: Buffer -> [Run] -> UnionLevel s -> Key -> ST s (LookupResult Value Blob)+doLookup :: WriteBuffer -> [Run] -> UnionLevel s -> Key -> ST s (LookupResult Value Blob) doLookup wb runs ul k = do- let acc0 = lookupBatch (Map.lookup k wb) k runs+ let acc0 = lookupBatch (lookupWriteBuffer k wb) k runs case ul of NoUnion -> pure (convertAcc acc0)@@ -1222,7 +1321,7 @@ treeBatches <- buildLookupTree tree let treeResults = lookupBatch Nothing k <$> treeBatches pure $ convertAcc $ foldLookupTree $- if null wb && null runs+ if writeBufferSize wb == 0 && null runs then treeResults else LookupNode MergeLevel [LookupBatch acc0, treeResults ] where@@ -1239,7 +1338,7 @@ -- In a real implementation, this would take all keys at once and be in IO. lookupBatch :: LookupAcc -> Key -> [Run] -> LookupAcc lookupBatch acc k rs =- let entries = [entry | r <- rs, Just entry <- [Map.lookup k r]]+ let entries = [entry | r <- rs, Just entry <- [lookupRun k r]] in foldl (updateAcc combine) acc entries data LookupTree a = LookupBatch a@@ -1333,11 +1432,12 @@ -- inserting without calling 'supplyUnionCredits'. supplyCreditsLevels :: NominalCredit -> Levels s -> ST s () supplyCreditsLevels nominalDeposit =- traverse_ $ \(Level ir _rs) -> do- case ir of- Single{} -> pure ()- Merging _mp nominalDebt nominalCreditVar- mr@(MergingRun _ physicalDebt _) -> do+ traverse_ $ \lvl -> do+ case lvl of+ EmptyLevel -> pure ()+ Level Single{} _ -> pure ()+ Level (Merging _mp nominalDebt nominalCreditVar+ mr@(MergingRun _ physicalDebt _)) _ -> do nominalCredit <- depositNominalCredit nominalDebt nominalCreditVar nominalDeposit@@ -1438,9 +1538,17 @@ where tr' = contramap (EventAt sc ln) tr + go !ln incoming (EmptyLevel : ls) = do+ assertST $ mergePolicyForLevel ln ls ul == LevelTiering -- mid level+ traceWith tr' LevelWasEmptyEvent+ ir <- newLevelMerge tr' conf ln LevelTiering (mergeTypeFor ls) incoming+ pure (Level ir [] : ls)+ where+ tr' = contramap (EventAt sc ln) tr+ go !ln incoming (Level ir rs : ls) = do r <- case ir of- Single r -> do+ Single _ r -> do traceWith tr' $ SingleRunCompletedEvent r pure r Merging mergePolicy _ _ mr -> do@@ -1511,10 +1619,18 @@ -> LSMConfig -> Int -> MergePolicyForLevel -> LevelMergeType -> [Run] -> ST s (IncomingRun s)-newLevelMerge tr _ _ _ _ [r] = do+newLevelMerge tr _ ln mergePolicy _ [r] = do+ origin <-+ if ln <= 1+ then do+ assertST $ ln == 1+ pure FromWriteBuffer+ else do+ assertST $ mergePolicy == LevelLevelling+ pure FromLevellingLevel traceWith tr $ NewSingleRunEvent r- pure (Single r)-newLevelMerge tr conf@LSMConfig{..} level mergePolicy mergeType rs = do+ pure (Single origin r)+newLevelMerge tr conf@LSMConfig{..} ln mergePolicy mergeType rs = do mergingRun@(MergingRun _ physicalDebt _) <- newMergingRun mergeType rs traceWith tr NewLevelMergeEvent { mergePolicy,@@ -1530,7 +1646,7 @@ -- The nominal debt equals the minimum of credits we will supply before we -- expect the merge to complete. This is the same as the number of updates -- in a run that gets moved to this level.- nominalDebt = NominalDebt (levelNumberToMaxRunSize LevelTiering conf level)+ nominalDebt = NominalDebt (levelNumberToMaxRunSize LevelTiering conf ln) -- The physical debt is the number of actual merge steps we will need to -- perform before the merge is complete. This is always the sum of the@@ -1548,16 +1664,16 @@ LevelLevelling -> -- Incoming runs, which may be slightly overfull with respect to the -- previous level- configSizeRatio * levelNumberToMaxRunSize LevelTiering conf level+ configSizeRatio * levelNumberToMaxRunSize LevelTiering conf ln -- The single run that was already on this level- + levelNumberToMaxRunSize LevelLevelling conf level+ + levelNumberToMaxRunSize LevelLevelling conf ln LevelTiering -> -- Incoming runs, which may be slightly overfull with respect to the -- previous level- configSizeRatio * levelNumberToMaxRunSize LevelTiering conf level+ configSizeRatio * levelNumberToMaxRunSize LevelTiering conf ln -- Held back run that is underfull with respect to the current -- level- + levelNumberToMaxRunSize LevelTiering conf (level - 1)+ + levelNumberToMaxRunSize LevelTiering conf (ln - 1) ------------------------------------------------------------------------------- -- MergingTree abstraction@@ -1600,24 +1716,19 @@ -- | Ensures that the merge contains more than one input, avoiding creating a -- pending merge where possible.-newPendingLevelMerge :: [IncomingRun s]+newPendingLevelMerge :: [PreExistingRun s] -> Maybe (MergingTree s) -> ST s (Maybe (MergingTree s)) newPendingLevelMerge [] t = pure t-newPendingLevelMerge [Single r] Nothing =+newPendingLevelMerge [PreExistingRun r] Nothing = Just . MergingTree <$> newSTRef (CompletedTreeMerge r)-newPendingLevelMerge [Merging{}] Nothing =+newPendingLevelMerge [PreExistingMergingRun{}] Nothing = -- This case should never occur. If there is a single entry in the list, -- there can only be one level in the input table. At level 1 there are no- -- merging runs, so it must be a PreExistingRun.+ -- merging runs, it must be a Single run flushed from a write buffer. error "newPendingLevelMerge: singleton Merging run"-newPendingLevelMerge irs tree = do- let prs = map incomingToPreExistingRun irs- st = PendingTreeMerge (PendingLevelMerge prs tree)- Just . MergingTree <$> newSTRef st- where- incomingToPreExistingRun (Single r) = PreExistingRun r- incomingToPreExistingRun (Merging _ _ _ mr) = PreExistingMergingRun mr+newPendingLevelMerge prs t = do+ Just . MergingTree <$> newSTRef (PendingTreeMerge (PendingLevelMerge prs t)) -- | Ensures that the merge contains more than one input. newPendingUnionMerge :: [MergingTree s] -> ST s (Maybe (MergingTree s))@@ -1629,15 +1740,20 @@ contentToMergingTree :: LSMContent s -> ST s (Maybe (MergingTree s)) contentToMergingTree (LSMContent wb ls ul) =- newPendingLevelMerge (buffers ++ levels) trees+ newPendingLevelMerge (maybeToList buffer ++ levels) trees where -- flush the write buffer (but this should not modify the content)- buffers- | bufferSize wb == 0 = []- | otherwise = [Single (bufferToRun wb)]+ buffer+ | writeBufferSize wb == 0 = Nothing+ | otherwise = Just (PreExistingRun (flushWriteBuffer wb)) - levels = flip concatMap ls $ \(Level ir rs) -> ir : map Single rs+ levels = flip concatMap ls $ \case+ EmptyLevel -> []+ Level ir rs -> toPreExisting ir : map PreExistingRun rs + toPreExisting (Single _ r) = PreExistingRun r+ toPreExisting (Merging _ _ _ mr) = PreExistingMergingRun mr+ trees = case ul of NoUnion -> Nothing Union t _ -> Just t@@ -1791,6 +1907,12 @@ expectCompletedMergingTree :: HasCallStack => MergingTree s -> ST s Run expectCompletedMergingTree = expectInvariant . isCompletedMergingTree +getCompletedMergingTree :: MergingTree s -> ST s (Maybe Run)+getCompletedMergingTree t =+ evalInvariant (isCompletedMergingTree t) >>= \case+ Left _ -> pure Nothing+ Right r -> pure (Just r)+ ------------------------------------------------------------------------------- -- Measurements --@@ -1799,7 +1921,7 @@ | MNode TreeMergeType [MTree r] deriving stock (Eq, Foldable, Functor, Show) -allLevels :: LSM s -> ST s (Buffer, [[Run]], Maybe (MTree Run))+allLevels :: LSM s -> ST s (WriteBuffer, [[Run]], Maybe (MTree Run)) allLevels (LSMHandle _ _ _conf lsmr) = do LSMContent wb ls ul <- readSTRef lsmr rs <- flattenLevels ls@@ -1812,11 +1934,12 @@ flattenLevels = mapM flattenLevel flattenLevel :: Level s -> ST s [Run]+flattenLevel EmptyLevel = pure [] flattenLevel (Level ir rs) = (++ rs) <$> flattenIncomingRun ir flattenIncomingRun :: IncomingRun s -> ST s [Run] flattenIncomingRun = \case- Single r -> pure [r]+ Single _ r -> pure [r] Merging _ _ _ mr -> flattenMergingRun mr flattenMergingRun :: MergingRun t s -> ST s [Run]@@ -1851,8 +1974,9 @@ (wb, levels, tree) <- allLevels lsm let r = mergek MergeLevel- (wb : concat levels ++ toList (mergeTree <$> tree))- pure (Map.mapMaybe justInsert r)+ (Run (bufferEntries wb) -- we don't flush, but treat wb as a run+ : concat levels ++ toList (mergeTree <$> tree))+ pure (Map.mapMaybe justInsert (runEntries r)) where mergeTree :: MTree Run -> Run mergeTree (MLeaf r) = r@@ -1862,7 +1986,7 @@ justInsert Delete = Nothing justInsert (Mupsert v) = Just (v, Nothing) -type Representation = (Run, [LevelRepresentation], Maybe (MTree Run))+type Representation = (WriteBuffer, [LevelRepresentation], Maybe (MTree Run)) type LevelRepresentation = (Maybe (MergePolicyForLevel, NominalDebt, NominalCredit,@@ -1879,7 +2003,9 @@ pure (wb, levels, tree) dumpLevel :: Level s -> ST s LevelRepresentation-dumpLevel (Level (Single r) rs) =+dumpLevel EmptyLevel =+ pure (Nothing, [])+dumpLevel (Level (Single _ r) rs) = pure (Nothing, (r:rs)) dumpLevel (Level (Merging mp nd ncv (MergingRun mt _ ref)) rs) = do mrs <- readSTRef ref@@ -1892,31 +2018,30 @@ representationShape :: Representation -> (Int, [([Int], [Int])], Maybe (MTree Int)) representationShape (wb, levels, tree) =- (summaryRun wb, map summaryLevel levels, fmap (fmap summaryRun) tree)+ (writeBufferSize wb, map summaryLevel levels, fmap (fmap runSize) tree) where summaryLevel (mmr, rs) = let (ongoing, complete) = summaryMR mmr- in (ongoing, complete <> map summaryRun rs)-- summaryRun = runSize+ in (ongoing, complete <> map runSize rs) summaryMR = \case Nothing -> ([], [])- Just (_, _, _, _, CompletedMerge r) -> ([], [summaryRun r])- Just (_, _, _, _, OngoingMerge _ rs _) -> (map summaryRun rs, [])+ Just (_, _, _, _, CompletedMerge r) -> ([], [runSize r])+ Just (_, _, _, _, OngoingMerge _ rs _) -> (map runSize rs, []) ------------------------------------------------------------------------------- -- Tracing -- -- TODO: these events are incomplete, in particular we should also trace what--- happens in the union level.+-- happens in the union level. Somewhat related: issue #445. data Event = NewTableEvent TableId LSMConfig | UpdateEvent TableId Key Entry | LookupEvent TableId Key | DuplicateEvent TableId TableId | UnionsEvent TableId [TableId]+ | SupplyUnionCreditsEvent TableId Credit | LevelEvent TableId (EventAt EventDetail) deriving stock Show @@ -1946,9 +2071,12 @@ } | SingleRunCompletedEvent Run + | UnionLevelMigratedEvent Run+ | RunTooSmallForLevelEvent MergePolicyForLevel Run | LevelIsFullEvent MergePolicyForLevel | LevelIsNotFullEvent MergePolicyForLevel+ | LevelWasEmptyEvent deriving stock Show -------------------------------------------------------------------------------@@ -1974,8 +2102,26 @@ , (1, pure Delete) ] +instance QC.Arbitrary Run where+ arbitrary = Run <$> QC.arbitrary+ shrink = map Run . QC.shrink . runEntries+ instance QC.Arbitrary LevelMergeType where arbitrary = QC.elements [MergeMidLevel, MergeLastLevel] instance QC.Arbitrary TreeMergeType where arbitrary = QC.elements [MergeLevel, MergeUnion]++instance QC.Arbitrary LSMConfig where+ arbitrary = do+ configMaxWriteBufferSize <- QC.chooseInt (1, 10)+ configSizeRatio <- QC.chooseInt (2, 8)+ pure LSMConfig {configMaxWriteBufferSize, configSizeRatio}+ shrink (LSMConfig size ratio) =+ [ LSMConfig size' ratio'+ | (size', ratio') <- QC.liftShrink2 QC.shrink shrinkSizeRatio (size, ratio)+ , size' >= 1+ ]+ where+ shrinkSizeRatio 4 = []+ shrinkSizeRatio _ = [4] -- try shrinking to four, the default ratio
test-prototypes/Main.hs view
@@ -5,6 +5,7 @@ import qualified Test.FormatPage import qualified Test.ScheduledMerges import qualified Test.ScheduledMerges.RunSizes+import qualified Test.ScheduledMergesDL import qualified Test.ScheduledMergesQLS main :: IO ()@@ -13,4 +14,5 @@ , Test.ScheduledMerges.tests , Test.ScheduledMerges.RunSizes.tests , Test.ScheduledMergesQLS.tests+ , Test.ScheduledMergesDL.tests ]
test-prototypes/Test/ScheduledMerges.hs view
@@ -13,7 +13,8 @@ import ScheduledMerges as LSM import qualified Test.QuickCheck as QC-import Test.QuickCheck (Arbitrary (arbitrary, shrink), Property)+import Test.QuickCheck (Arbitrary (arbitrary, shrink), Property,+ (.&&.)) import Test.QuickCheck.Exception (isDiscard) import Test.Tasty import Test.Tasty.HUnit (HasCallStack, testCase)@@ -24,7 +25,7 @@ tests = testGroup "Test.ScheduledMerges" [ testCase "test_regression_empty_run" test_regression_empty_run , testCase "test_merge_again_with_incoming" test_merge_again_with_incoming- , testProperty "prop_union" prop_union+ , testProperty "prop_union_complete" prop_union_complete , testGroup "T" [ localOption (QuickCheckTests 1000) $ -- super quick, run more testProperty "Arbitrary satisfies invariant" prop_arbitrarySatisfiesInvariant@@ -176,35 +177,114 @@ -- properties -- --- | Supplying enough credits for the remaining debt completes the union merge.-prop_union :: [[(LSM.Key, LSM.Entry)]] -> Property-prop_union kess = length (filter (not . null) kess) > 1 QC.==>+-- | Supplying enough credits for the remaining debt completes the union merge+-- (as externally observable through 'LSM.remainingUnionDebt'). However, a+-- special union level remains.+prop_union_complete :: LSMConfig -> NestedUnionData -> Property+prop_union_complete conf nestedUnionData = QC.ioProperty $ runWithTracer $ \tr -> stToIO $ do- ts <- traverse (uncurry $ mkTable tr) (zip [LSM.TableId 0..] kess)- t <- LSM.unions tr (LSM.TableId (length kess)) ts+ tidCounter <- newSTRef (LSM.TableId 0)+ t <- mkNestedUnion tr conf tidCounter nestedUnionData + rep <- dumpRepresentation t debt@(UnionDebt x) <- LSM.remainingUnionDebt t- _ <- LSM.supplyUnionCredits t (UnionCredits x)++ leftovers <- LSM.supplyUnionCredits tr t (UnionCredits x)++ rep' <- dumpRepresentation t debt' <- LSM.remainingUnionDebt t - rep <- dumpRepresentation t pure $ QC.counterexample (show (debt, debt')) $ QC.conjoin- [ debt =/= UnionDebt 0- , debt' === UnionDebt 0- , hasUnionWith isCompleted rep+ [ QC.counterexample "before" $+ -- The input is a non-empty list of structurally non-empty tables+ -- (i.e. they have runs or at least a non-empty write buffer).+ -- Therefore there must be a merging tree.+ debt =/= UnionDebt 0+ .&&. hasUnionLevelWith (not . isCompleted) rep+ , QC.counterexample "after" $+ debt' === UnionDebt 0+ .&&. hasNoUnionLevel rep'+ , QC.counterexample "leftovers" $+ leftovers >= 0 ] where isCompleted = \case MLeaf{} -> True MNode{} -> False -mkTable :: Tracer (ST s) Event -> LSM.TableId -> [(LSM.Key, LSM.Entry)] -> ST s (LSM s)-mkTable tr tid ks = do- t <- LSM.new tr tid++-- | For simplicity, this is not a recursive structure. We just nest once, or+-- not at all if there is just a single 'UnionData'.+newtype NestedUnionData = NestedUnionData [UnionData]+ deriving stock Show++instance Arbitrary NestedUnionData where+ arbitrary = do+ numUnionInputs <- QC.chooseInt (1, 10)+ NestedUnionData <$> QC.vectorOf numUnionInputs arbitrary++ shrink (NestedUnionData unionInputs) =+ [ NestedUnionData unionInputs'+ | unionInputs' <- shrink unionInputs+ , not (null unionInputs')+ ]++-- | Inputs to a union, plus some extra updates to perform on the result.+-- Note that we want at least two inputs, so there is some merging required.+data UnionData = UnionData [(LSM.Key, LSM.Entry)] [TableData]+ deriving stock Show++unionDataInvariant :: UnionData -> Bool+unionDataInvariant (UnionData _ tableInputs) = length tableInputs >= 2++instance Arbitrary UnionData where+ arbitrary = do+ numUnionInputs <- QC.oneof [pure 2, QC.chooseInt (3, 6)]+ UnionData <$> arbitrary <*> QC.vectorOf numUnionInputs arbitrary++ shrink (UnionData kes tableInputs) =+ [ data'+ | (kes', tableInputs') <- shrink (kes, tableInputs)+ , let data' = UnionData kes' tableInputs'+ , unionDataInvariant data'+ ]++newtype TableData = TableData [(LSM.Key, LSM.Entry)]+ deriving stock Show+ deriving Arbitrary+ via QC.NonEmptyList (LSM.Key, LSM.Entry)++mkNestedUnion :: Tracer (ST s) Event -> LSMConfig -> STRef s LSM.TableId+ -> NestedUnionData -> ST s (LSM s)+mkNestedUnion tr conf tidCounter (NestedUnionData unionInputs) = do+ tid <- freshTableId tidCounter+ ts <- traverse (mkUnion tr conf tidCounter) unionInputs+ LSM.unions tr tid ts++mkUnion :: Tracer (ST s) Event -> LSMConfig -> STRef s LSM.TableId+ -> UnionData -> ST s (LSM s)+mkUnion tr conf tidCounter (UnionData kes tableInputs) = do+ tid <- freshTableId tidCounter+ ts <- traverse (mkTable tr conf tidCounter) tableInputs+ table <- LSM.unions tr tid ts+ LSM.updates tr table kes+ pure table++mkTable :: Tracer (ST s) Event -> LSMConfig -> STRef s LSM.TableId+ -> TableData -> ST s (LSM s)+mkTable tr conf tidCounter (TableData ks) = do+ tid <- freshTableId tidCounter+ t <- LSM.newWith tr tid conf LSM.updates tr t ks pure t +freshTableId :: STRef s LSM.TableId -> ST s LSM.TableId+freshTableId ref = do+ tid <- readSTRef ref+ modifySTRef' ref succ+ pure tid+ ------------------------------------------------------------------------------- -- tests for MergingTree --@@ -448,7 +528,7 @@ -- genMergeCreditForRuns :: [NonEmptyRun] -> QC.Gen (MergeDebt, MergeCredit) genMergeCreditForRuns rs = do- let totalDebt = sum (map (length . getNonEmptyRun) rs)+ let totalDebt = sum (map (runSize . getNonEmptyRun) rs) suppliedCredits <- QC.chooseInt (0, totalDebt-1) unspentCredits <- QC.chooseInt (0, min (mergeBatchSize-1) suppliedCredits) let spentCredits = suppliedCredits - unspentCredits@@ -472,7 +552,7 @@ shrinkMergeCreditForRuns rs' MergeCredit {spentCredits, unspentCredits} = [ assert (mergeDebtInvariant md' mc') (md', mc')- | let totalDebt' = sum (map (length . getNonEmptyRun) rs')+ | let totalDebt' = sum (map (runSize . getNonEmptyRun) rs') , suppliedCredits' <- shrink (min (spentCredits+unspentCredits) (totalDebt'-1)) , unspentCredits' <- shrink (min unspentCredits suppliedCredits')@@ -487,8 +567,8 @@ ] instance Arbitrary NonEmptyRun where- arbitrary = NonEmptyRun <$> (arbitrary `QC.suchThat` (not . null))- shrink (NonEmptyRun r) = [NonEmptyRun r' | r' <- shrink r, not (null r')]+ arbitrary = NonEmptyRun <$> (arbitrary `QC.suchThat` (\r -> runSize r > 0))+ shrink (NonEmptyRun r) = [NonEmptyRun r' | r' <- shrink r, runSize r' > 0] prop_arbitrarySatisfiesInvariant :: T -> Property prop_arbitrarySatisfiesInvariant t =@@ -551,10 +631,16 @@ , "actual shape: " <> show shape ] -hasUnionWith :: (MTree Int -> Bool) -> Representation -> Property-hasUnionWith p rep = do+hasNoUnionLevel :: Representation -> Property+hasNoUnionLevel rep = do let (_, _, shape) = representationShape rep- QC.counterexample "expected suitable Union" $+ QC.counterexample "expected no union level" $+ Nothing === shape++hasUnionLevelWith :: (MTree Int -> Bool) -> Representation -> Property+hasUnionLevelWith p rep = do+ let (_, _, shape) = representationShape rep+ QC.counterexample "expected suitable union level" $ QC.counterexample (show shape) $ case shape of Nothing -> False
+ test-prototypes/Test/ScheduledMergesDL.hs view
@@ -0,0 +1,47 @@+{-# OPTIONS_GHC -Wno-orphans #-}++module Test.ScheduledMergesDL (tests) where++import ScheduledMerges as LSM+import Test.ScheduledMergesQLS as QLS (Action (..), Model, prop_LSM)++import Test.QuickCheck (NonNegative (..), Property)+import Test.QuickCheck.DynamicLogic+import Test.QuickCheck.StateModel.Lockstep hiding (ModelOp)+import Test.Tasty (TestTree, testGroup)+import Test.Tasty.QuickCheck (testProperty)++tests :: TestTree+tests = testGroup "Test.ScheduledMergesDL" [+ testProperty "prop_empty_union" prop_empty_union+ ]++instance DynLogicModel (Lockstep Model)++prop_empty_union :: Property+prop_empty_union = forAllDL dl_empty_union prop_LSM++-- | Create a table with an empty run in the union level, then run arbitrary+-- actions to see if all invariants still hold.+dl_empty_union :: DL (Lockstep Model) ()+dl_empty_union = do+ -- Create a union level that will result in an empty run. This is easy to+ -- achieve if the inputs to the union only contain deletes (which will get+ -- removed in a last level merge, which the union merges are).+ tInput <- action $ ANew conf+ _ <- action $ ADelete (unsafeMkGVar tInput opIdentity) (Right (K 0))+ tUnion <- action $ AUnions [unsafeMkGVar tInput opIdentity, unsafeMkGVar tInput opIdentity]++ -- Perform some actions so tUnion gets regular levels as well.+ anyActions_++ -- Now complete the union.+ _ <- action $ ASupplyUnion (unsafeMkGVar tUnion opIdentity) (NonNegative (UnionCredits 100))++ anyActions_+ where+ conf =+ LSMConfig+ { configMaxWriteBufferSize = 2+ , configSizeRatio = 2+ }
test-prototypes/Test/ScheduledMergesQLS.hs view
@@ -3,7 +3,12 @@ {-# OPTIONS_GHC -Wno-orphans #-} {-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} -module Test.ScheduledMergesQLS (tests) where+module Test.ScheduledMergesQLS (+ tests+ , Model+ , prop_LSM+ , Action (..)+ ) where import Control.Monad.Reader (ReaderT (..)) import Control.Monad.ST@@ -404,7 +409,7 @@ AUnions vars -> do tid <- incrTidVar tidVar stToIO $ unions tr tid (map lookUpVar vars)- ASupplyUnion var c -> stToIO $ supplyUnionCredits (lookUpVar var) (getNonNegative c) >> pure ()+ ASupplyUnion var c -> stToIO $ supplyUnionCredits tr (lookUpVar var) (getNonNegative c) >> pure () ADump var -> stToIO $ logicalValue (lookUpVar var) where lookUpVar :: ModelVar Model a -> a
test/Database/LSMTree/Model/Session.hs view
@@ -598,7 +598,7 @@ }) openTableFromSnapshot ::- forall k v b m.(+ forall k v b m. ( MonadState Model m , MonadError Err m , C k v b
test/Test/Database/LSMTree/Internal/BloomFilter.hs view
@@ -44,6 +44,8 @@ prop_total_deserialisation_whitebox , testProperty "bloomQueries (bulk)" $ prop_bloomQueries+ , testProperty "prop_packUnpack_RunIxKeyIx" prop_packUnpack_RunIxKeyIx+ , testProperty "prop_packUnpack_RunIxKeyIx_limits" prop_packUnpack_RunIxKeyIx_limits ] testSalt :: Bloom.Salt@@ -168,3 +170,50 @@ === map (\(RunIxKeyIx rix kix) -> (rix, kix)) (VP.toList (bloomQueries testSalt (V.fromList filters') (V.fromList keys')))++{-------------------------------------------------------------------------------+ RunIxKeyIx+-------------------------------------------------------------------------------}++-- | Test that 'RunIx' and 'KeyIx' roundtrip through the 'RunIxKeyIx' pattern+-- synonym+--+-- More specifically, if we apply a 'RunIxKeyIx' pattern synonym to a pair of+-- 'RunIx' and 'KeyIx', and then pattern match on it agains, then we would+-- expect to get the same 'RunIx' and 'KeyIx' out as the ones we put in.+--+-- There used to be a bug where this went wrong because of a typo in a bit-mask.+-- This property test should ensure that we catch such mistakes in the future.+-- See PR #841 for more information.+--+-- <PR https://github.com/IntersectMBO/lsm-tree/pull/841>+--+prop_packUnpack_RunIxKeyIx :: Int_0xffff -> Int_0xffff -> Property+prop_packUnpack_RunIxKeyIx r k =+ case RunIxKeyIx r.unwrap k.unwrap of+ RunIxKeyIx r' k' -> r.unwrap === r' .&&. k.unwrap === k'++-- | A variant of 'prop_packUnpack_RunIxKeyIx' applied to 'RunIx' and 'KeyIx'+-- that are close to their upper bounds.+prop_packUnpack_RunIxKeyIx_limits :: Property+prop_packUnpack_RunIxKeyIx_limits = conjoin [+ prop_packUnpack_RunIxKeyIx 0xffff 0xffff+ , prop_packUnpack_RunIxKeyIx (0xffff - 1) 0xffff+ , prop_packUnpack_RunIxKeyIx 0xffff (0xffff - 1)+ , prop_packUnpack_RunIxKeyIx (0xffff - 1) (0xffff - 1)+ ]++-- | An Int in the inclusive range @(0, 0xffff)@+newtype Int_0xffff = Int_0xffff { unwrap :: Int }+ deriving stock (Show, Eq)+ deriving newtype Num++instance Arbitrary Int_0xffff where+ arbitrary = Int_0xffff <$> chooseInt (0, 0xffff)+ shrink x = [+ Int_0xffff y+ | y <- shrink x.unwrap+ , 0 <= y+ , y < 0xffff+ ]+
test/Test/Database/LSMTree/Internal/Snapshot/Codec/Golden.hs view
@@ -7,9 +7,11 @@ , Annotation ) where +import Codec.CBOR.Read (deserialiseFromBytes) import Codec.CBOR.Write (toLazyByteString) import Control.Monad (when)-import qualified Data.ByteString.Lazy as BSL (writeFile)+import Control.Monad.Class.MonadThrow (Exception (displayException))+import qualified Data.ByteString.Lazy as BSL import qualified Data.Set as Set import Data.Typeable import qualified Data.Vector as V@@ -38,14 +40,22 @@ import qualified Test.Tasty as Tasty import Test.Tasty (TestTree, testGroup) import qualified Test.Tasty.Golden as Au+import Test.Tasty.HUnit (Assertion, assertEqual, assertFailure,+ testCase) import Test.Tasty.QuickCheck (testProperty) tests :: TestTree tests =- handleOutputFiles $- testGroup "Test.Database.LSMTree.Internal.Snapshot.Codec.Golden" $- concat (forallSnapshotTypes snapshotCodecGoldenTest)- ++ [testProperty "prop_noUnexpectedOrMissingGoldenFiles" prop_noUnexpectedOrMissingGoldenFiles]+ testGroup "Test.Database.LSMTree.Internal.Snapshot.Codec.Golden" [+ handleOutputFiles $+ testGroup "Generate golden files" $+ concat (forallSnapshotTypes snapshotCodecGoldenTest) +++ [ testProperty "prop_noUnexpectedOrMissingGoldenFiles" prop_noUnexpectedOrMissingGoldenFiles+ ]+ , testGroup "Backwards compatibility" [+ testCase "test_compatTableConfigV0" test_compatTableConfigV0+ ]+ ] {------------------------------------------------------------------------------- Configuration@@ -74,12 +84,12 @@ -> [TestTree] snapshotCodecGoldenTest proxy = [ go annotation datum- | (annotation, datum) <- enumGoldenAnnotated' proxy+ | (annotation, datum) <- enumGoldenAnnotated' proxy v ] where+ v = currentSnapshotVersion go ann datum =- let v = currentSnapshotVersion- outputFilePath = goldenDataFilePath </> filePathOutput proxy ann v+ let outputFilePath = goldenDataFilePath </> filePathOutput proxy ann v goldenFilePath = goldenDataFilePath </> filePathGolden proxy ann v -- IO actions@@ -99,10 +109,11 @@ -- | Check that are no missing or unexpected files in the output directory prop_noUnexpectedOrMissingGoldenFiles :: Property prop_noUnexpectedOrMissingGoldenFiles = once $ ioProperty $ do- let expectedFiles = Set.fromList $ concat $ forallSnapshotTypes $ \p -> concat [- filePathsGolden p v- | v <- supportedVersions p- ]+ let expectedFiles = Set.fromList $ concat $ forallSnapshotTypes $ \p ->+ concat [+ filePathsGolden p v+ | v <- allCompatibleSnapshotVersions+ ] let hfs = ioHasFS goldenDataMountPoint actualDirectoryEntries <- FS.listDirectory hfs (FS.mkFsPath [])@@ -122,6 +133,43 @@ pure $ propMissing .&&. propUnexpected {-------------------------------------------------------------------------------+ Backwards compatibility tests+-------------------------------------------------------------------------------}++test_compatTableConfigV0 :: Assertion+test_compatTableConfigV0 =+ assertGoldenFileDecodesTo (Proxy @TableConfig) "A" V0 $+ TableConfig {+ confMergePolicy = singGolden V0+ , confMergeSchedule = singGolden V0+ , confSizeRatio = singGolden V0+ , confWriteBufferAlloc = singGolden V0+ , confBloomFilterAlloc = singGolden V0+ , confFencePointerIndex = singGolden V0+ , confDiskCachePolicy = singGolden V0+ -- MergeBatchSize not included in V0, decoder uses WriteBufferAlloc+ , confMergeBatchSize = MergeBatchSize magicNumber2+ }++-- | For types that changed their snapshot format between versions, we should+-- also test that we can in fact still decode the old format.+assertGoldenFileDecodesTo ::+ (DecodeVersioned a, Eq a, Show a, Typeable a)+ => Proxy a+ -> String+ -> SnapshotVersion+ -> a+ -> Assertion+assertGoldenFileDecodesTo proxy ann v expected = do+ let fp = goldenDataFilePath </> filePathGolden proxy ann v+ lbs <- BSL.readFile fp+ case deserialiseFromBytes (decodeVersioned v) lbs of+ Left err ->+ assertFailure $ "Error decoding " ++ fp ++ ": " ++ displayException err+ Right (_, decoded) ->+ assertEqual "" expected decoded++{------------------------------------------------------------------------------- Mapping -------------------------------------------------------------------------------} @@ -206,31 +254,32 @@ -- -- The default implementation is to return a singleton list containing -- 'singGolden'.- enumGolden :: [a]- enumGolden = [ singGolden ]+ enumGolden :: SnapshotVersion -> [a]+ enumGolden v = [ singGolden v ] -- | Enumerated values with an annotation for naming purposes. The enumeration -- should be /shallow/, and the annotations should be unique. -- -- The default implementation is to annotate 'enumGolden' with capital letters -- starting with @\'A\'@.- enumGoldenAnnotated :: [(Annotation, a)]- enumGoldenAnnotated = zip [[c] | c <- ['A' .. 'Z']] enumGolden+ enumGoldenAnnotated :: SnapshotVersion -> [(Annotation, a)]+ enumGoldenAnnotated v = zip [[c] | c <- ['A' .. 'Z']] (enumGolden v) -- | A singleton enumerated value. This is mainly useful for superclass -- instances. -- -- The default implementation is to take the 'head' of 'enumGoldenAnnotated'.- singGolden :: a- singGolden = snd $ head enumGoldenAnnotated-- supportedVersions :: Proxy a -> [SnapshotVersion]- supportedVersions _ = allCompatibleSnapshotVersions+ singGolden :: SnapshotVersion -> a+ singGolden v = snd $ head (enumGoldenAnnotated v) type Annotation = String -enumGoldenAnnotated' :: EnumGolden a => Proxy a -> [(Annotation, a)]-enumGoldenAnnotated' _ = enumGoldenAnnotated+enumGoldenAnnotated' ::+ EnumGolden a+ => Proxy a+ -> SnapshotVersion+ -> [(Annotation, a)]+enumGoldenAnnotated' _ v = enumGoldenAnnotated v {------------------------------------------------------------------------------- Enumeration class: names and file paths@@ -250,7 +299,7 @@ -> [String] filePathsGolden p v = [ filePathGolden p annotation v- | (annotation, _) <- enumGoldenAnnotated' p+ | (annotation, _) <- enumGoldenAnnotated' p v ] filePathOutput :: Typeable a => Proxy a -> String -> SnapshotVersion -> String@@ -264,13 +313,14 @@ -------------------------------------------------------------------------------} instance EnumGolden SnapshotMetaData where- singGolden = SnapshotMetaData singGolden singGolden singGolden singGolden singGolden+ singGolden v = SnapshotMetaData (singGolden v) (singGolden v) (singGolden v)+ (singGolden v) (singGolden v) where _coveredAllCases = \case SnapshotMetaData{} -> () instance EnumGolden SnapshotLabel where- enumGolden = [+ enumGolden _v = [ SnapshotLabel "UserProvidedLabel" , SnapshotLabel "" ]@@ -279,47 +329,48 @@ SnapshotLabel{} -> () instance EnumGolden TableConfig where- singGolden = TableConfig singGolden singGolden singGolden singGolden- singGolden singGolden singGolden singGolden+ singGolden v = TableConfig (singGolden v) (singGolden v) (singGolden v)+ (singGolden v) (singGolden v) (singGolden v)+ (singGolden v) (singGolden v) where _coveredAllCases = \case TableConfig{} -> () instance EnumGolden MergePolicy where- singGolden = LazyLevelling+ singGolden _v = LazyLevelling where _coveredAllCases = \case LazyLevelling{} -> () instance EnumGolden SizeRatio where- singGolden = Four+ singGolden _v = Four where _coveredAllCases = \case Four{} -> () instance EnumGolden WriteBufferAlloc where- singGolden = AllocNumEntries magicNumber2+ singGolden _v = AllocNumEntries magicNumber2 where _coveredAllCases = \case AllocNumEntries{} -> () instance EnumGolden BloomFilterAlloc where- enumGolden = [ AllocFixed magicNumber3, AllocRequestFPR pi ]+ enumGolden _v = [ AllocFixed magicNumber3, AllocRequestFPR pi ] where _coveredAllCases = \case AllocFixed{} -> () AllocRequestFPR{} -> () instance EnumGolden FencePointerIndexType where- enumGolden = [ CompactIndex, OrdinaryIndex ]+ enumGolden _v = [ CompactIndex, OrdinaryIndex ] where _coveredAllCases = \case CompactIndex{} -> () OrdinaryIndex{} -> () instance EnumGolden DiskCachePolicy where- enumGolden = [ DiskCacheAll, DiskCacheLevelOneTo magicNumber3, DiskCacheNone ]+ enumGolden _v = [ DiskCacheAll, DiskCacheLevelOneTo magicNumber3, DiskCacheNone ] where _coveredAllCases = \case DiskCacheAll{} -> ()@@ -327,32 +378,34 @@ DiskCacheNone{} -> () instance EnumGolden MergeSchedule where- enumGolden = [ OneShot, Incremental ]+ enumGolden _v = [ OneShot, Incremental ] where _coveredAllCases = \case OneShot{} -> () Incremental{} -> () instance EnumGolden MergeBatchSize where- enumGolden = map MergeBatchSize [ 1, 1000 ]- supportedVersions _ = [V1]+ enumGolden v+ | v >= V1 = map MergeBatchSize [ 1, 1000 ]+ | otherwise = [] instance EnumGolden (SnapLevels SnapshotRun) where- singGolden = SnapLevels singGolden+ singGolden v = SnapLevels (singGolden v) where _coveredAllCases = \case SnapLevels{} -> () instance EnumGolden (SnapLevel SnapshotRun) where- singGolden = SnapLevel singGolden singGolden+ singGolden v = SnapLevel (singGolden v) (singGolden v) where _coveredAllCases = \case SnapLevel{} -> () instance EnumGolden (SnapIncomingRun SnapshotRun) where- enumGolden = [- SnapIncomingMergingRun singGolden singGolden singGolden singGolden- , SnapIncomingSingleRun singGolden+ enumGolden v = [+ SnapIncomingMergingRun (singGolden v) (singGolden v) (singGolden v)+ (singGolden v)+ , SnapIncomingSingleRun (singGolden v) ] where _coveredAllCases = \case@@ -360,30 +413,30 @@ SnapIncomingSingleRun{} -> () instance EnumGolden MergePolicyForLevel where- enumGolden = [ LevelTiering, LevelLevelling ]+ enumGolden _v = [ LevelTiering, LevelLevelling ] where _coveredAllCases = \case LevelTiering -> () LevelLevelling -> () instance EnumGolden LevelMergeType where- enumGolden = [ MergeMidLevel, MergeLastLevel ]+ enumGolden _v = [ MergeMidLevel, MergeLastLevel ] where _coveredAllCases = \case MergeMidLevel{} -> () MergeLastLevel{} -> () instance EnumGolden (SnapMergingTree SnapshotRun) where- singGolden = SnapMergingTree singGolden+ singGolden v = SnapMergingTree (singGolden v) where _coveredAllCases = \case SnapMergingTree{} -> () instance EnumGolden (SnapMergingTreeState SnapshotRun) where- enumGolden = [- SnapCompletedTreeMerge singGolden- , SnapPendingTreeMerge singGolden- , SnapOngoingTreeMerge singGolden+ enumGolden v = [+ SnapCompletedTreeMerge (singGolden v)+ , SnapPendingTreeMerge (singGolden v)+ , SnapOngoingTreeMerge (singGolden v) ] where _coveredAllCases = \case@@ -392,9 +445,9 @@ SnapOngoingTreeMerge{} -> () instance EnumGolden (SnapPendingMerge SnapshotRun) where- enumGolden = [- SnapPendingLevelMerge singGolden singGolden- , SnapPendingUnionMerge singGolden+ enumGolden v = [+ SnapPendingLevelMerge (singGolden v) (singGolden v)+ , SnapPendingUnionMerge (singGolden v) ] where _coveredAllCases = \case@@ -402,9 +455,9 @@ SnapPendingUnionMerge{} -> () instance EnumGolden (SnapPreExistingRun SnapshotRun) where- enumGolden = [- SnapPreExistingRun singGolden- , SnapPreExistingMergingRun singGolden+ enumGolden v = [+ SnapPreExistingRun (singGolden v)+ , SnapPreExistingMergingRun (singGolden v) ] where _coveredAllCases = \case@@ -412,43 +465,44 @@ SnapPreExistingMergingRun{} -> () instance EnumGolden TreeMergeType where- enumGolden = [ MergeLevel, MergeUnion ]+ enumGolden _v = [ MergeLevel, MergeUnion ] where _coveredAllCases = \case MergeLevel{} -> () MergeUnion{} -> () instance EnumGolden a => EnumGolden (Maybe a) where- enumGolden = [ Just singGolden, Nothing ]+ enumGolden v = [ Just (singGolden v), Nothing ] where _coveredAllCases = \case Just{} -> () Nothing{} -> () instance EnumGolden a => EnumGolden (V.Vector a) where- enumGolden = [- V.fromList [ singGolden, singGolden ]+ enumGolden v = [+ V.fromList [ singGolden v, singGolden v ] , mempty- , V.fromList [ singGolden ]+ , V.fromList [ singGolden v ] ] instance EnumGolden a => EnumGolden [a] where- enumGolden = [- [singGolden, singGolden]+ enumGolden v = [+ [ singGolden v, singGolden v ] , []- , [singGolden]+ , [ singGolden v] ] instance EnumGolden RunParams where- singGolden = RunParams singGolden singGolden singGolden+ singGolden v = RunParams (singGolden v) (singGolden v) (singGolden v) where _coveredAllCases = \case RunParams{} -> () instance EnumGolden t => EnumGolden (SnapMergingRun t SnapshotRun) where- enumGolden = [- SnapCompletedMerge singGolden singGolden- , SnapOngoingMerge singGolden singGolden singGolden singGolden+ enumGolden v = [+ SnapCompletedMerge (singGolden v) (singGolden v)+ , SnapOngoingMerge (singGolden v) (singGolden v) (singGolden v)+ (singGolden v) ] where _coveredAllCases = \case@@ -456,7 +510,7 @@ SnapOngoingMerge{} -> () instance EnumGolden RunBloomFilterAlloc where- enumGolden = [+ enumGolden _v = [ RunAllocFixed magicNumber3 , RunAllocRequestFPR pi ]@@ -466,13 +520,13 @@ RunAllocRequestFPR{} -> () instance EnumGolden RunNumber where- singGolden = RunNumber magicNumber3+ singGolden _v = RunNumber magicNumber3 where _coveredAllCases = \case RunNumber{} -> () instance EnumGolden IndexType where- enumGolden = [+ enumGolden _v = [ Compact , Ordinary ]@@ -482,7 +536,7 @@ Ordinary{} -> () instance EnumGolden RunDataCaching where- enumGolden = [+ enumGolden _v = [ CacheRunData , NoCacheRunData ]@@ -492,31 +546,31 @@ NoCacheRunData{} -> () instance EnumGolden SnapshotRun where- singGolden = SnapshotRun singGolden singGolden singGolden+ singGolden v = SnapshotRun (singGolden v) (singGolden v) (singGolden v) where _coveredAllCases = \case SnapshotRun{} -> () instance EnumGolden MergeDebt where- singGolden = MergeDebt magicNumber2+ singGolden _v = MergeDebt magicNumber2 where _coveredAllCases = \case MergeDebt{} -> () instance EnumGolden MergeCredits where- singGolden = MergeCredits magicNumber2+ singGolden _v = MergeCredits magicNumber2 where _coveredAllCases = \case MergeCredits{} -> () instance EnumGolden NominalDebt where- singGolden = NominalDebt magicNumber2+ singGolden _v = NominalDebt magicNumber2 where _coveredAllCases = \case NominalDebt{} -> () instance EnumGolden NominalCredits where- singGolden = NominalCredits magicNumber1+ singGolden _v = NominalCredits magicNumber1 where _coveredAllCases = \case NominalCredits{} -> ()