haskey-0.1.0.0: src/Database/Haskey/Store/InMemory.hs
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ScopedTypeVariables #-}
-- | Binary in-memory storage back-end. Can be used as a storage back-end for
-- the append-only page allocator (see "Data.BTree.Alloc").
module Database.Haskey.Store.InMemory (
-- * Storage
Page(..)
, MemoryFile
, MemoryFiles
, MemoryStoreConfig(..)
, defMemoryStoreConfig
, memoryStoreConfigWithPageSize
, MemoryStoreT
, runMemoryStoreT
, newEmptyMemoryStore
-- * Exceptions
, FileNotFoundError(..)
, PageNotFoundError(..)
, WrongNodeTypeError(..)
, WrongOverflowValueError(..)
) where
import Control.Applicative (Applicative, (<$>))
import Control.Monad
import Control.Monad.Catch
import Control.Monad.IO.Class
import Control.Monad.Reader
import Data.ByteString (ByteString)
import Data.ByteString.Lazy (toStrict)
import Data.Coerce
import Data.IORef
import Data.Map (Map)
import Data.Maybe (fromJust)
import Data.Typeable (Typeable)
import Data.Word (Word64)
import qualified Data.Map as M
import Data.BTree.Impure.Structures
import Data.BTree.Primitives
import Database.Haskey.Alloc.Concurrent
import Database.Haskey.Store.Class
import Database.Haskey.Store.Page
import Database.Haskey.Utils.Monad.Catch (justErrM)
--------------------------------------------------------------------------------
-- | A file containing a collection of pages.
type MemoryFile = Map PageId ByteString
-- | A collection of 'File's, each associated with a certain @fp@ handle.
--
-- This is shareable amongst multiple threads.
type MemoryFiles fp = IORef (Map fp MemoryFile)
-- | Access the files.
get :: MonadIO m => MemoryStoreT fp m (Map fp MemoryFile)
get = MemoryStoreT . lift $ ask >>= liftIO . readIORef
-- | Access the files.
gets :: (Functor m, MonadIO m)
=> (Map fp MemoryFile -> a)
-> MemoryStoreT fp m a
gets f = f <$> get
-- | Modify the files.
modify' :: MonadIO m =>
(Map fp MemoryFile -> Map fp MemoryFile)
-> MemoryStoreT fp m ()
modify' f = MemoryStoreT . lift $ ask >>= liftIO . flip modifyIORef' f
lookupFile :: (MonadThrow m, Ord fp, Show fp, Typeable fp)
=> fp -> Map fp MemoryFile -> m MemoryFile
lookupFile fp m = justErrM (FileNotFoundError fp) $ M.lookup fp m
lookupPage :: (Functor m, MonadThrow m, Ord fp, Show fp, Typeable fp)
=> fp -> PageId -> Map fp MemoryFile -> m ByteString
lookupPage fp pid m = M.lookup pid <$> lookupFile fp m
>>= justErrM (PageNotFoundError fp pid)
-- | Monad in which binary storage operations can take place.
--
-- Two important instances are 'StoreM' making it a storage back-end, and
-- 'ConcurrentMetaStoreM' making it a storage back-end compatible with the
-- concurrent page allocator.
newtype MemoryStoreT fp m a = MemoryStoreT
{ fromMemoryStoreT :: ReaderT MemoryStoreConfig (ReaderT (MemoryFiles fp) m) a
} deriving (Applicative, Functor, Monad,
MonadIO, MonadThrow, MonadCatch, MonadMask,
MonadReader MemoryStoreConfig)
-- | Memory store configuration.
--
-- The default configuration can be obtained by using 'defMemoryStoreConfig'.
--
-- A configuration with a specific page size can be obtained by using
-- 'memoryStoreConfigWithPageSize'.
data MemoryStoreConfig = MemoryStoreConfig {
memoryStoreConfigPageSize :: !PageSize
, memoryStoreConfigMaxKeySize :: !Word64
, memoryStoreConfigMaxValueSize :: !Word64
} deriving (Show)
-- | The default configuration.
--
-- This is an unwrapped 'memoryStoreConfigWithPageSize' with a page size of
-- 4096.
defMemoryStoreConfig :: MemoryStoreConfig
defMemoryStoreConfig = fromJust (memoryStoreConfigWithPageSize 4096)
-- | Create a configuration with a specific page size.
--
-- The maximum key and value sizes are calculated using 'calculateMaxKeySize'
-- and 'calculateMaxValueSize'.
--
-- If the page size is too small, 'Nothing' is returned.
memoryStoreConfigWithPageSize :: PageSize -> Maybe MemoryStoreConfig
memoryStoreConfigWithPageSize pageSize
| keySize < 8 && valueSize < 8 = Nothing
| otherwise = Just MemoryStoreConfig {
memoryStoreConfigPageSize = pageSize
, memoryStoreConfigMaxKeySize = keySize
, memoryStoreConfigMaxValueSize = valueSize }
where
keySize = calculateMaxKeySize pageSize (encodedPageSize zeroHeight)
valueSize = calculateMaxValueSize pageSize keySize (encodedPageSize zeroHeight)
-- | Run the storage operations in the 'MemoryStoreT' monad, given a collection of
-- 'File's.
runMemoryStoreT :: MemoryStoreT fp m a -- ^ Action to run
-> MemoryStoreConfig -- ^ Configuration
-> MemoryFiles fp -- ^ Data
-> m a
runMemoryStoreT m config = runReaderT (runReaderT (fromMemoryStoreT m) config)
-- | Construct a store with an empty database with name of type @hnd@.
newEmptyMemoryStore :: IO (MemoryFiles hnd)
newEmptyMemoryStore = newIORef M.empty
--------------------------------------------------------------------------------
instance (Applicative m, Monad m, MonadIO m, MonadThrow m,
Ord fp, Show fp, Typeable fp) =>
StoreM fp (MemoryStoreT fp m)
where
openHandle fp =
modify' $ M.insertWith (flip const) fp M.empty
flushHandle _ = return ()
closeHandle _ = return ()
removeHandle fp =
modify' $ M.delete fp
nodePageSize = return encodedPageSize
maxPageSize = asks memoryStoreConfigPageSize
maxKeySize = asks memoryStoreConfigMaxKeySize
maxValueSize = asks memoryStoreConfigMaxValueSize
getNodePage hnd h key val nid = do
bs <- get >>= lookupPage hnd (nodeIdToPageId nid)
case viewHeight h of
UZero -> decodeM (leafNodePage h key val) bs >>= \case
LeafNodePage heightSrc n ->
justErrM WrongNodeTypeError $ castNode heightSrc h n
USucc _ -> decodeM (indexNodePage h key val) bs >>= \case
IndexNodePage heightSrc n ->
justErrM WrongNodeTypeError $ castNode heightSrc h n
putNodePage hnd height nid node =
modify' $ M.update (Just . M.insert (nodeIdToPageId nid) pg) hnd
where
pg = case viewHeight height of
UZero -> toStrict . encode $ LeafNodePage height node
USucc _ -> toStrict . encode $ IndexNodePage height node
getOverflow hnd val = do
bs <- get >>= lookupPage hnd 0
decodeM (overflowPage val) bs >>= \case
OverflowPage v -> justErrM WrongOverflowValueError $ castValue v
putOverflow hnd val =
modify' $ M.update (Just . M.insert 0 pg) hnd
where
pg = toStrict . encode $ OverflowPage val
listOverflows _ = gets M.keys
--------------------------------------------------------------------------------
instance (Applicative m, Monad m, MonadIO m, MonadThrow m) =>
ConcurrentMetaStoreM (MemoryStoreT FilePath m)
where
putConcurrentMeta h meta =
modify' $ M.update (Just . M.insert 0 pg) h
where
pg = toStrict . encode $ ConcurrentMetaPage meta
readConcurrentMeta hnd k v = do
Just bs <- gets (M.lookup hnd >=> M.lookup 0)
decodeM (concurrentMetaPage k v) bs >>= \case
ConcurrentMetaPage meta -> return . Just $! coerce meta
--------------------------------------------------------------------------------
-- | Exception thrown when a file is accessed that doesn't exist.
newtype FileNotFoundError hnd = FileNotFoundError hnd deriving (Show, Typeable)
instance (Typeable hnd, Show hnd) => Exception (FileNotFoundError hnd) where
-- | Exception thrown when a page that is accessed doesn't exist.
data PageNotFoundError hnd = PageNotFoundError hnd PageId deriving (Show, Typeable)
instance (Typeable hnd, Show hnd) => Exception (PageNotFoundError hnd) where
-- | Exception thrown when a node cannot be cast to the right type.
--
-- As used in 'getNodePage'.
data WrongNodeTypeError = WrongNodeTypeError deriving (Show, Typeable)
instance Exception WrongNodeTypeError where
-- | Exception thrown when a value from an overflow page cannot be cast.
--
-- As used in 'getOverflow'.
data WrongOverflowValueError = WrongOverflowValueError deriving (Show, Typeable)
instance Exception WrongOverflowValueError where
--------------------------------------------------------------------------------