legion-extra-0.1.2.0: src/Network/Legion/Persistence.hs
{-# LANGUAGE NamedFieldPuns #-}
{- |
This module contains some basis persistence strategies useful for
testing, or getting started.
-}
module Network.Legion.Persistence (
newMemoryPersistence,
diskPersistence,
) where
import Prelude hiding (lookup, readFile, writeFile)
import Control.Concurrent.STM (atomically, newTVar, modifyTVar', readTVar,
TVar)
import Control.Monad.Trans.Class (lift)
import Data.Attoparsec.ByteString (parseOnly, atEnd)
import Data.Attoparsec.ByteString.Char8 (hexadecimal)
import Data.Binary (Binary, encode, decode)
import Data.Bits (testBit)
import Data.Bool (bool)
import Data.ByteString (readFile, writeFile)
import Data.ByteString.Char8 (pack)
import Data.ByteString.Lazy (toStrict, fromStrict)
import Data.Conduit (Source, (=$=), awaitForever, yield)
import Data.Conduit.List (sourceList)
import Data.DoubleWord (Word256(Word256), Word128(Word128))
import Data.Either (rights)
import Data.Map (Map, insert, lookup)
import Data.Word (Word64)
import Network.Legion (Persistence(Persistence, getState, saveState,
list, saveCluster), PartitionPowerState, Peer, ClusterPowerState,
PartitionKey(K))
import System.Directory (removeFile, doesFileExist, getDirectoryContents)
import qualified Data.Map as Map
{- |
A convenient memory-based persistence layer. Good for testing or for
applications (like caches) that don't have durability requirements.
-}
newMemoryPersistence :: IO (Persistence e o s)
newMemoryPersistence = do
cacheT <- atomically (newTVar Map.empty)
return Persistence {
saveCluster = const . const $ return (),
getState = fetchState cacheT,
saveState = saveState_ cacheT,
list = list_ cacheT
}
where
saveState_
:: TVar (Map PartitionKey (PartitionPowerState e o s))
-> PartitionKey
-> Maybe (PartitionPowerState e o s)
-> IO ()
saveState_ cacheT key (Just state) =
(atomically . modifyTVar' cacheT . insert key) state
saveState_ cacheT key Nothing =
(atomically . modifyTVar' cacheT . Map.delete) key
fetchState
:: TVar (Map PartitionKey (PartitionPowerState e o s))
-> PartitionKey
-> IO (Maybe (PartitionPowerState e o s))
fetchState cacheT key = atomically $
lookup key <$> readTVar cacheT
list_
:: TVar (Map PartitionKey (PartitionPowerState e o s))
-> Source IO (PartitionKey, PartitionPowerState e o s)
list_ cacheT =
sourceList . Map.toList =<< lift (atomically (readTVar cacheT))
{- |
A convenient way to persist partition states to disk. This persistence
implementation is very simplistic, and may not be suitable for all uses.
-}
diskPersistence :: (Binary e, Binary s)
=> FilePath
{- ^ The directory under which partition states will be stored. -}
-> (Peer -> ClusterPowerState -> IO ())
{- ^
A user-supplied mechanism to for saving the cluster state, for
recovery purposes.
-}
-> Persistence e o s
diskPersistence directory saveCluster = Persistence {
saveCluster,
getState,
saveState,
list
}
where
getState :: (Binary e, Binary s)
=> PartitionKey
-> IO (Maybe (PartitionPowerState e o s))
getState key =
let path = toPath key in
doesFileExist path >>= bool
(return Nothing)
((Just . decode . fromStrict) <$> readFile path)
saveState :: (Binary e, Binary s)
=> PartitionKey
-> Maybe (PartitionPowerState e o s)
-> IO ()
saveState key (Just state) =
writeFile (toPath key) (toStrict (encode state))
saveState key Nothing =
let path = toPath key in
doesFileExist path >>= bool
(return ())
(removeFile path)
list :: (Binary e, Binary s)
=> Source IO (PartitionKey, PartitionPowerState e o s)
list = do
keys <- lift $ readHexList <$> getDirectoryContents directory
sourceList keys =$= fillData
where
fillData = awaitForever (\key -> do
let path = toPath key
state <- lift ((decode . fromStrict) <$> readFile path)
yield (key, state)
)
readHexList = rights . fmap fromHex -- . filter notSys
-- notSys = not . (`elem` [".", ".."])
{- |
Convert a key to a path
-}
toPath :: PartitionKey -> FilePath
toPath key = directory ++ "/" ++ toHex key
{- | Convert a `PartitionKey` into a hex string. -}
toHex :: PartitionKey -> String
toHex (K (Word256 (Word128 a b) (Word128 c d))) =
concatMap toHex64 [a, b, c, d]
{- |
Convert a `Word64` into a hex string.
I know I'm going to hell for this, but I just can't abide the
@hexstring@ package pulling @aeson@ into our dependency tree.
-}
toHex64 :: Word64 -> String
toHex64 w = fmap (digit . quad) [15, 14..0]
where
quad :: Int -> (Int, Int, Int, Int)
quad n = let base = n * 4 in (base + 3, base + 2, base + 1, base)
digit :: (Int, Int, Int, Int) -> Char
digit (a, b, c, d) =
case (testBit w a, testBit w b, testBit w c, testBit w d) of
(False, False, False, False) -> '0'
(False, False, False, True) -> '1'
(False, False, True, False) -> '2'
(False, False, True, True) -> '3'
(False, True, False, False) -> '4'
(False, True, False, True) -> '5'
(False, True, True, False) -> '6'
(False, True, True, True) -> '7'
(True, False, False, False) -> '8'
(True, False, False, True) -> '9'
(True, False, True, False) -> 'a'
(True, False, True, True) -> 'b'
(True, True, False, False) -> 'c'
(True, True, False, True) -> 'd'
(True, True, True, False) -> 'e'
(True, True, True, True) -> 'f'
{- | Maybe convert a hex string into a partition key -}
fromHex :: String -> Either String PartitionKey
fromHex str
| length str > 64 =
Left "trailing characters while parsing hex PartitionKey"
| otherwise =
K <$> parseOnly parser (pack str)
where
parser = do
w <- hexadecimal
atEnd >>= bool
(fail "not a valid hex string")
(return w)