|
| 1 | +{-# LANGUAGE LambdaCase #-} |
| 2 | +{-# LANGUAGE OverloadedStrings #-} |
| 3 | +{-# LANGUAGE RecordWildCards #-} |
| 4 | + |
| 5 | +module Test.Database.LSMTree (tests) where |
| 6 | + |
| 7 | +import Control.Tracer |
| 8 | +import Data.Function (on) |
| 9 | +import Data.Monoid |
| 10 | +import qualified Data.Vector as V |
| 11 | +import qualified Data.Vector.Algorithms as VA |
| 12 | +import Data.Void |
| 13 | +import Data.Word |
| 14 | +import Database.LSMTree |
| 15 | +import Database.LSMTree.Extras (showRangesOf) |
| 16 | +import Database.LSMTree.Extras.Generators () |
| 17 | +import qualified System.FS.API as FS |
| 18 | +import Test.QuickCheck |
| 19 | +import Test.Tasty |
| 20 | +import Test.Tasty.QuickCheck |
| 21 | +import Test.Util.FS |
| 22 | + |
| 23 | +tests :: TestTree |
| 24 | +tests = testGroup "Test.Database.LSMTree" [ |
| 25 | + testProperty "prop_goodAndBandSessionSalt" prop_goodAndBandSessionSalt |
| 26 | + ] |
| 27 | + |
| 28 | +-- | For now, the session salt is always set by the user when opening a session. |
| 29 | +-- This is fine for new sessions, because there is no data in the session |
| 30 | +-- directory yet, but if the user passes the wrong salt when restoring a |
| 31 | +-- session, then bad things can happen. In particular, bloom filters stored in a |
| 32 | +-- snapshot might have been created with one salt, but if we query them with a |
| 33 | +-- different salt, then the query results are bad. This test verifies that |
| 34 | +-- reopening a session and snapshot with a good salt leads to good lookup |
| 35 | +-- results, and it verifies that doign the same with a bad salt leads to bad |
| 36 | +-- lookup results. |
| 37 | +-- |
| 38 | +-- NOTE: this only tests with /positive/ lookups, i.e., lookups for keys that |
| 39 | +-- are known to exist in the tables. |
| 40 | +-- |
| 41 | +-- TODO: store the session salt in the session directory, so that the user can |
| 42 | +-- not set a bad salt. |
| 43 | +prop_goodAndBandSessionSalt :: |
| 44 | + Positive (Small Int) |
| 45 | + -> V.Vector (Key, Value) |
| 46 | + -> Property |
| 47 | +prop_goodAndBandSessionSalt (Positive (Small bufferSize)) ins = |
| 48 | + checkCoverage $ |
| 49 | + ioProperty $ |
| 50 | + withTempIOHasBlockIO "prop_sessionSalt" $ \hfs hbio -> do |
| 51 | + -- Open a session and create a snapshot for some arbitrary table contents |
| 52 | + withSession nullTracer hfs hbio goodSalt sessionDir $ \session -> |
| 53 | + withTableWith conf session $ \(table :: Table IO Key Value Void) -> do |
| 54 | + inserts table $ V.map (\(k, v) -> (k, v, Nothing)) insWithoutDupKeys |
| 55 | + saveSnapshot "snap" "KeyValueBlob" table |
| 56 | + |
| 57 | + -- Determine the expected results of key lookups |
| 58 | + let |
| 59 | + expectedValues :: V.Vector (Maybe Value) |
| 60 | + expectedValues = V.map (Just . snd) insWithoutDupKeys |
| 61 | + |
| 62 | + -- Restore the session using the good salt, open the snapshot, perform lookups |
| 63 | + goodLookups <- |
| 64 | + withSession nullTracer hfs hbio goodSalt sessionDir $ \session -> |
| 65 | + withTableFromSnapshot session "snap" "KeyValueBlob" $ \(table :: Table IO Key Value Void) -> do |
| 66 | + lookups table $ V.map fst insWithoutDupKeys |
| 67 | + |
| 68 | + -- Determine the result of key lookups using the good salt |
| 69 | + let |
| 70 | + goodValues :: V.Vector (Maybe Value) |
| 71 | + goodValues = V.map getValue goodLookups |
| 72 | + |
| 73 | + -- Restore the session using a bad salt, open the snapshot, perform lookups |
| 74 | + badLookups <- |
| 75 | + withSession nullTracer hfs hbio badSalt sessionDir $ \session -> |
| 76 | + withTableFromSnapshot session "snap" "KeyValueBlob" $ \(table :: Table IO Key Value Void) -> do |
| 77 | + lookups table $ V.map fst insWithoutDupKeys |
| 78 | + |
| 79 | + -- Determine the result of key lookups using a bad salt |
| 80 | + let |
| 81 | + badValues :: V.Vector (Maybe Value) |
| 82 | + badValues = V.map getValue badLookups |
| 83 | + |
| 84 | + pure $ |
| 85 | + tabulate "number of keys" [ showRangesOf 10 (V.length insWithoutDupKeys) ] $ |
| 86 | + -- For a significant portion of the cases, the lookups results obtained |
| 87 | + -- using a bad salt should mismatch the expected lookup results |
| 88 | + cover 40 ( expectedValues /= badValues ) "bad salt leads to bad lookups" $ |
| 89 | + -- The lookup results using a bad salt should /always/ match the |
| 90 | + -- expected lookup results. |
| 91 | + expectedValues === goodValues |
| 92 | + where |
| 93 | + -- Duplicate keys in inserts make the property more complicated, because |
| 94 | + -- keys that are inserted /earlier/ (towards the head of the vector) are |
| 95 | + -- overridden by keys that are inserted /later/ (towards the tail of the |
| 96 | + -- vector). So, we remove duplicate keys instead |
| 97 | + insWithoutDupKeys :: V.Vector (Key, Value) |
| 98 | + insWithoutDupKeys = VA.nubBy (compare `on` fst) ins |
| 99 | + |
| 100 | + goodSalt :: Salt |
| 101 | + goodSalt = 17 |
| 102 | + |
| 103 | + badSalt :: Salt |
| 104 | + badSalt = 19 |
| 105 | + |
| 106 | + sessionDir = FS.mkFsPath [] |
| 107 | + |
| 108 | + conf = defaultTableConfig { |
| 109 | + confWriteBufferAlloc = AllocNumEntries bufferSize |
| 110 | + } |
| 111 | + |
| 112 | +newtype Key = Key Word64 |
| 113 | + deriving stock (Show, Eq, Ord) |
| 114 | + deriving newtype (Arbitrary, SerialiseKey) |
| 115 | + |
| 116 | +newtype Value = Value Word64 |
| 117 | + deriving stock (Show, Eq) |
| 118 | + deriving newtype (Arbitrary, SerialiseValue) |
| 119 | + deriving ResolveValue via Sum Word64 |
| 120 | + |
| 121 | +newtype Blob = Blob Void |
| 122 | + deriving stock (Show, Eq) |
| 123 | + deriving newtype SerialiseValue |
0 commit comments