packages feed

arena (empty) → 0.1

raw patch · 7 files changed

+655/−0 lines, 7 filesdep +arenadep +basedep +bytessetup-changed

Dependencies added: arena, base, bytes, bytestring, containers, criterion, digest, directory, filepath, mtl, persistent-vector, safe, semigroups, unix

Files

+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2015-2016, davean++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of davean nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ arena.cabal view
@@ -0,0 +1,79 @@+name:                arena+version:             0.1+synopsis:            A journaled data store+license:             BSD3+license-file:        LICENSE+author:              davean+maintainer:          davean <davean@xkcd.com>+copyright:           Copyright (C) 2015-2016 davean+stability:           provisional+category:            Database+build-type:          Simple+cabal-version:       >=1.22+bug-reports:         oss@xkcd.com+description:+  @arena@ provides durable storage of data and summaries of that data.++  On insert, data is written to a journal. Each piece of data is added to a running summary of the current journal. When the summary indicates the correct amount of data has accumulated, the journal data is moved, as a block accompanied by its summary, to long-term storage. The data type, summary type, and accumulation policy are configurable.++source-repository head+  type: git+  location: http://git.xkrd.net/distributed-systems/arena++library+  hs-source-dirs:      src+  default-language:    Haskell2010+  exposed-modules:+        Database.Arena+  build-depends:+        base >=4.8 && <5+      , bytestring >= 0.10.4.1+      , filepath >= 1.2.0.0+      , semigroups >= 0.17+      , mtl >= 2.2+      , bytes >= 0.14+      , digest >= 0.0.1.2+      , directory >= 1.2.5.0+      , safe >= 0.3+      , containers >= 0.5.4.0+      , unix >= 2.7.1.0+      , persistent-vector >= 0.1.1++test-suite test+  type: exitcode-stdio-1.0+  default-language: Haskell2010+  hs-source-dirs: tests+  main-is: test.hs+  build-depends:+      base >=4.8 && <5+    , arena+    , directory+    , mtl+    , semigroups++test-suite example+  type: exitcode-stdio-1.0+  default-language: Haskell2010+  hs-source-dirs: tests+  main-is: example.hs+  build-depends:+      base >=4.8 && <5+    , arena+    , bytes+    , containers+    , directory+    , mtl+    , semigroups++benchmark bench+  type: exitcode-stdio-1.0+  default-language: Haskell2010+  hs-source-dirs: bench+  main-is: bench.hs+  build-depends:+      base >=4.8 && <5+    , arena+    , directory+    , criterion >= 1.1.1.0+    , mtl+    , semigroups
+ bench/bench.hs view
@@ -0,0 +1,32 @@+{-# LANGUAGE OverloadedStrings   #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TupleSections       #-}++module Main where++import           Criterion.Main+import           Data.Semigroup+import           Database.Arena+import           System.Directory++prepDirectory :: IO ()+prepDirectory = do+  createDirectoryIfMissing True "test_data"+  removeDirectoryRecursive "test_data"+  createDirectoryIfMissing True "test_data/journal"+  createDirectoryIfMissing True "test_data/data"++main = defaultMain [+        bgroup "inserts" [ bench "20"    . nfIO $ inserts 20+                         , bench "10000" . nfIO $ inserts 10000]+       ]+++startTestArena :: Int -> IO (ArenaDB (Sum Int, Sum Int) Int Int)+startTestArena n = startArena ((1,) . Sum) (getSum . snd) ((succ n <=) . getSum . fst) "test_data"++inserts :: Int -> IO ()+inserts n = do+  prepDirectory+  ad <- startTestArena n+  runArena ad $ mapM_ addData [1..n]
+ src/Database/Arena.hs view
@@ -0,0 +1,330 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE RecordWildCards            #-}+{-# LANGUAGE ScopedTypeVariables        #-}++module Database.Arena+    (+      ArenaLocation(..)++    , ArenaT+    , Arena+    , ArenaDB+    , runArenaT+    , runArena+    , startArena, initArena+    , addData+    , accessData+    ) where++import           Control.Applicative+import           Control.Concurrent.MVar+import           Control.Monad+import           Control.Monad.Reader+import           Control.Monad.Trans+import           Data.Bytes.Get+import           Data.Bytes.Put+import           Data.Bytes.Serial+import qualified Data.ByteString         as BS+import qualified Data.ByteString.Lazy    as BSL+import           Data.Digest.CRC32+import           Data.Either+import           Data.Foldable+import           Data.IORef+import           Data.List+import           Data.Maybe+import           Data.Semigroup+import qualified Data.Set                as Set+import           Data.String+import           Data.Typeable           (cast)+import qualified Data.Vector.Persistent  as PV+import           Data.Word+import           GHC.IO.Device+import           GHC.IO.Exception+import qualified GHC.IO.FD               as FD+import           GHC.IO.Handle+import qualified GHC.IO.Handle.FD        as FD+import           GHC.IO.Handle.Internals+import           GHC.IO.Handle.Types+import           Safe+import           System.Directory+import           System.FilePath+import           System.IO+import           System.IO.Error+import           System.Posix.IO         (handleToFd)+import           System.Posix.Types      (Fd (Fd))+import           System.Posix.Unistd     (fileSynchroniseDataOnly)++-- | The base directory for the arena files to be stored under.+newtype ArenaLocation = ArenaLocation { getArenaLocation :: FilePath }+  deriving (Eq, Ord, Read, Show, IsString)++type ArenaID     = Word32+type JournalHash = Word32++data JournalFrame =+    JF {+      jLength :: Word32+      -- ^ The amount of data stored in the frame.+    , jHash :: JournalHash+      -- ^ A hash of the data stored in the frame so we can detect write corruption.+      --   This is sufficient for the entire frame as all other elements are fixed+      --   size and directly impact the data's hash.+    , jData :: BS.ByteString+      -- ^ The user's serialized data.+    }++mkJournal :: Serial a => a -> JournalFrame+mkJournal a = JF (fromIntegral . BS.length $ d) (crc32 d) d+  where d = runPutS . serialize $ a++instance Serial JournalFrame where+    serialize (JF l h d) = serialize l *> serialize h *> putByteString d+    deserialize = do+      l <- deserialize+      h <- deserialize+      d <- getByteString . fromIntegral $ l+      if h == crc32 d+      then return $ JF l h d+      else fail "Journal Frame failed CRC check---Also, uh, this method shouldn't be in Monad"++data OpenJournal a b =+  OJ {+    ojArenaID :: ArenaID+    -- ^ The current datablock generation, starting from zero and strictly monotonic.+  , ojHandle :: Handle+    -- ^ The journal file, opened and positioned for writing+  , ojSummary :: Option b+    -- ^ The current summary, if any values have been stored in the journal+  , ojVals :: PV.Vector a+    -- ^ The values currently held in the journal on disk, in a 'PV.Vector' (instead of a list) to+    --   improve GC performance, and provide fast snoc to avoid the use of reverse.+  }++readDataFile :: (MonadIO m, Serial a) => ArenaLocation -> ArenaID -> m [a]+readDataFile l ai = liftIO $ do+  d <- BSL.readFile (dataFile l ai)+  return $ runGetL (many deserialize) d++syncHandle :: Handle -> IO ()+syncHandle h = do+    hFlush h+    unsafeSyncHandle h+  where+    unsafeSyncHandle h@(DuplexHandle {}) =+      ioError (ioeSetErrorString (mkIOError IllegalOperation+                                  "unsafeSyncHandle" (Just h) Nothing)+               "unsafeSyncHandle only works on file descriptors")+    unsafeSyncHandle h@(FileHandle _ m) = do+      withMVar m $ \h_@Handle__{haType=_,..} -> do+        case cast haDevice of+          Nothing -> ioError (ioeSetErrorString (mkIOError IllegalOperation+                                                 "unsafeSyncHandle" (Just h) Nothing)+                              ("handle is not a file descriptor"))+          Just fd -> do+            flushWriteBuffer h_+            fileSynchroniseDataOnly . Fd . FD.fdFD $ fd++withFileSync :: FilePath -> (Handle -> IO r) -> IO r+withFileSync fp f = liftIO $ withFile fp WriteMode go+  where go h = f h <* syncHandle h++data ArenaDB summary finalized d = ArenaDB {+      adSummarize      :: d -> summary+      -- ^ Convert the user's data to an instance of the summary semigroup+    , adFinalize       :: summary -> finalized+      -- ^ Removal of data only used for the merge operation or determining fullness,+      --   changing the summary to a version used purely for the datablock indexing.+    , adArenaFull      :: summary -> Bool+      -- ^ Determine if we've reached a datablock boundary.+    , adArenaLocation  :: ArenaLocation+      -- ^ The location on the filesystem in which we store the data and journal.+    , adDataRef        :: IORef [(finalized, IO [d])]+      -- ^ The list of datablocks, seen as their summary and accessors, for read operations.+    , adCurrentJournal :: MVar (OpenJournal d summary)+      -- ^ The journal for writing new data, in an 'MVar' so we can lock it for consistency when+      --   doing writes or data access.+    }++-- | * @s@: The summary semigroup for the journal data.+--   * @f@: The finalized summary associated with a datablock.+--   * @d@: The user's datatype stored in the arena.+newtype ArenaT s f d m a = ArenaT { unArenaT :: ReaderT (ArenaDB s f d) m a }+    deriving (Functor, Applicative, Monad, MonadTrans, MonadReader (ArenaDB s f d), MonadIO)++runArenaT :: ArenaDB s f d -> ArenaT s f d m a -> m a+runArenaT ad = flip runReaderT ad . unArenaT++type Arena s f d a = ArenaT s f d IO a++runArena :: ArenaDB s f d -> Arena s f d a -> IO a+runArena = runArenaT++summarize :: Monad m => d -> ArenaT s f d m s+summarize d = asks adSummarize <*> pure d++finalize :: Monad m => s -> ArenaT s f d m f+finalize s = asks adFinalize <*> pure s++journalDir, dataDir, tempJournal :: ArenaLocation -> FilePath+journalDir (ArenaLocation fp) = fp            </> "journal"+dataDir    (ArenaLocation fp) = fp            </> "data"+tempJournal al                = journalDir al </> "temp"++journalFile, dataFile, dataSummaryFile :: ArenaLocation -> ArenaID -> FilePath+journalFile     al i = journalDir al </> show i+dataFile        al i = dataDir    al </> addExtension (show i) "data"+dataSummaryFile al i = dataDir    al </> addExtension (show i) "header"++journalDir', dataDir', tempJournal' :: Monad m => ArenaT s f d m FilePath+journalDir'  = asks (journalDir  . adArenaLocation)+dataDir'     = asks (dataDir     . adArenaLocation)+tempJournal' = asks (tempJournal . adArenaLocation)++journalFile', dataFile', dataSummaryFile' :: Monad m => ArenaID -> ArenaT s f d m FilePath+journalFile'     ai = asks (journalFile     . adArenaLocation) <*> pure ai+dataFile'        ai = asks (dataFile        . adArenaLocation) <*> pure ai+dataSummaryFile' ai = asks (dataSummaryFile . adArenaLocation) <*> pure ai++data TheFiles = TheFiles {+      theJournalDir+    , theDataDir+    , theTempJournal+    , theJournalFile+    , theDataFile+    , theDataSummaryFile :: FilePath+    } deriving (Eq, Ord, Read, Show)++theFiles :: Monad m => Maybe ArenaID -> ArenaT s f d m TheFiles+theFiles Nothing   = theFiles (Just $ -1)+theFiles (Just ai) =+  TheFiles <$> journalDir'     <*> dataDir'     <*> tempJournal'+           <*> journalFile' ai <*> dataFile' ai <*> dataSummaryFile' ai++-- | Setup the directory structure for Arena.+--   This is performed implicitly by 'startArena'.+initArena :: ArenaLocation -> IO ()+initArena al = do+  createDirectoryIfMissing True . journalDir $ al+  createDirectoryIfMissing True . dataDir $ al++-- | Launch an 'ArenaDB', using the given summarizing, finalizing, and block policy functions,+--   at the given 'ArenaLocation'.+--+--   __NB:__ Two 'ArenaDB's must not be run concurrently with a shared 'ArenaLocation'.+--   Data loss from the journal is likely to result.+startArena+  :: (Serial d, Serial f, Semigroup s)+    => (d -> s) -> (s -> f) -> (s -> Bool) -> ArenaLocation -> IO (ArenaDB s f d)+startArena adSummarize adFinalize adArenaFull adArenaLocation = do+    initArena adArenaLocation+    runArena fakeConf $ do+      adCurrentJournal <- internArenas >>= cleanJournal >>= liftIO . newMVar+      adDataRef        <- readAllData  >>= liftIO . newIORef+      return ArenaDB {..}+    where fakeConf = ArenaDB { adCurrentJournal = error "uninitialized current journal"+                               , adDataRef        = error "uninitialized data ref"+                               , .. }++readJournalFile :: (Serial d, MonadIO m) => ArenaID -> ArenaT s f d m (PV.Vector d)+readJournalFile ai = do+  d <- journalFile' ai >>= liftIO . BSL.readFile+  return . PV.fromList . map (head . rights . pure . runGetS deserialize . jData) . runGetL (many deserialize) $ d+++cleanJournal :: (MonadIO m, Serial d, Semigroup s) => ArenaID -> ArenaT s f d m (OpenJournal d s)+cleanJournal ai = do+  TheFiles {..} <- theFiles (Just ai)+  liftIO $ doesFileExist theTempJournal >>= (`when` removeFile theTempJournal)+  je <- liftIO $ doesFileExist theJournalFile+  case je of+    False -> do+     jh <- liftIO $ openFile theJournalFile WriteMode+     return $ OJ ai jh (Option Nothing) mempty+    True -> do+      as <- readJournalFile ai+      liftIO $ withFileSync theTempJournal $ \h ->+        mapM_ (BS.hPutStr h . runPutS . serialize . mkJournal) as+      liftIO $ renameFile theTempJournal theJournalFile+      jh <- liftIO $ openFile theJournalFile AppendMode+      as' <- if null as+            then return $ Option Nothing+            else Option . Just <$> regenerateSummary as+      return $ OJ ai jh as' as++readArenaIDs :: MonadIO m => FilePath -> m [ArenaID]+readArenaIDs dir = liftIO go+    where go = mapMaybe readMay <$> listDirectory dir++internArenas :: (MonadIO m, Serial d, Serial f, Semigroup s) => ArenaT s f d m ArenaID+internArenas = do+  js <- journalDir' >>= readArenaIDs+  if null js+  then return 0+  else do+    let latest = maximum js+        old    = delete latest js+    mapM_ internArenaFile old+    return latest++regenerateSummary :: (Semigroup s, Monad m) => PV.Vector d -> ArenaT s f d m s+regenerateSummary ds = foldl1 (<>) <$> traverse summarize ds++internArenaFile :: (MonadIO m, Serial f, Serial d, Semigroup s) => ArenaID -> ArenaT s f d m ()+internArenaFile ai = do+  TheFiles {..} <- theFiles (Just ai)+  as  <- readJournalFile ai+  fas <- regenerateSummary as >>= finalize+  liftIO $ do withFileSync theDataFile  $ \h -> mapM_ (BS.hPutStr h . runPutS . serialize) as+              withFileSync theDataSummaryFile $ \h -> BS.hPutStr h . runPutS . serialize $ fas+              removeFile theJournalFile+++readDataFile' :: (MonadIO m, Serial d) => ArenaID -> ArenaT s f d m (IO [d])+readDataFile' ai = asks (readDataFile . adArenaLocation) <*> pure ai++readAllData :: (MonadIO m, Serial f, Serial d) => ArenaT s f d m [(f, IO [d])]+readAllData = do+  TheFiles {..} <- theFiles Nothing+  ds <- Set.fromList . mapMaybe (readMay . dropExtension) <$> liftIO (listDirectory theDataDir)+  forM (Set.toList ds) $ \d -> do+                dsf <- dataSummaryFile' d+                c <- runGetL deserialize <$> liftIO (BSL.readFile dsf)+                rdf <- readDataFile' d+                return (c, rdf)++-- | Access an atomic snapshot of the 'ArenaDB' as a list of summaries and accessors+--   for their associated data. One datablock is the journal, and thus does not satisfy+--   the block policy.+--+--   The returned IO actions in the list provide access to a state consistent with the time+--   when the list of accessors was returned, but do not hold more then the journal at that+--   time's contents in memory.+accessData :: MonadIO m => ArenaT s f d m [(f, IO [d])]+accessData = do+  ArenaDB {..} <- ask+  liftIO . withMVar adCurrentJournal $ \(OJ _ _ s as) -> do+               d <- readIORef adDataRef+               return $ if null as+                        then d+                        else (adFinalize . fromJust . getOption $ s, return . toList $ as) : d++-- | Durably insert a piece of data into the 'ArenaDB'.+--   This funtion returns after the data is sync'd to disk.+addData :: (MonadIO m, Serial d, Serial f, Semigroup s) => d -> ArenaT s f d m ()+addData d = do+  conf@ArenaDB {..} <- ask+  liftIO . modifyMVar_ adCurrentJournal $ \(OJ ai h s ds) -> do+              BS.hPutStr h . runPutS . serialize . mkJournal $ d+              let s' = s <> (pure . adSummarize $ d)+              syncHandle h+              case adArenaFull . fromJust . getOption $ s' of+                False -> return $ OJ ai h s' (ds `PV.snoc` d)+                True -> do+                  hClose h+                  let (ArenaT rdr) = internArenaFile ai -- :: Arena s f d ()+                  runReaderT rdr conf+                  atomicModifyIORef' adDataRef $ \ods ->+                    ((adFinalize . fromJust . getOption $ s', readDataFile adArenaLocation ai):ods, ())+                  let ai' = ai + 1+                  h' <- openFile (journalFile adArenaLocation ai') WriteMode+                  return $ OJ ai' h' mempty mempty
+ tests/example.hs view
@@ -0,0 +1,128 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE OverloadedStrings #-}++module Main where++import           Control.Monad.Trans+import           GHC.Generics+import           Data.Semigroup+import           Data.Bytes.Serial+import           Database.Arena+import           Data.List+import           Data.Map (Map)+import qualified Data.Map as Map+import           Data.Ord++-- | Suppose we have a bunch of (simplified) web server log entries, each containing the source IP,+--   time, and number of bytes served for an HTTP request:+data LogEntry =+  LogEntry {+    leIP          :: String+  , leTimeStamp   :: Time+  , leBytesServed :: Int+  } deriving (Generic, Show)++type Time = Int++-- | We might query this data to find the amount of data we've served over a given time period:+bytesServedInPeriod :: Time -> Time -> [LogEntry] -> Int+bytesServedInPeriod start end les = sum [ bytes | LogEntry _ ts bytes <- les+                                                , ts >= start+                                                , ts <= end ]++-- | To execute this query more more efficiently, we'd like to batch up our entries and store+--   them alongside an index of some salient information about them.  Specifically, we'd like to+--   know how much data we've served, over what time period we served it, and the source IP that+--   made the most requests (the "heaviest hitter").+data LogIndex =+  LogIndex {+    liHeavyHitter :: String+  , liBytesServed :: Int+  , liStartTime   :: Time+  , liEndTime     :: Time+  } deriving (Generic, Show)++-- | Conceptually, our data can now look like @[(LogIndex, [LogEntry])]@+fastBytesServedInPeriod :: Time -> Time -> [(LogIndex, [LogEntry])] -> Int+fastBytesServedInPeriod start end blocks = sum fullyContained + sum partiallyContained+    where fullyContained = [ bs | (LogIndex _ bs liStart liEnd, _) <- blocks+                                , liStart >= start && liEnd <= end ]+          partiallyContained = [ bytesServedInPeriod start end es+                                 | (LogIndex _ bs liStart liEnd, es) <- blocks+                                 , (liStart <= start && start <= liEnd) || (liStart <= end && end <= liEnd)]++-- | To build these indexed blocks <https://en.wikipedia.org/wiki/Online_algorithm online>, we need+--   to keep track of some more information.  In particular, we need to keep count of the log+--   entries for each IP, the total bytes served, the start and end times, and the total count of+--   the log entries as we process them.+data LogSummary =+  LogSummary {+    lsHeavyHitters :: Map String Int+  , lsBytesServed  :: Sum Int+  , lsStartTime    :: Min Time+  , lsEndTime      :: Max Time+  , lsCount        :: Sum Int+  }++-- | By making 'LogSummary' a 'Semigroup', @arena@ can maintain the running summary we require+--   automatically.+instance Semigroup LogSummary where+    LogSummary hh bs st et c <> LogSummary hh' bs' st' et' c' =+       LogSummary (Map.unionWith (+) hh hh') (bs <> bs') (st <> st') (et <> et') (c <> c')++-- | All we need to be able to do is summarize individual 'LogEntry's:+summarizeEntry :: LogEntry -> LogSummary+summarizeEntry (LogEntry ip t bytes) =+  LogSummary (Map.singleton ip 1) (Sum bytes) (Min t) (Max t) (Sum 1)++-- | Of course we need to be able to convert our summaries into 'LogIndex'es:+summaryToIndex :: LogSummary -> LogIndex+summaryToIndex (LogSummary hh (Sum bs) (Min st) (Max et) _) =+    LogIndex heaviestHitter bs st et+  where+    heaviestHitter = fst . maximumBy (comparing snd) . Map.toList $ hh++-- | Now we just need to know when we're done building a block.  Let's assume that 5000 entries per+--   block is a reasonable policy:+blockDone :: LogSummary -> Bool+blockDone ls = lsCount ls == 5000++-- | In order for @arena@ to actually write our data to disk, we need to be able to serialize it.+--   We can just let GHC generics get us the instances we need:+instance Serial LogEntry+instance Serial LogIndex++-- | We can have @arena@ tie all this together for us:+type LogArena a = Arena LogSummary LogIndex LogEntry a++makeLogDB :: ArenaLocation -> IO (ArenaDB LogSummary LogIndex LogEntry)+makeLogDB = startArena summarizeEntry summaryToIndex blockDone++-- | Now we can use @arena@'s 'accessData' function to pull our indexed blocks off disk.  Notice+--   that 'accessData' gives back the slightly different type then above:+--   @[(LogIndex, IO [LogEntry])]@.  This is because @arena@ won't read the actual 'LogEntry's+--   (of which there might be many) unless you specifically force the corresponding IO action.+queryBytesServed :: Time -> Time -> LogArena Int+queryBytesServed start end = do+  blocks <- accessData+  let fullyContained = [ bs | (LogIndex _ bs liStart liEnd, _) <- blocks+                            , liStart >= start && liEnd <= end ]+      partiallyContained = [ bytesServedInPeriod start end <$> es+                              | (LogIndex _ bs liStart liEnd, es) <- blocks+                              , (liStart <= start && start <= liEnd) || (liStart <= end && end <= liEnd)]+  forced <- liftIO . sequence $ partiallyContained+  return $ sum fullyContained + sum forced++-- | Now to use the database, we just add and query data:+addAndQueryData :: LogArena ()+addAndQueryData = do+  mapM_ addData [LogEntry (show $ n `mod` 23) n ((n * n - 1) `mod` 29)+                     | n <- [1..7000]] -- sufficiently random looking data+  46666 <- queryBytesServed 2345 5678+  return ()++-- | Make the database in @/tmp@ and go!+main :: IO ()+main = do+  db <- makeLogDB "/tmp/log_arena"+  runArena db addAndQueryData
+ tests/test.hs view
@@ -0,0 +1,54 @@+{-# LANGUAGE ScopedTypeVariables, TupleSections, OverloadedStrings #-}+module Main where++import System.Mem+import Control.Concurrent++import Control.Monad.Trans+import Data.List+import Data.Semigroup+import System.Directory+import Database.Arena+import qualified Control.Exception as E++assert :: Bool -> String -> IO ()+assert True _ = return ()+assert False err = E.throwIO . E.AssertionFailed $ err++assert' :: MonadIO m => Bool -> String -> m ()+assert' b s = liftIO $ assert b s++prepDirectory :: IO ()+prepDirectory = do+  createDirectoryIfMissing True "test_data"+  removeDirectoryRecursive "test_data"+  createDirectoryIfMissing True "test_data/journal"+  createDirectoryIfMissing True "test_data/data"++main :: IO ()+main = do+  prepDirectory+  tok <- startArena ((1,) . Sum) (getSum . snd) ((5 <) . getSum . fst) "test_data"+  as <- runArena tok monadTest1+  runArena tok $ monadTest2 as++monadTest1 :: Arena (Sum Int, Sum Int) Int Int [(Int, [Int])]+monadTest1 = do+  addData 65+  sm <- (sum . fmap fst) <$> accessData+  assert' (sm == 65) $ "Count was wrong: " ++ show sm+  mapM_ addData [1..14]+  sm <- (sum . fmap fst) <$> accessData+  as <- accessData >>= mapM (\(c, act) -> (c,) <$> liftIO act)+  liftIO $! do putStrLn . show . sort $ as+               assert' (sm == (65 + sum [1..14])) "Larger sum incorrect!"+               performMajorGC+               threadDelay 100000+               performMajorGC+  return as++monadTest2 :: [(Int, [Int])] -> Arena (Sum Int, Sum Int) Int Int ()+monadTest2 as = do+  as' <- accessData >>= mapM (\(c, act) -> (c,) <$> liftIO act)+  liftIO $! do putStrLn . show . sort $ as'+               assert ((sort as) == (sort as')) "doesn't match old data"