Skip to content

Commit cfbe30b

Browse files
committed
Prototype: change credit scaling, using nominal vs physical credit
Previously we've had a somewhat complex method to scale credits from those supplied in a level (1 per update) to the credits used in merging. The existing scheme has a number of downsides: * no clear distinction between scaled and unscaled credits, what does each one measure? * complex, needs scaling dependent of the merge-policy * always uses worst case supply of credits so often finishes early * rounding errors compound problem of credit over-supply * no satisfactory way to check we do not over-supply credits * contributing credits to a merge from multiple handles will cause the merge to finish earlier than any of the handles needs it to finish, thus doing work more eagerly than necessary. The new scheme involves: * distinguishing physical vs nominal credits, with a clear definition for each measure * converting between nominal and physical without merge policy * physical debt is no longer worst case but matches actual cost * integer rounding errors do not compound * we can assert that we reach the nominal and physical debt totals at the same moment (when the merge is done) and thus we can assert no over-supply of physical credits. * we can avoid supplying credits too quickly to a merge that is shared between multiple handles, each one only pushes it as far as it needs.
1 parent 74f8807 commit cfbe30b

File tree

2 files changed

+122
-67
lines changed

2 files changed

+122
-67
lines changed

prototypes/ScheduledMerges.hs

Lines changed: 119 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ module ScheduledMerges (
6363
LevelMergeType(..),
6464
MergeCredit(..),
6565
MergeDebt(..),
66+
NominalCredit(..),
67+
NominalDebt(..),
6668
Run,
6769
runSize,
6870
supplyCreditsMergingTree,
@@ -123,7 +125,9 @@ data Level s = Level !(IncomingRun s) ![Run]
123125
-- single run without having to read the 'STRef', and secondly to make it easier
124126
-- to avoid supplying merge credits. It's not essential, but simplifies things
125127
-- somewhat.
126-
data IncomingRun s = Merging !MergePolicy !(MergingRun LevelMergeType s)
128+
data IncomingRun s = Merging !MergePolicy
129+
!NominalDebt !(STRef s NominalCredit)
130+
!(MergingRun LevelMergeType s)
127131
| Single !Run
128132

129133
-- | The merge policy for a LSM level can be either tiering or levelling.
@@ -321,7 +325,7 @@ invariant (LSMContent _ levels ul) = do
321325
mrs <- case ir of
322326
Single r ->
323327
return (CompletedMerge r)
324-
Merging mp (MergingRun mt _ ref) -> do
328+
Merging mp _ _ (MergingRun mt _ ref) -> do
325329
assertST $ mp == mergePolicyForLevel ln ls ul
326330
&& mt == mergeTypeForLevel ls ul
327331
readSTRef ref
@@ -520,7 +524,9 @@ assertST p = assert p $ return ()
520524
--
521525

522526
-- | Credits for keeping track of merge progress. These credits
523-
-- correspond directly to merge steps performed.
527+
-- correspond directly to merge steps performed. We also call these \"physical\"
528+
-- credits (since they correspond to steps done), and as opposed to \"nominal\"
529+
-- credits in 'NominalCredit' and 'NominalDebt'.
524530
type Credit = Int
525531

526532
-- | Debt for keeping track of the total merge work to do.
@@ -640,19 +646,18 @@ mergeBatchSize = 32
640646
-- Merging run abstraction
641647
--
642648

643-
newMergingRun :: IsMergeType t => Maybe Debt -> t -> [Run] -> ST s (MergingRun t s)
644-
newMergingRun mdebt mergeType runs = do
649+
newMergingRun :: IsMergeType t => t -> [Run] -> ST s (MergingRun t s)
650+
newMergingRun mergeType runs = do
645651
assertST $ length runs > 1
646652
-- in some cases, no merging is required at all
647653
(debt, state) <- case filter (\r -> runSize r > 0) runs of
648654
[] -> let (r:_) = runs -- just re-use the empty input
649655
in return (runSize r, CompletedMerge r)
650656
[r] -> return (runSize r, CompletedMerge r)
651657
rs -> do
652-
let !cost = sum (map runSize rs)
653-
!debt = case mdebt of
654-
Nothing -> cost
655-
Just d -> assert (d >= cost) d
658+
-- The (physical) debt is always exactly the cost (merge steps),
659+
-- which is the sum of run lengths in elements.
660+
let !debt = sum (map runSize rs)
656661
let merged = mergek mergeType rs -- deliberately lazy
657662
return (debt, OngoingMerge zeroMergeCredit rs merged)
658663
MergingRun mergeType (MergeDebt debt) <$> newSTRef state
@@ -715,6 +720,15 @@ supplyCreditsMergingRun =
715720
writeSTRef ref (OngoingMerge mergeCredit' rs r)
716721
return 0
717722

723+
suppliedCreditMergingRun :: MergingRun t s -> ST s Credit
724+
suppliedCreditMergingRun (MergingRun _ d ref) =
725+
readSTRef ref >>= \case
726+
CompletedMerge{} ->
727+
let MergeDebt { totalDebt } = d in
728+
return totalDebt
729+
OngoingMerge MergeCredit {spentCredits, unspentCredits} _ _ ->
730+
return (spentCredits + unspentCredits)
731+
718732
-------------------------------------------------------------------------------
719733
-- LSM handle
720734
--
@@ -757,7 +771,7 @@ update tr (LSMHandle scr lsmr) k op = do
757771
sc <- readSTRef scr
758772
content@(LSMContent wb ls unionLevel) <- readSTRef lsmr
759773
modifySTRef' scr (+1)
760-
supplyCreditsLevels 1 ls
774+
supplyCreditsLevels (NominalCredit 1) ls
761775
invariant content
762776
let wb' = Map.insertWith combine k op wb
763777
if bufferSize wb' >= maxBufferSize
@@ -769,7 +783,7 @@ update tr (LSMHandle scr lsmr) k op = do
769783
else
770784
writeSTRef lsmr (LSMContent wb' ls unionLevel)
771785

772-
supplyMergeCredits :: LSM s -> Credit -> ST s ()
786+
supplyMergeCredits :: LSM s -> NominalCredit -> ST s ()
773787
supplyMergeCredits (LSMHandle scr lsmr) credits = do
774788
content@(LSMContent _ ls _) <- readSTRef lsmr
775789
modifySTRef' scr (+1)
@@ -972,48 +986,79 @@ lookupsTree k = go
972986
lookupBatch' = lookupBatch Nothing k
973987

974988
-------------------------------------------------------------------------------
975-
-- Updates
989+
-- Nominal credits
976990
--
977991

992+
newtype NominalCredit = NominalCredit Credit
993+
deriving stock Show
994+
995+
newtype NominalDebt = NominalDebt Credit
996+
deriving stock Show
997+
978998
-- TODO: If there is a UnionLevel, there is no (more expensive) last level merge
979999
-- in the regular levels, so a little less merging work is required than if
9801000
-- there was no UnionLevel. It might be a good idea to spend this "saved" work
9811001
-- on the UnionLevel instead. This makes future lookups cheaper and ensures that
9821002
-- we can get rid of the UnionLevel at some point, even if a user just keeps
9831003
-- inserting without calling 'supplyUnionCredits'.
984-
supplyCreditsLevels :: Credit -> Levels s -> ST s ()
985-
supplyCreditsLevels unscaled =
1004+
supplyCreditsLevels :: NominalCredit -> Levels s -> ST s ()
1005+
supplyCreditsLevels nominalDeposit =
9861006
traverse_ $ \(Level ir _rs) -> do
9871007
case ir of
9881008
Single{} -> return ()
989-
Merging mp mr -> do
990-
factor <- creditsForMerge mp mr
991-
let credits = ceiling (fromIntegral unscaled * factor)
992-
when (credits > 0) $ do
993-
_ <- supplyCreditsMergingRun credits mr
994-
-- we don't mind leftover credits, each level completes independently
995-
return ()
996-
997-
-- | The general case (and thus worst case) of how many merge credits we need
998-
-- for a level. This is based on the merging policy at the level.
999-
--
1000-
creditsForMerge :: MergePolicy -> MergingRun t s -> ST s Rational
1009+
Merging _mp nominalDebt nominalCreditVar
1010+
mr@(MergingRun _ physicalDebt _) -> do
1011+
1012+
nominalCredit <- depositNominalCredit
1013+
nominalDebt nominalCreditVar nominalDeposit
1014+
physicalCredit <- suppliedCreditMergingRun mr
1015+
let !physicalCredit' = scaleNominalToPhysicalCredit
1016+
nominalDebt physicalDebt nominalCredit
1017+
-- Our target physicalCredit' could actually be less than the
1018+
-- actual current physicalCredit if other tables were contributing
1019+
-- credits to the shared merge.
1020+
!physicalDeposit = physicalCredit' - physicalCredit
1021+
1022+
-- So we may have a zero or negative deposit, which we ignore.
1023+
when (physicalDeposit > 0) $ do
1024+
leftoverCredits <- supplyCreditsMergingRun physicalDeposit mr
1025+
-- For merges at ordinary levels (not unions) we expect to hit the
1026+
-- debt limit exactly and never exceed it.
1027+
assert (leftoverCredits == 0) $ return ()
1028+
1029+
scaleNominalToPhysicalCredit ::
1030+
NominalDebt
1031+
-> MergeDebt
1032+
-> NominalCredit
1033+
-> Credit
1034+
scaleNominalToPhysicalCredit (NominalDebt nominalDebt)
1035+
MergeDebt { totalDebt = physicalDebt }
1036+
(NominalCredit nominalCredit) =
1037+
floor $ toRational nominalCredit * toRational physicalDebt
1038+
/ toRational nominalDebt
1039+
-- This specification using Rational as an intermediate representation can
1040+
-- be implemented efficiently using only integer operations.
1041+
1042+
depositNominalCredit ::
1043+
NominalDebt
1044+
-> STRef s NominalCredit
1045+
-> NominalCredit
1046+
-> ST s NominalCredit
1047+
depositNominalCredit (NominalDebt nominalDebt)
1048+
nominalCreditVar
1049+
(NominalCredit deposit) = do
1050+
NominalCredit before <- readSTRef nominalCreditVar
1051+
-- Depositing _could_ leave the credit higher than the debt, because
1052+
-- sometimes under-full runs mean we don't shuffle runs down the levels
1053+
-- as quickly as the worst case. So here we do just drop excess nominal
1054+
-- credits.
1055+
let !after = NominalCredit (min (before + deposit) nominalDebt)
1056+
writeSTRef nominalCreditVar after
1057+
return after
10011058

1002-
-- A levelling merge has 1 input run and one resident run, which is (up to) 4x
1003-
-- bigger than the others.
1004-
-- It needs to be completed before another run comes in.
1005-
creditsForMerge MergePolicyLevelling _ =
1006-
return $ (1 + 4) / 1
1007-
1008-
-- A tiering merge has 5 runs at most (once could be held back to merged again)
1009-
-- and must be completed before the level is full (once 4 more runs come in).
1010-
creditsForMerge MergePolicyTiering (MergingRun _ _ ref) = do
1011-
readSTRef ref >>= \case
1012-
CompletedMerge _ -> return 0
1013-
OngoingMerge _ rs _ -> do
1014-
let numRuns = length rs
1015-
assertST $ numRuns `elem` [4, 5]
1016-
return $ fromIntegral numRuns / 4
1059+
-------------------------------------------------------------------------------
1060+
-- Updates
1061+
--
10171062

10181063
increment :: forall s. Tracer (ST s) Event
10191064
-> Counter -> Run -> Levels s -> UnionLevel s -> ST s (Levels s)
@@ -1035,7 +1080,7 @@ increment tr sc run0 ls0 ul = do
10351080
go !ln incoming (Level ir rs : ls) = do
10361081
r <- case ir of
10371082
Single r -> return r
1038-
Merging mergePolicy mr -> do
1083+
Merging mergePolicy _ _ mr -> do
10391084
r <- expectCompletedMergingRun mr
10401085
traceWith tr' MergeCompletedEvent {
10411086
mergePolicy,
@@ -1094,26 +1139,38 @@ newLevelMerge :: Tracer (ST s) EventDetail
10941139
-> [Run] -> ST s (IncomingRun s)
10951140
newLevelMerge _ _ _ _ [r] = return (Single r)
10961141
newLevelMerge tr level mergePolicy mergeType rs = do
1142+
assertST (length rs `elem` [4, 5])
1143+
mergingRun@(MergingRun _ physicalDebt _) <- newMergingRun mergeType rs
1144+
assertST (totalDebt physicalDebt <= maxPhysicalDebt)
10971145
traceWith tr MergeStartedEvent {
10981146
mergePolicy,
10991147
mergeType,
1100-
mergeDebt = debt,
1148+
mergeDebt = totalDebt physicalDebt,
11011149
mergeRunsSize = map runSize rs
11021150
}
1103-
assertST (length rs `elem` [4, 5])
1104-
Merging mergePolicy <$> newMergingRun (Just debt) mergeType rs
1151+
nominalCreditVar <- newSTRef (NominalCredit 0)
1152+
pure (Merging mergePolicy nominalDebt nominalCreditVar mergingRun)
11051153
where
1106-
-- How much we need to discharge before the merge can be guaranteed
1107-
-- complete. More precisely, this is the maximum amount a merge at this
1108-
-- level could need. While the real @cost@ of a merge would lead to merges
1109-
-- finishing early, the overestimation @debt@ means that in this prototype
1110-
-- merges will only complete at the last possible moment.
1111-
-- Note that for levelling this is includes the single run in the current
1112-
-- level.
1113-
debt = case mergePolicy of
1114-
MergePolicyLevelling -> 4 * tieringRunSize (level-1)
1115-
+ levellingRunSize level
1116-
MergePolicyTiering -> length rs * tieringRunSize (level-1)
1154+
-- The nominal debt equals the minimum of credits we will supply before we
1155+
-- expect the merge to complete. This is the same as the number of updates
1156+
-- in a run that gets moved to this level.
1157+
nominalDebt = NominalDebt (tieringRunSize level)
1158+
1159+
-- The physical debt is the number of actual merge steps we will need to
1160+
-- perform before the merge is complete. This is always the sum of the
1161+
-- lengths of the input runs.
1162+
--
1163+
-- As we supply nominal credit, we scale them and supply physical credits,
1164+
-- such that we pay off the physical and nominal debts at the same time.
1165+
--
1166+
-- We can bound the worst case physical debt: this is the maximum amount of
1167+
-- steps a merge at this level could need. Note that for levelling this is
1168+
-- includes the single run in the current level.
1169+
maxPhysicalDebt =
1170+
case mergePolicy of
1171+
MergePolicyLevelling -> 4 * tieringRunSize (level-1)
1172+
+ levellingRunSize level
1173+
MergePolicyTiering -> length rs * tieringRunSize (level-1)
11171174

11181175
-- | Only based on run count, not their sizes.
11191176
tieringLevelIsFull :: Int -> [Run] -> [Run] -> Bool
@@ -1179,8 +1236,8 @@ newPendingLevelMerge irs tree = do
11791236
st = PendingTreeMerge (PendingLevelMerge prs tree)
11801237
Just . MergingTree <$> newSTRef st
11811238
where
1182-
incomingToPreExistingRun (Single r) = PreExistingRun r
1183-
incomingToPreExistingRun (Merging _ mr) = PreExistingMergingRun mr
1239+
incomingToPreExistingRun (Single r) = PreExistingRun r
1240+
incomingToPreExistingRun (Merging _ _ _ mr) = PreExistingMergingRun mr
11841241

11851242
-- | Ensures that the merge contains more than one input.
11861243
newPendingUnionMerge :: [MergingTree s] -> ST s (Maybe (MergingTree s))
@@ -1293,12 +1350,10 @@ supplyCreditsMergingTreeState credits !state = do
12931350
else do
12941351
-- all children must be done, create new merge!
12951352
(mergeType, rs) <- expectCompletedChildren pm
1296-
-- no reason to claim a larger debt than sum of run sizes
1297-
let debt = Nothing
12981353
case rs of
12991354
[r] -> return (c', CompletedTreeMerge r)
13001355
_ -> do
1301-
state' <- OngoingTreeMerge <$> newMergingRun debt mergeType rs
1356+
state' <- OngoingTreeMerge <$> newMergingRun mergeType rs
13021357
-- use any remaining credits to progress the new merge
13031358
supplyCreditsMergingTreeState c' state'
13041359

@@ -1375,8 +1430,8 @@ flattenLevel (Level ir rs) = (++ rs) <$> flattenIncomingRun ir
13751430

13761431
flattenIncomingRun :: IncomingRun s -> ST s [Run]
13771432
flattenIncomingRun = \case
1378-
Single r -> return [r]
1379-
Merging _ mr -> flattenMergingRun mr
1433+
Single r -> return [r]
1434+
Merging _ _ _ mr -> flattenMergingRun mr
13801435

13811436
flattenMergingRun :: MergingRun t s -> ST s [Run]
13821437
flattenMergingRun (MergingRun _ _ ref) = do
@@ -1438,7 +1493,7 @@ dumpRepresentation (LSMHandle _ lsmr) = do
14381493
dumpLevel :: Level s -> ST s LevelRepresentation
14391494
dumpLevel (Level (Single r) rs) =
14401495
return (Nothing, (r:rs))
1441-
dumpLevel (Level (Merging mp (MergingRun mt _ ref)) rs) = do
1496+
dumpLevel (Level (Merging mp _nd _nc (MergingRun mt _ ref)) rs) = do
14421497
mrs <- readSTRef ref
14431498
return (Just (mp, mt, mrs), rs)
14441499

prototypes/ScheduledMergesTest.hs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ test_regression_empty_run =
7373
]
7474

7575
-- finish merge
76-
LSM.supplyMergeCredits lsm 16
76+
LSM.supplyMergeCredits lsm (NominalCredit 16)
7777

7878
expectShape lsm
7979
0
@@ -143,7 +143,7 @@ test_merge_again_with_incoming =
143143
]
144144

145145
-- complete the merge (20 entries, but credits get scaled up by 1.25)
146-
LSM.supplyMergeCredits lsm 16
146+
LSM.supplyMergeCredits lsm (NominalCredit 16)
147147

148148
expectShape lsm
149149
0
@@ -272,7 +272,7 @@ fromP (PMergingRun m) = PreExistingMergingRun <$> fromM m
272272
fromM :: IsMergeType t => M t -> ST s (MergingRun t s)
273273
fromM m = do
274274
let (mergeType, mergeDebt, state) = case m of
275-
MCompleted mt md r -> (mt, md, CompletedMerge r)
275+
MCompleted mt md r -> (mt, md, CompletedMerge r)
276276
MOngoing mt md mc rs -> (mt, md, OngoingMerge mc rs' (mergek mt rs'))
277277
where rs' = map getNonEmptyRun rs
278278
MergingRun mergeType mergeDebt <$> newSTRef state

0 commit comments

Comments
 (0)