Skip to content

Commit 2780fa2

Browse files
authored
Merge pull request #564 from IntersectMBO/wenkokke/issue558
feat(issue558): test silent snapshot corruption
2 parents e6dd4e6 + d31449c commit 2780fa2

File tree

8 files changed

+318
-115
lines changed

8 files changed

+318
-115
lines changed

src-control/Control/ActionRegistry.hs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ module Control.ActionRegistry (
2525
, CommitActionRegistryError (..)
2626
, AbortActionRegistryError (..)
2727
, AbortActionRegistryReason (..)
28+
, getReasonExitCaseException
2829
-- * Registering actions #registeringActions#
2930
-- $registering-actions
3031
, withRollback
@@ -372,6 +373,11 @@ data AbortActionRegistryReason =
372373
| ReasonExitCaseAbort
373374
deriving stock Show
374375

376+
getReasonExitCaseException :: AbortActionRegistryReason -> Maybe SomeException
377+
getReasonExitCaseException = \case
378+
ReasonExitCaseException e -> Just e
379+
ReasonExitCaseAbort -> Nothing
380+
375381
data AbortActionRegistryError =
376382
AbortActionRegistryError AbortActionRegistryReason (NonEmpty ActionError)
377383
deriving stock Show

test/Database/LSMTree/Class.hs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ module Database.LSMTree.Class (
1414
, module Types
1515
) where
1616

17+
import Control.Monad (void)
1718
import Control.Monad.Class.MonadThrow (MonadThrow (..))
1819
import Data.Kind (Constraint, Type)
1920
import Data.List.NonEmpty (NonEmpty)
@@ -24,6 +25,11 @@ import Database.LSMTree as Types (LookupResult (..), QueryResult (..),
2425
resolveDeserialised)
2526
import qualified Database.LSMTree as R
2627
import Database.LSMTree.Class.Common as Common
28+
import qualified Database.LSMTree.Internal as RI (SessionEnv (..), Table (..),
29+
Table' (..), withOpenSession)
30+
import qualified Database.LSMTree.Internal.Paths as RIP
31+
import Test.Util.FS (flipRandomBitInRandomFileHardlinkSafe)
32+
import Test.Util.QC (Choice)
2733

2834
-- | Class abstracting over table operations.
2935
--
@@ -140,6 +146,14 @@ class (IsSession (Session h)) => IsTable h where
140146
-> h m k v b
141147
-> m ()
142148

149+
corruptSnapshot ::
150+
( IOLike m
151+
)
152+
=> Choice
153+
-> SnapshotName
154+
-> h m k v b
155+
-> m ()
156+
143157
openSnapshot ::
144158
( IOLike m
145159
, C k v b
@@ -222,6 +236,21 @@ withCursor offset tbl = bracket (newCursor offset tbl) (closeCursor (Proxy @h))
222236
Real instance
223237
-------------------------------------------------------------------------------}
224238

239+
-- | Snapshot corruption for the real instance.
240+
-- Implemented here, instead of as part of the public API.
241+
rCorruptSnapshot ::
242+
IOLike m
243+
=> Choice
244+
-> SnapshotName
245+
-> R.Table m k v b
246+
-> m ()
247+
rCorruptSnapshot choice name (RI.Table' t) =
248+
RI.withOpenSession (RI.tableSession t) $ \seshEnv ->
249+
let hfs = RI.sessionHasFS seshEnv
250+
root = RI.sessionRoot seshEnv
251+
namedSnapDir = RIP.getNamedSnapshotDir (RIP.namedSnapshotDir root name)
252+
in void $ flipRandomBitInRandomFileHardlinkSafe hfs choice namedSnapDir
253+
225254
instance IsTable R.Table where
226255
type Session R.Table = R.Session
227256
type TableConfig R.Table = R.TableConfig
@@ -244,6 +273,7 @@ instance IsTable R.Table where
244273
readCursor _ = R.readCursor
245274

246275
createSnapshot = R.createSnapshot
276+
corruptSnapshot = rCorruptSnapshot
247277
openSnapshot sesh snap = R.openSnapshot sesh R.configNoOverride snap
248278

249279
duplicate = R.duplicate

test/Database/LSMTree/Model/IO.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ instance Class.IsTable Table where
8585
runInOpenSession s (Model.readCursor x1 c)
8686

8787
createSnapshot x1 x2 (Table s t) = runInOpenSession s (Model.createSnapshot x1 x2 t)
88+
corruptSnapshot _ x (Table s _t) = runInOpenSession s (Model.corruptSnapshot x)
8889
openSnapshot s x1 x2 = Table s <$> runInOpenSession s (Model.openSnapshot x1 x2)
8990

9091
duplicate (Table s t) = Table s <$> runInOpenSession s (Model.duplicate t)

test/Database/LSMTree/Model/Session.hs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ module Database.LSMTree.Model.Session (
3232
, runModelM
3333
, runModelMWithInjectedErrors
3434
-- ** Errors
35-
, Err (.., DefaultErrDiskFault)
35+
, Err (.., DefaultErrDiskFault, DefaultErrSnapshotCorrupted)
3636
-- * Tables
3737
, Table
3838
, TableConfig (..)
@@ -241,19 +241,23 @@ runModelMWithInjectedErrors (Just _) _ onErrors st =
241241
pattern DefaultErrDiskFault :: Err
242242
pattern DefaultErrDiskFault = ErrDiskFault "default"
243243

244+
-- | The default 'ErrSnapshotCorrupted' that model operations will throw.
245+
pattern DefaultErrSnapshotCorrupted :: Err
246+
pattern DefaultErrSnapshotCorrupted = ErrSnapshotCorrupted "default"
247+
244248
--
245249
-- Errors
246250
--
247251

248252
data Err =
249253
ErrTableClosed
250-
| ErrSnapshotCorrupted
251254
| ErrSnapshotExists
252255
| ErrSnapshotDoesNotExist
253256
| ErrSnapshotWrongType
254257
| ErrBlobRefInvalidated
255258
| ErrCursorClosed
256259
-- | Something went wrong with the file system.
260+
| ErrSnapshotCorrupted String
257261
| ErrDiskFault String
258262
| ErrOther String
259263
deriving stock Eq
@@ -262,8 +266,6 @@ instance Show Err where
262266
showsPrec d = \case
263267
ErrTableClosed ->
264268
showString "ErrTableClosed"
265-
ErrSnapshotCorrupted ->
266-
showString "ErrSnapshotCorrupted"
267269
ErrSnapshotExists ->
268270
showString "ErrSnapshotExists"
269271
ErrSnapshotDoesNotExist ->
@@ -274,6 +276,10 @@ instance Show Err where
274276
showString "ErrBlobRefInvalidated"
275277
ErrCursorClosed ->
276278
showString "ErrCursorClosed"
279+
ErrSnapshotCorrupted s ->
280+
showParen (d > appPrec) $
281+
showString "ErrSnapshotCorrupted " .
282+
showParen True (showString s)
277283
ErrDiskFault s ->
278284
showParen (d > appPrec) $
279285
showString "ErrDiskFault " .
@@ -577,7 +583,7 @@ openSnapshot label name = do
577583
throwError ErrSnapshotDoesNotExist
578584
Just (Snapshot conf label' tbl corrupted) -> do
579585
when corrupted $
580-
throwError ErrSnapshotCorrupted
586+
throwError DefaultErrSnapshotCorrupted
581587
when (label /= label') $
582588
throwError ErrSnapshotWrongType
583589
case fromSomeTable tbl of
@@ -592,6 +598,9 @@ openSnapshot label name = do
592598
Just table' ->
593599
newTableWith conf table'
594600

601+
-- To match the implementation of the real table, this should not corrupt the
602+
-- snapshot if there are _no non-empty files_; however, since there are no such
603+
-- snapshots, this is probably fine.
595604
corruptSnapshot ::
596605
(MonadState Model m, MonadError Err m)
597606
=> SnapshotName

test/Test/Database/LSMTree/Internal/Snapshot/FS.hs

Lines changed: 26 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import Control.Monad.IOSim (runSimOrThrow)
99
import Control.Tracer
1010
import Data.Bifunctor (Bifunctor (..))
1111
import Data.Maybe (fromJust)
12-
import qualified Data.Set as Set
1312
import qualified Data.Vector as V
1413
import Data.Word
1514
import Database.LSMTree.Extras (showPowersOf10)
@@ -28,10 +27,10 @@ import System.FS.Sim.Error hiding (genErrors)
2827
import qualified System.FS.Sim.MockFS as MockFS
2928
import Test.Database.LSMTree.Internal.Snapshot.Codec ()
3029
import Test.QuickCheck
31-
import Test.QuickCheck.Gen (genDouble)
3230
import Test.Tasty
3331
import Test.Tasty.QuickCheck
3432
import Test.Util.FS
33+
import Test.Util.QC (Choice)
3534

3635
tests :: TestTree
3736
tests = testGroup "Test.Database.LSMTree.Internal.Snapshot.FS" [
@@ -162,15 +161,7 @@ instance Arbitrary TestErrors where
162161
Snapshot corruption
163162
-------------------------------------------------------------------------------}
164163

165-
-- | A 'Double' in the @[0, 1)@ range.
166-
newtype Double_0_1 = Double_0_1 Double
167-
deriving stock (Show, Eq)
168-
169-
instance Arbitrary Double_0_1 where
170-
arbitrary = Double_0_1 <$> genDouble
171-
shrink (Double_0_1 x) = [Double_0_1 x' | x' <- shrink x, 0 <= x', x' < 1]
172-
173-
-- TODO: an alternative to generating doubles a priori is to run the monadic
164+
-- TODO: an alternative to generating a Choice a priori is to run the monadic
174165
-- code in @PropertyM (IOSim s)@, and then we can do quantification inside the
175166
-- monadic property using @pick@. This complicates matters, however, because
176167
-- functions like @withSimHasBlockIO@ and @withTable@ would have to run in
@@ -179,14 +170,9 @@ instance Arbitrary Double_0_1 where
179170
prop_flipSnapshotBit ::
180171
Positive (Small Int)
181172
-> V.Vector (Word64, Entry Word64 Word64)
182-
-> Double_0_1 -- ^ Used to pick which file to corrupt
183-
-> Double_0_1 -- ^ Used to pick which bit to flip in the file we picked
173+
-> Choice -- ^ Used to pick which file/bit to corrupt.
184174
-> Property
185-
prop_flipSnapshotBit
186-
(Positive (Small bufferSize))
187-
es
188-
(Double_0_1 pickFile)
189-
(Double_0_1 pickBit) =
175+
prop_flipSnapshotBit (Positive (Small bufferSize)) es pickFileBit =
190176
runSimOrThrow $
191177
withSimHasBlockIO propNoOpenHandles MockFS.empty $ \hfs hbio _fsVar ->
192178
withSession nullTracer hfs hbio root $ \s ->
@@ -195,42 +181,28 @@ prop_flipSnapshotBit
195181
updates resolve es' t
196182
createSnap t
197183

198-
-- Pick a random file from the named snapshot directory
199-
files <- listDirectoryRecursiveFiles hfs (getNamedSnapshotDir namedSnapDir)
200-
let i = round (fromIntegral (Set.size files - 1) * pickFile)
201-
let file = Set.elemAt i files
202-
let path = getNamedSnapshotDir namedSnapDir </> file
203-
-- Pick a random bit from the file that we want to corrupt
204-
n <- withFile hfs path ReadMode $ hGetSize hfs
205-
let j = round (fromIntegral (n * 8 - 1) * pickBit)
206-
207-
-- Some info for the test output
208-
let
209-
tabCorruptedFile = tabulate "Corrupted file" [show path]
210-
counterCorruptedFile = counterexample ("Corrupted file: " ++ show path)
211-
tabFlippedBit = tabulate "Flipped bit" [showPowersOf10 j]
212-
counterFlippedBit = counterexample ("Flipped bit: " ++ show j)
213-
214-
-- TODO: check forgotten refs
215-
if n <= 0 then -- file is empty
216-
pure $ tabulate "Result" ["No corruption applied"] True
217-
else do -- file is non-empty
218-
219-
-- Flip a bit and try to open the snapshot
220-
flipFileBit hfs path j
221-
t' <- try @_ @SomeException $ bracket (openSnap s) close $ \_ -> pure ()
222-
223-
pure $
224-
tabCorruptedFile $ counterCorruptedFile $ tabFlippedBit $ counterFlippedBit $
225-
case t' of
226-
-- If we find an error, we detected corruption. Success!
227-
Left e ->
228-
tabulate
229-
"Result"
230-
["Corruption detected: " <> getConstructorName e]
231-
True
232-
-- The corruption was not detected. Failure!
233-
Right _ -> property False
184+
-- Corrupt the snapshot
185+
flipRandomBitInRandomFile hfs pickFileBit (getNamedSnapshotDir namedSnapDir) >>= \case
186+
Nothing -> pure $ property False
187+
Just (path, j) -> do
188+
-- Some info for the test output
189+
let tabCorruptedFile = tabulate "Corrupted file" [show path]
190+
counterCorruptedFile = counterexample ("Corrupted file: " ++ show path)
191+
tabFlippedBit = tabulate "Flipped bit" [showPowersOf10 j]
192+
counterFlippedBit = counterexample ("Flipped bit: " ++ show j)
193+
194+
t' <- try @_ @SomeException $ bracket (openSnap s) close $ \_ -> pure ()
195+
pure $
196+
tabCorruptedFile $ counterCorruptedFile $ tabFlippedBit $ counterFlippedBit $
197+
case t' of
198+
-- If we find an error, we detected corruption. Success!
199+
Left e ->
200+
tabulate
201+
"Result"
202+
["Corruption detected: " <> getConstructorName e]
203+
True
204+
-- The corruption was not detected. Failure!
205+
Right _ -> property False
234206
where
235207
root = FS.mkFsPath []
236208
namedSnapDir = namedSnapshotDir (SessionRoot root) snapName

0 commit comments

Comments
 (0)