Skip to content

Commit 69e5cf8

Browse files
committed
Add removeReferenceN
In some cases, we have to remove more than one reference at a time. For example, when a merge is completed, then the reference count of each input run should be reduced by value of the reference count of the output run. You can view this as the references being "transferred" from the input runs to the output run.
1 parent 1678933 commit 69e5cf8

File tree

4 files changed

+84
-3
lines changed

4 files changed

+84
-3
lines changed

src-control/Control/RefCount.hs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ module Control.RefCount (
1010
, mkRefCounter1
1111
, addReference
1212
, removeReference
13+
, removeReferenceN
1314
, upgradeWeakReference
1415
, readRefCount
1516
) where
@@ -21,7 +22,9 @@ import Control.Monad.Class.MonadThrow
2122
import Control.Monad.Primitive
2223
import Data.Maybe
2324
import Data.Primitive.PrimVar
25+
import Data.Word
2426
import GHC.Stack
27+
import Text.Printf
2528

2629
-- | A reference counter with an optional finaliser action. Once the reference
2730
-- count reaches @0@, the finaliser will be run.
@@ -89,6 +92,43 @@ removeReference RefCounter{countVar, finaliser} = mask_ $ do
8992
assertWithCallStack (prevCount > 0) $ pure ()
9093
when (prevCount == 1) $ sequence_ finaliser
9194

95+
-- TODO: remove uses of this API. Eventually all references should be singular,
96+
-- and not use patterns where if A contains B then N references on A becomes N
97+
-- references on B. Instead this should be a single reference from A to B,
98+
-- irrespective of the number of references to A.
99+
{-# SPECIALISE removeReferenceN :: HasCallStack => RefCounter IO -> Word64 -> IO () #-}
100+
-- | Decrease the reference counter by @n@. @n@ must be a positive number.
101+
--
102+
-- The count must be known (from context) to be non-zero and at least as large
103+
-- as @n@. Typically this will be because the caller has @n@ references already
104+
-- (that they took out themselves or were given).
105+
removeReferenceN :: (HasCallStack, PrimMonad m, MonadMask m) => RefCounter m -> Word64 -> m ()
106+
removeReferenceN RefCounter{countVar, finaliser} n = mask_ $ do
107+
-- n should be positive
108+
assert (n > 0) $ pure ()
109+
let !n' = fromIntegralChecked n
110+
prevCount <- fetchSubInt countVar n'
111+
-- the reference count must not already be 0, because then the finaliser
112+
-- will have run already
113+
assertWithCallStack (prevCount > 0) $ pure ()
114+
-- the reference count can not go below zero
115+
assertWithCallStack (prevCount >= n') $ pure ()
116+
when (prevCount <= n') $ sequence_ finaliser
117+
118+
-- TODO: remove when removeReferenceN is removed
119+
{-# INLINABLE fromIntegralChecked #-}
120+
-- | Like 'fromIntegral', but throws an error when @(x :: a) /= fromIntegral
121+
-- (fromIntegral x :: b)@.
122+
fromIntegralChecked :: (HasCallStack, Integral a, Integral b, Show a) => a -> b
123+
fromIntegralChecked x
124+
| x'' == x
125+
= x'
126+
| otherwise
127+
= error $ printf "fromIntegralChecked: conversion failed, %s /= %s" (show x) (show x'')
128+
where
129+
x' = fromIntegral x
130+
x'' = fromIntegral x'
131+
92132
-- | Try to turn a \"weak\" reference on something into a proper reference.
93133
-- This is by analogy with @deRefWeak :: Weak v -> IO (Maybe v)@, but for
94134
-- reference counts.
@@ -120,6 +160,7 @@ upgradeWeakReference RefCounter{countVar} = do
120160
then return True
121161
else casLoop prevCount'
122162

163+
-- TODO: remove when removeRefenceN is removed
123164
{-# SPECIALISE readRefCount :: RefCounter IO -> IO RefCount #-}
124165
-- | Warning: reading the current reference count is inherently racy as there is
125166
-- no way to reliably act on the information. It can be useful for debugging.

src/Database/LSMTree/Internal/Merge.hs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ module Database.LSMTree.Internal.Merge (
99
, new
1010
, addReference
1111
, removeReference
12+
, removeReferenceN
13+
, readRefCount
1214
, complete
1315
, stepsToCompletion
1416
, stepsToCompletionCounted
@@ -24,12 +26,13 @@ import Control.Monad.Class.MonadThrow (MonadCatch, MonadMask (..),
2426
MonadThrow (..))
2527
import Control.Monad.Fix (MonadFix)
2628
import Control.Monad.Primitive (PrimMonad, PrimState, RealWorld)
27-
import Control.RefCount (RefCounter)
29+
import Control.RefCount (RefCount (..), RefCounter)
2830
import qualified Control.RefCount as RC
2931
import Data.Coerce (coerce)
3032
import Data.Primitive.MutVar
3133
import Data.Traversable (for)
3234
import qualified Data.Vector as V
35+
import Data.Word
3336
import Database.LSMTree.Internal.BlobRef (BlobRef)
3437
import Database.LSMTree.Internal.Entry
3538
import Database.LSMTree.Internal.Run (Run, RunDataCaching)
@@ -130,6 +133,14 @@ addReference Merge{..} = RC.addReference mergeRefCounter
130133
removeReference :: (HasCallStack, PrimMonad m, MonadMask m) => Merge m h -> m ()
131134
removeReference Merge{..} = RC.removeReference mergeRefCounter
132135

136+
{-# SPECIALISE removeReferenceN :: Merge IO h -> Word64 -> IO () #-}
137+
removeReferenceN :: (HasCallStack, PrimMonad m, MonadMask m) => Merge m h -> Word64 -> m ()
138+
removeReferenceN r = RC.removeReferenceN (mergeRefCounter r)
139+
140+
{-# SPECIALISE readRefCount :: Merge IO h -> IO RefCount #-}
141+
readRefCount :: PrimMonad m => Merge m h -> m RefCount
142+
readRefCount Merge{..} = RC.readRefCount mergeRefCounter
143+
133144
{-# SPECIALISE finaliser ::
134145
MutVar RealWorld MergeState
135146
-> RunBuilder IO h

src/Database/LSMTree/Internal/Run.hs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ module Database.LSMTree.Internal.Run (
4444
, sizeInPages
4545
, addReference
4646
, removeReference
47+
, removeReferenceN
4748
, mkBlobRefForRun
4849
-- ** Run creation
4950
, fromMutable
@@ -64,6 +65,7 @@ import qualified Control.RefCount as RC
6465
import Data.BloomFilter (Bloom)
6566
import qualified Data.ByteString.Short as SBS
6667
import Data.Foldable (for_)
68+
import Data.Word (Word64)
6769
import Database.LSMTree.Internal.BlobRef (BlobRef (..), BlobSpan (..))
6870
import Database.LSMTree.Internal.BloomFilter (bloomFilterFromSBS)
6971
import qualified Database.LSMTree.Internal.CRC32C as CRC
@@ -134,6 +136,10 @@ addReference r = RC.addReference (runRefCounter r)
134136
removeReference :: (PrimMonad m, MonadMask m) => Run m h -> m ()
135137
removeReference r = RC.removeReference (runRefCounter r)
136138

139+
{-# SPECIALISE removeReferenceN :: Run IO h -> Word64 -> IO () #-}
140+
removeReferenceN :: (PrimMonad m, MonadMask m) => Run m h -> Word64 -> m ()
141+
removeReferenceN r = RC.removeReferenceN (runRefCounter r)
142+
137143
-- | Helper function to make a 'BlobRef' that points into a 'Run'.
138144
mkBlobRefForRun :: Run m h -> BlobSpan -> BlobRef m (FS.Handle h)
139145
mkBlobRefForRun Run{runBlobFile, runRefCounter} blobRefSpan =

test-control/Test/Control/RefCount.hs

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,17 @@
33
module Test.Control.RefCount (tests) where
44

55
import Control.Concurrent.Class.MonadMVar
6-
import Control.Exception (AssertionFailed (..))
6+
import Control.Exception (AssertionFailed (..), try)
77
import Control.Monad
8-
import Control.Monad.Class.MonadThrow
98
import Control.RefCount
9+
import Data.Either (isRight)
1010
import Test.Tasty (TestTree, testGroup)
1111
import Test.Tasty.QuickCheck
1212

1313
tests :: TestTree
1414
tests = testGroup "Control.RefCount" [
1515
testProperty "prop_refCount" prop_refCount
16+
, testProperty "prop_removeReferenceN" prop_removeReferenceN
1617
]
1718

1819
prop_refCount :: Property
@@ -80,3 +81,25 @@ prop_refCount = once $ ioProperty $ do
8081
#else
8182
check = \case Left (AssertionFailed _) -> False; Right () -> True
8283
#endif
84+
85+
prop_removeReferenceN :: Positive Int -> NonNegative Int -> Property
86+
prop_removeReferenceN (Positive n) (NonNegative m) = ioProperty $ do
87+
obj <- newMVar False
88+
ref <- unsafeMkRefCounterN (RefCount n) $ Just (void $ modifyMVar_ obj (\x -> pure (not x)) )
89+
90+
e1 <- try @AssertionFailed $ removeReferenceN ref (fromIntegral m)
91+
n1 <- readRefCount ref -- 0
92+
b1 <- readMVar obj -- True
93+
94+
pure $
95+
counterexample "e1" (if n < m || m == 0
96+
then check e1
97+
else isRight e1) .&&.
98+
counterexample "n1" (n1 == RefCount (n - m)) .&&.
99+
counterexample "b1" (b1 == (isRight e1 && m >= n))
100+
where
101+
#ifdef NO_IGNORE_ASSERTS
102+
check = \case Left (AssertionFailed _) -> True; Right () -> False
103+
#else
104+
check = \case Left (AssertionFailed _) -> False; Right () -> True
105+
#endif

0 commit comments

Comments
 (0)