happstack-state 0.2.1 → 6.1.4
raw patch · 22 files changed
Files
- Setup.hs +1/−1
- happstack-state.cabal +36/−18
- src/Happstack/State.hs +4/−4
- src/Happstack/State/CentralLogServer.hs +251/−0
- src/Happstack/State/Checkpoint.hs +2/−23
- src/Happstack/State/ClockTime.hs +18/−0
- src/Happstack/State/ComponentSystem.hs +15/−5
- src/Happstack/State/ComponentTH.hs +102/−32
- src/Happstack/State/Control.hs +35/−35
- src/Happstack/State/Monad.hs +21/−5
- src/Happstack/State/NetworkChan.hs +64/−0
- src/Happstack/State/Saver.hs +19/−2
- src/Happstack/State/Saver/Impl/File.hs +149/−7
- src/Happstack/State/Saver/Types.hs +1/−0
- src/Happstack/State/Spread.hs +0/−173
- src/Happstack/State/Transaction.hs +32/−35
- src/Happstack/State/TxControl.hs +81/−17
- src/Happstack/State/Types.hs +4/−3
- src/Happstack/State/Util.hs +4/−3
- tests/Happstack/State/Tests/CheckpointProperties.hs +9/−11
- tests/Happstack/State/Tests/Helpers.hs +3/−9
- tests/Happstack/State/Tests/SaverProperties.hs +34/−29
Setup.hs view
@@ -1,3 +1,3 @@ #!/usr/bin/env runhaskell import Distribution.Simple-main = defaultMainWithHooks defaultUserHooks+main = defaultMainWithHooks simpleUserHooks
happstack-state.cabal view
@@ -1,5 +1,5 @@ Name: happstack-state-Version: 0.2.1+Version: 6.1.4 Synopsis: Event-based distributed state. Description: Unplug your machine and restart and have your app recover to exactly where it left off. Happstack-State spares you the need to deal with all the marshalling, consistency, and configuration headache that you would have if you used an external DBMS for this purpose. Its component model makes it easy to compose big applications from smaller reliable parts. Use event subscription to trigger IO actions and support comet-style or irc-bot applications. License: BSD3@@ -8,26 +8,41 @@ Maintainer: Happstack team <happs@googlegroups.com> homepage: http://happstack.com Category: Web, Distributed Computing-Cabal-Version: >= 1.2.3+Cabal-Version: >= 1.6 Build-Type: Simple +source-repository head+ type: darcs+ subdir: happstack-state+ location: http://patch-tag.com/r/mae/happstack+ flag base4 Flag tests Description: Build the testsuite, and include the tests in the library Default: False +flag replication+ Description: Enable state replication through a central serialization service.+ Default: False+ Library Exposed-modules: Happstack.State -- Happstack.State.Logger Happstack.State.Saver+ Happstack.State.ClockTime Happstack.State.Control Happstack.State.Transaction Happstack.State.ComponentTH Happstack.State.ComponentSystem+ Happstack.State.Saver.Impl.File+ Happstack.State.Saver.Impl.Memory+ Happstack.State.Saver.Impl.Queue+ Happstack.State.Saver.Types if flag(tests)+ Build-Depends: QuickCheck >= 2 && <3, HUnit, network Exposed-modules: Happstack.State.Tests Happstack.State.Tests.Helpers@@ -35,13 +50,7 @@ Happstack.State.Tests.SaverProperties Other-modules: Happstack.State.Checkpoint- Happstack.State.Spread Happstack.State.Monad- Happstack.State.Saver.Impl.File- Happstack.State.Saver.Impl.Memory- Happstack.State.Saver.Impl.Queue- Happstack.State.Saver.Types--- Happstack.State.OperationModes Happstack.State.Types Happstack.State.Util Happstack.State.TxControl@@ -53,21 +62,27 @@ extensible-exceptions, filepath, hslogger >= 1.0.2,- hspread >= 0.3,- happstack-util >= 0.2.1 && < 0.3,- happstack-data >= 0.2.1 && < 0.3,- mtl,+ happstack-util >= 6.0 && < 6.1,+ happstack-data >= 6.0.1 && < 6.1,+ mtl >= 1.1 && < 2.1, old-time, random,- stm,+ stm >= 2.1.2.2, template-haskell + if flag(replication)+ Other-modules: Happstack.State.CentralLogServer+ Happstack.State.NetworkChan+ Build-Depends: hS3 >= 0.5.1 && < 0.6,+ bencode >= 0.5,+ process,+ SHA >= 1.4.0 && < 2+ cpp-options: -DREPLICATION+ if flag(base4) Build-Depends: base >= 4 && < 5, syb else Build-Depends: base < 4- if flag(tests)- Build-Depends: QuickCheck < 2, HUnit hs-source-dirs: src if flag(tests)@@ -82,18 +97,21 @@ OverlappingInstances, UndecidableInstances, CPP, TypeFamilies, FunctionalDependencies, TypeOperators, GADTs, EmptyDataDecls, ExistentialQuantification, RankNTypes,- ScopedTypeVariables, PatternSignatures, PatternGuards,+ ScopedTypeVariables, PatternGuards, MagicHash, TypeSynonymInstances- ghc-options: -Wall+ if impl(ghc >= 6.12)+ ghc-options: -Wall -fno-warn-unused-do-bind -fno-warn-orphans+ else+ ghc-options: -Wall -fno-warn-orphans GHC-Prof-Options: -auto-all Executable happstack-state-tests Main-Is: Test.hs GHC-Options: -threaded- Build-depends: HUnit hs-source-dirs: tests, src if flag(tests) Buildable: True+ Build-depends: HUnit else Buildable: False
src/Happstack/State.hs view
@@ -3,7 +3,7 @@ Ev, AnyEv, TxControl, query, update, Update, Query, -- * Types- TxId, EpochMilli, TxConfig(..),nullTxConfig,Saver(..),+ TxId, EpochMilli, Saver(..), -- * Misc utilities module Happstack.State.Monad, getEventId, getTime, getEventClockTime, getEventStream, EventItem(..),@@ -13,11 +13,12 @@ module Happstack.Data.SerializeTH, module Happstack.State.Control,- module Happstack.State.TxControl, module Happstack.State.ComponentTH, module Happstack.State.ComponentSystem,- closeTxControl,+ --closeTxControl,+ runTxSystem, createCheckpoint,+ shutdownSystem, -- * Unsafe things unsafeIOToEv ) where@@ -28,7 +29,6 @@ import Happstack.Data.Serialize import Happstack.Data.SerializeTH import Happstack.State.Transaction-import Happstack.State.Checkpoint import Happstack.State.ComponentSystem import Happstack.State.Types import Happstack.State.Util
+ src/Happstack/State/CentralLogServer.hs view
@@ -0,0 +1,251 @@+module Happstack.State.CentralLogServer+ ( ApplicationName+ , connectToCluster+ , changeEventMapping+ , createCheckpoint+ ) where++import Data.Binary+import qualified Data.ByteString.Lazy.Char8 as L+import Control.Concurrent+import System.Process+import System.Exit+import System.IO+import Control.Applicative ((<$>), (<*>))+import Data.Int+import Data.List+import Control.Exception+import qualified Data.Map as Map+import Control.Monad+import System.Random (mkStdGen)+import Network.AWS.AWSConnection+import Network.AWS.S3Bucket hiding (key)+import Network.AWS.S3Object++import Happstack.Data.Serialize+import Happstack.State.Transaction+import Happstack.State.Types+import Happstack.State.NetworkChan+import Data.BEncode+import Data.BEncode.Parser++import Data.Digest.Pure.SHA++import System.Log.Logger++logLS :: Priority -> String -> IO ()+logLS = logM "Happstack.State.CentralLogServer"++type ApplicationName = String++data Entry = Entry EntryId EpochMilli EntryData deriving (Show)+type EntryId = Int64+type EntryData = L.ByteString+type URL = String++instance Binary Entry where+ get = Entry <$> get <*> get <*> get+ put (Entry eid epoch edata)+ = put eid >> put epoch >> put edata++data ForeignEvent = ForeignEvent Int L.ByteString+instance Binary ForeignEvent where+ get = ForeignEvent <$> get <*> get+ put (ForeignEvent hash event)+ = put hash >> put event++data CheckpointMsg = NoCheckpoint | Checkpoint EntryId URL deriving (Show)+instance BEncodeable CheckpointMsg where+ bencode NoCheckpoint = BString (L.pack "NoCheckpoint")+ bencode (Checkpoint eid url) = BDict (Map.singleton "Checkpoint" $ BDict $ Map.fromList [ ("entry_id", BInt $ fromIntegral eid)+ , ("url", BString (encode url))])+ bdecode = do str <- bstring token+ guard (str == "NoCheckpoint")+ return NoCheckpoint+ <|>+ do setInput =<< dict "Checkpoint"+ eid <- bint (dict "entry_id")+ url <- bbytestring (dict "url")+ return (Checkpoint (fromIntegral eid) $ decode url)++instance BEncodeable Entry where+ bencode entry = BString (encode entry)+ bdecode = fmap decode (bbytestring token)++data Response = Response Entry Bool+instance BEncodeable Response where+ bencode (Response entry isOwner)+ = BDict (Map.fromList [("entry",bencode entry)+ ,("owner",BString $ L.pack $ show isOwner)])+ bdecode = do owner <- bstring (dict "owner")+ setInput =<< dict "entry"+ entry <- bdecode+ case owner of+ "True" -> return $ Response entry True+ _ -> return $ Response entry False++instance BEncodeable L.ByteString where+ bencode bs = BString bs+ bdecode = bbytestring token++data UserMsg = NewEvent L.ByteString | NewCheckpoint EntryId URL deriving (Show)+instance BEncodeable UserMsg where+ bencode (NewEvent event)+ = BDict (Map.singleton "NewEvent" (BString event))+ bencode (NewCheckpoint eid url)+ = BDict $ Map.singleton "NewCheckpoint" $ BDict $ Map.fromList [ ("entry_id", BInt $ fromIntegral eid)+ , ("url", BString $ L.pack url) ]+ bdecode = do setInput =<< dict "NewEvent"+ fmap NewEvent (bbytestring token)+ <|>+ do setInput =<< dict "NewCheckpoint"+ eid <- bint (dict "entry_id")+ url <- bbytestring (dict "url")+ return (NewCheckpoint (fromIntegral eid) (L.unpack url))++data Cluster+ = Cluster { clusterChan :: NetworkChan+ , clusterS3 :: AWSConnection+ , clusterBucket :: String+ , clusterLastId :: MVar EntryId }++-- FIXME: The logging service should have a domain name.+logServerAddress :: URL+logServerAddress = "174.129.13.114"++-- Connect to the central log server via SSH.+-- The appropriate identities have to be added to the autentication agent. See ssh-add(1).+connectToCluster :: ApplicationName -> EventMap -> IO Cluster+connectToCluster appName localEventMap+ = do logLS NOTICE "Retrieving S3 credentials from environment."+ Just conn <- amazonS3ConnectionFromEnv+ bucket <- initiateBucket conn -- FIXME: Run this function in 'createCheckpoint' instead?+ logLS NOTICE "Running LogRelay through ssh."+ (inh, outh) <- sshConnect ("app_" ++ appName) logServerAddress "LogRelay"+ logLS NOTICE "Creating network channel."+ chan <- newNetworkChan inh outh+ checkpoint <- peek chan :: IO CheckpointMsg+ lastId <- newEmptyMVar+ case checkpoint of+ NoCheckpoint -> do putMVar lastId 0+ logLS NOTICE "No checkpoint available."+ Checkpoint eid url -> do putMVar lastId eid+ let (checkpoint_bucket,key) = read url+ logLS NOTICE $ "Checkpoint available from this location: " ++ show (checkpoint_bucket,key)+ ret <- getObject conn (S3Object { obj_bucket = checkpoint_bucket+ , obj_name = key+ , content_type = ""+ , obj_headers = []+ , obj_data = L.empty })+ case ret of+ Left err -> error (show err)+ Right obj -> do (stateMap, _rest) <- evaluate $ deserialize (obj_data obj)+ forM_ (Map.toList stateMap) $ \(stateTy, state) ->+ setNewState' localEventMap stateTy state+ return Cluster { clusterChan = chan+ , clusterS3 = conn+ , clusterBucket = bucket+ , clusterLastId = lastId }++-- Update a snapshot of the component states to S3 and notify the log-server about it.+createCheckpoint :: MVar TxControl -> Cluster -> IO ()+createCheckpoint ctlVar cluster+ = withMVar ctlVar $ \ctl ->+ do -- This ID number is used as the cutoff for the checkpoint. After this checkpoint has been+ -- saved, events younger than this ID will not be sent when restoring the state.+ lastId <- readMVar (clusterLastId cluster)+ allStates <- mapM getState (ctlAllComponents ctl)+ let stateMap = Map.fromList (zip (ctlAllComponents ctl) allStates)+ checkpoint = serialize stateMap+ let bucket = clusterBucket cluster+ key = "checkpoint_" ++ show (sha1 checkpoint)+ sendObject (clusterS3 cluster) (S3Object { obj_bucket = bucket+ , obj_name = key+ , content_type = "application/octet-stream"+ , obj_headers = []+ , obj_data = checkpoint })+ let url = show (bucket, key)+ poke (clusterChan cluster) (NewCheckpoint lastId url)++{- Find a bucket to place new checkpoints in. If no suitable bucket exists, create one.+ Race conditions aren't important since we're just trying to keep the happs buckets to a + minimum (preferable just one).+-}+initiateBucket :: AWSConnection -> IO String+initiateBucket conn+ = do response <- listBuckets conn+ case response of+ Left err -> error (show err)+ Right buckets -> case [ bucket_name bucket | bucket <- buckets, "happs_checkpoints" `isPrefixOf` bucket_name bucket ] of+ (bucket:_) -> return bucket+ [] -> do ret <- createBucketWithPrefix conn "happs_checkpoints"+ case ret of+ Left err -> error (show err)+ Right bucket -> return bucket++{-+ The new event mapping pipes Update events through the logging server. Query events+ do not need to be sequenced and are therefore executed normally.+ Incoming Update events may have been sent from another client and hence will not+ have a local listener attached. Those alien events are still executed (to update the+ state) but their computed result is discarded. For events that originated locally,+ the computed response is sent to the appropriate listener.+-}+changeEventMapping :: EventMap -> Cluster -> IO EventMap+changeEventMapping localEventMap cluster+ = do logLS NOTICE "Create new event mapper"+ responseIndex <- newMVar Map.empty+ let chan = clusterChan cluster+ let insertEID uniq = do mv <- newEmptyMVar+ modifyMVar_ responseIndex $ \idx -> return $ Map.insert uniq mv idx+ return $ takeMVar mv+ returnResponse uniq object = modifyMVar_ responseIndex $ \idx ->+ case Map.lookup uniq idx of+ Nothing -> return idx -- This shouldn't happen.+ Just mv -> do putMVar mv object -- Notify the caller about the new response.+ return $ Map.delete uniq idx+ listener = forever $+ do Response (Entry eid time foreign) owner <- peek chan+ let ForeignEvent uniq object = decode foreign+ logLS NOTICE $ "Received entry with id: " ++ show uniq ++ " " ++ show owner+ let txContext = TxContext { txId = eid+ , txRand = 0+ , txTime = time+ , txStdGen = mkStdGen 0 }+ response <- runColdEventFunc txContext (fst $ deserialize object) localEventMap+ swapMVar (clusterLastId cluster) eid+ -- If we sent this event, pipe the result to the listener.+ when owner $ returnResponse uniq response+ forkIO listener+ uniqVar <- newMVar 0+ let newEventMap = flip Map.map localEventMap $ \handler ->+ case handler of+ UpdateHandler runCold _ parse+ -> let runHot ev = do uniq <- modifyMVar uniqVar (\a -> return (a+1, a))+ let obj = serialize $ mkObject ev+ foreign = ForeignEvent uniq obj+ logLS NOTICE $ "New locally unique id: " ++ show uniq+ wait <- insertEID uniq+ poke chan (NewEvent (encode foreign))+ response <- wait+ logLS NOTICE $ "Received response for: " ++ show uniq+ return $ parseObject response+ in UpdateHandler runCold runHot parse+ QueryHandler{} -> handler+ return newEventMap++++type UserName = String+type ServerAddress = String+type Command = String++-- Run a command through an SSH tunnel.+sshConnect :: UserName -> ServerAddress -> Command -> IO (Handle, Handle)+sshConnect userName serverAddress command+ = do (inh, outh, errh, _pid) <- runInteractiveProcess "ssh" ["-o", "BatchMode=yes", userName ++ "@" ++ serverAddress, command] Nothing Nothing+ tid <- myThreadId+ forkIO $ do errMsg <- hGetLine errh+ hPutStrLn stderr $ "SSH tunneling failed: " ++ errMsg+ throwTo tid (ExitFailure 255)+ return (inh, outh)
src/Happstack/State/Checkpoint.hs view
@@ -1,8 +1,6 @@ {-# LANGUAGE CPP, TemplateHaskell, DeriveDataTypeable #-} module Happstack.State.Checkpoint- ( createTxControl- , closeTxControl- , restoreState+ ( restoreState , createCheckpoint ) where @@ -11,7 +9,6 @@ import Happstack.Data.Serialize import Happstack.Data.SerializeTH import Happstack.State.Transaction-import Happstack.State.ComponentSystem import Data.Typeable import Data.Maybe@@ -20,8 +17,6 @@ import Control.Exception as E import qualified Data.ByteString.Lazy.Char8 as L import qualified Data.Map as M-import System.IO-import qualified Data.Map as M import System.Log.Logger @@ -44,24 +39,8 @@ instance Version State $(deriveSerialize ''State) --- | Starts a new TxControl-createTxControl :: (Methods state, Component state) =>- Saver -> Proxy state -> IO (MVar TxControl)-createTxControl saver prox- = do -- The state hasn't been loaded yet. Ignore events.- eventSaverVar <- newMVar =<< createWriter NullSaver "events" 0- newMVar $ TxControl- { ctlSaver = saver- , ctlEventSaver = eventSaverVar- , ctlAllComponents = allStateTypes prox- , ctlComponentVersions = componentVersions prox- , ctlChildren = [] }+ --- | Saves and ends the TxControl-closeTxControl :: MVar TxControl -> IO ()-closeTxControl ctlVar- = do ctl <- takeMVar ctlVar- writerClose =<< takeMVar (ctlEventSaver ctl) -- FIXME: It may be nice to print out what components were saved on disk -- compared to the components actually used in the application.
+ src/Happstack/State/ClockTime.hs view
@@ -0,0 +1,18 @@+{-# LANGUAGE TemplateHaskell, TypeFamilies, DeriveDataTypeable,+ FlexibleInstances, MultiParamTypeClasses, FlexibleContexts,+ UndecidableInstances, StandaloneDeriving, TypeSynonymInstances+ #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}+-- |instances of Typeable, Data, Serialize, Version, and Default for ClockTime+module Happstack.State.ClockTime (ClockTime(..)) where++import Data.Generics (Data, Typeable)+import Happstack.Data (deriveNewData)+import Happstack.State (Version, deriveSerialize)+import System.Time (ClockTime(..))++deriving instance Typeable ClockTime+deriving instance Data ClockTime+instance Version ClockTime+$(deriveSerialize ''ClockTime)+$(deriveNewData [''ClockTime])
src/Happstack/State/ComponentSystem.hs view
@@ -3,7 +3,6 @@ module Happstack.State.ComponentSystem where import Happstack.Data.Serialize-import Happstack.Data.Proxy import Happstack.State.Types import Data.Typeable@@ -34,9 +33,11 @@ class (Serialize ev, Serialize res) => QueryEvent ev res | ev -> res ----------------------------------------------------------------- Methods contain the query and update handlers for a component+-- Methods contain the query and update handl ers for a component -------------------------------------------------------------- +-- | Method is the actual type that all Updates and Querys eventually+-- get lifted into via 'mkMethods'. data Method st where Update :: (UpdateEvent ev res) => (ev -> Update st res) -> Method st Query :: (QueryEvent ev res) => (ev -> Query st res) -> Method st@@ -49,10 +50,10 @@ methodType m = case m of Update fn -> let ev :: (ev -> Update st res) -> ev ev _ = undefined- in show (typeOf (ev fn))+ in showQualifiedTypeRep (typeOf (ev fn)) Query fn -> let ev :: (ev -> Query st res) -> ev ev _ = undefined- in show (typeOf (ev fn))+ in showQualifiedTypeRep (typeOf (ev fn)) -- | Class for enumerating the set of defined methods by the type of the state. -- Instances should not be defined directly, but using 'mkMethods'@@ -92,11 +93,19 @@ -------------------------------------------------------------- -- Class for walking the component tree. --------------------------------------------------------------+-- | SubHandlers is used to build up the set of components corresponding to+-- the instance type. class SubHandlers a where subHandlers :: a -> Collect () +-- | In correspondence with its role as [] in the type level list,+-- the instance for End does not add any components to the set. instance SubHandlers End where subHandlers ~End = return ()++-- | This is the instance that completes the definition of :+: and End as being+-- the constructors of a type level list in SubHandlers. Note that since b+-- needs to be an instance of SubHandlers, the list needs to be terminated with End. instance (Methods a, Component a, SubHandlers b) => SubHandlers (a :+: b) where subHandlers ~(a :+: b) = do collectHandlers' (proxy a) subHandlers b@@ -121,7 +130,7 @@ collectHandlers' :: (Methods a, Component a) => Proxy a -> Collect () collectHandlers' prox- = let key = show (typeOf (unProxy prox))+ = let key = showQualifiedTypeRep (typeOf (unProxy prox)) item = MethodMap $ Map.fromList [ (methodType m, m) | m <- methods prox ] versions = collectVersions prox in do addItem key item versions (onLoad prox)@@ -129,5 +138,6 @@ where sub :: Component a => Proxy a -> Dependencies a sub _ = undefined +-- | An error is thrown when this is evaluated. dup :: String -> b dup key = error $ "Duplicate component: " ++ key
src/Happstack/State/ComponentTH.hs view
@@ -19,7 +19,15 @@ import Data.Generics.Basics -nubCxt :: [TypeQ] -> Q [Type]+#if MIN_VERSION_template_haskell(2,4,0)+type CtxElemQ = PredQ+type CtxElem = Pred+#else+type CtxElemQ = TypeQ+type CtxElem = Type+#endif++nubCxt :: [CtxElemQ] -> Q [CtxElem] nubCxt tsQ = do ts <- cxt tsQ return $ nub ts@@ -55,23 +63,38 @@ ds5 <- genSerializeInstances methodInfos return (ds1 ++ [ds2] ++ ds4 ++ ds5 ) -mkKeyConstraints :: [Name] -> [TypeQ]+mkKeyConstraints :: [Name] -> [CtxElemQ] mkKeyConstraints keys- = [ appT (conT ''Typeable) (varT key) | key <- keys ] ++- [ appT (conT ''Serialize) (varT key) | key <- keys ]+ = [ mkCtxt ''Typeable (varT key) | key <- keys ] +++ [ mkCtxt ''Serialize (varT key) | key <- keys ] -mkMethodConstraints :: [Name] -> MethodInfo -> [TypeQ]+mkMethodConstraints :: [Name] -> MethodInfo -> [CtxElemQ] mkMethodConstraints keys method = map return (substMethodContext method keys) -substMethodContext :: MethodInfo -> [Name] -> [Type]+substMethodContext :: MethodInfo -> [Name] -> [CtxElem] substMethodContext method keys = let relation = zip (methodKeys method) keys- worker (VarT old) | Just new <- lookup old relation++ workerT (VarT old) | Just new <- lookup old relation = VarT new- worker (AppT l r) = AppT (worker l) (worker r)- worker (ForallT c names t) = ForallT c names (worker t)- worker t = t+ workerT (AppT l r) = AppT (workerT l) (workerT r)+ workerT (ForallT c names t) = ForallT c names (workerT t)+ workerT t = t++#if MIN_VERSION_template_haskell(2,4,0)+ workerP (ClassP old typs)+ = let new = case lookup old relation of+ Nothing -> old+ Just nm -> nm+ in ClassP new $ map workerT typs+ workerP p = p++ worker = workerP+#else+ worker = workerT+#endif+ in map worker (methodContext method) mkType :: Name -> [Name] -> TypeQ@@ -120,14 +143,21 @@ genEventInstance method = do let keys = methodKeys method eventType = foldl appT (conT (upperName (methodName method))) (map varT keys)- instanceD (nubCxt $ [appT (conT ''Serialize) eventType- ,appT (conT ''Serialize) (return (methodResult method))]+ instanceD (nubCxt $ [mkCtxt ''Serialize eventType+ ,mkCtxt ''Serialize (return (methodResult method))+ ] ++ mkKeyConstraints keys ++ mkMethodConstraints keys method ) (appT (appT (conT (methodClass method)) eventType) (return (methodResult method))) [] +mkCtxt :: Name -> TypeQ -> CtxElemQ+#if MIN_VERSION_template_haskell(2,4,0)+mkCtxt name typ = classP name [typ]+#else+mkCtxt name typ = appT (conT name) typ+#endif genMethodStructs :: [Name] -> [MethodInfo] -> Q [Dec] genMethodStructs derv meths@@ -137,8 +167,16 @@ genMethodStruct :: [Name] -> MethodInfo -> Q [Dec] genMethodStruct derv method = do let c = NormalC (upperName (methodName method)) (zip (repeat NotStrict ) (methodArgs method))- return [ DataD [] (upperName (methodName method)) (methodKeys method) [c] (derv) ]+ return [ DataD [] (upperName (methodName method)) (map mkTyVarBndr $ methodKeys method) [c] (derv) ] + where++#if MIN_VERSION_template_haskell(2,4,0)+ mkTyVarBndr = PlainTV+#else+ mkTyVarBndr = id+#endif+ upperName :: Name -> Name upperName = mkName . upperFirst . nameBase @@ -148,7 +186,7 @@ data MethodInfo = Method { methodName :: Name , methodKeys :: [Name]- , methodContext:: [Type]+ , methodContext:: [CtxElem] , methodArgs :: [Type] , methodClass :: Name , methodEv :: Name@@ -179,12 +217,12 @@ _ -> error $ "Method is not a function: " ++ nameBase method ++ " is a " ++ showInfo methodInfo showInfo :: Info -> String-showInfo (ClassI _) = "class"-showInfo (TyConI _) = "type constructor"-showInfo (PrimTyConI _ _ _) = "primitive type constructor"-showInfo (DataConI _ _ _ _) = "data constructor"-showInfo (VarI _ _ _ _) = "variable"-showInfo (TyVarI _ _) = "type variable"+showInfo ClassI{} = "class"+showInfo TyConI{} = "type constructor"+showInfo PrimTyConI{} = "primitive type constructor"+showInfo DataConI{} = "data constructor"+showInfo VarI{} = "variable"+showInfo TyVarI{} = "type variable" showInfo x = pprint x @@ -231,24 +269,56 @@ getStateKeys (ConT _st) = [] getStateKeys v = error $ "Bad state type: " ++ pprint v ++ " (expected a constant, an application or a type variable)" -isMonadState :: [Type] -> Type -> Maybe Type-isMonadState contxt name = listToMaybe [ state | AppT (AppT (ConT m) state) mName <- contxt, - mName == name, m == ''MonadState ]+-- |Ignoring EqualP predicates, returns all names associated with a+-- context predicate+getPredKeys :: CtxElem -> [Name]+#if MIN_VERSION_template_haskell(2,4,0)+getPredKeys (ClassP nm typs) = nm : concatMap getStateKeys typs+getPredKeys _ = []+#else+getPredKeys = getStateKeys+#endif -isMonadReader :: [Type] -> Type -> Maybe Type-isMonadReader contxt name = listToMaybe [ state | AppT (AppT (ConT m) state) mName <- contxt, - mName == name, m == ''MonadReader ] -isRelevant :: [Name] -> Type -> Bool-isRelevant keys t = isAcceptableContext t && any (`elem` keys) (getStateKeys t)+isMonadState :: [CtxElem] -> Type -> Maybe Type+isMonadState = isMonadX ''MonadState -isAcceptableContext :: Type -> Bool+isMonadReader :: [CtxElem] -> Type -> Maybe Type+isMonadReader = isMonadX ''MonadReader++isMonadX :: Name -> [CtxElem] -> Type -> Maybe Type+#if MIN_VERSION_template_haskell(2,4,0)+isMonadX monadType contxt name = listToMaybe [ state | ClassP m [state,mName] <- contxt, + mName == name, m == monadType ]+#else+isMonadX monadType contxt name = listToMaybe [ state | AppT (AppT (ConT m) state) mName <- contxt,+ mName == name, m == monadType ]+#endif++++isRelevant :: [Name] -> CtxElem -> Bool+isRelevant keys p = isAcceptableContext p && any (`elem` keys) (getPredKeys p)++isAcceptableContext :: CtxElem -> Bool+#if MIN_VERSION_template_haskell(2,4,0)+isAcceptableContext (ClassP con _) = con `notElem` [''MonadState, ''MonadReader] +#else isAcceptableContext (AppT r r') = isAcceptableContext r && isAcceptableContext r' isAcceptableContext (ConT con) = con `notElem` [''MonadState, ''MonadReader]+#endif isAcceptableContext _ = True requireSimpleCon :: Name -> Info -> [Name]-requireSimpleCon _ (TyConI (DataD _ _ names _ _derv)) = names-requireSimpleCon _ (TyConI (NewtypeD _ _ names _ _derv)) = names-requireSimpleCon _ (TyConI (TySynD _ names _)) = names+requireSimpleCon _ (TyConI (DataD _ _ names _ _derv)) = map conv names+requireSimpleCon _ (TyConI (NewtypeD _ _ names _ _derv)) = map conv names+requireSimpleCon _ (TyConI (TySynD _ names _)) = map conv names requireSimpleCon name _ = error $ "Cannot create component from '"++pprint name++"'. Expected a data structure."++#if MIN_VERSION_template_haskell(2,4,0)+conv :: TyVarBndr -> Name+conv (PlainTV nm) = nm+conv (KindedTV nm _) = nm+#else+conv = id+#endif
src/Happstack/State/Control.hs view
@@ -2,20 +2,19 @@ module Happstack.State.Control ( startSystemState- , startSystemStateMultimaster+ , startSystemStateAmazon+ , processLoggingFlags , stdSaver , waitForTermination ) where +import Control.Applicative ((<$>)) import System.Log.Logger import qualified System.Log.Handler as SLH import System.Log.Handler.Simple import System.Log.Handler.Syslog -import Control.Monad-import Data.List import Data.Char-import Data.Maybe import System.Environment import System.IO import System.Exit@@ -23,6 +22,8 @@ import Control.Monad.Trans import Control.Concurrent +import Numeric (showHex)+ #ifdef UNIX import System.Posix.Signals hiding (Handler) import System.Posix.IO ( stdInput )@@ -31,30 +32,43 @@ import Happstack.State.Transaction import Happstack.State.Saver+#ifdef REPLICATION+import Happstack.State.CentralLogServer (ApplicationName)+#endif import Happstack.State.TxControl import Happstack.State.ComponentSystem import Happstack.Data.Proxy hiding (proxy) --- | Starts the MACID system without multimaster support. Uses the default Saver.+-- | Starts the MACID system without replication support. Uses the default behavior+-- of saving the state into the _local directory. startSystemState :: (Methods a, Component a) => Proxy a -> IO (MVar TxControl) startSystemState proxy- = do _txConfig <- parseArgs- saver <- stdSaver+ = do saver <- stdSaver runTxSystem saver proxy --- | Starts the MACID system with multimaster support. Uses the default Saver.-startSystemStateMultimaster :: (Methods a, Component a) =>- Proxy a -> IO (MVar TxControl)-startSystemStateMultimaster proxy- = do _txConfig <- parseArgs- saver <- stdSaver- runTxSystem' True saver proxy+#ifdef REPLICATION+startSystemStateAmazon :: (Methods a, Component a) => ApplicationName -> Proxy a -> IO (MVar TxControl)+startSystemStateAmazon appName proxy+ = runTxSystemAmazon appName proxy+#else+type ApplicationName = String+startSystemStateAmazon :: (Methods a, Component a) => ApplicationName -> Proxy a -> IO (MVar TxControl)+startSystemStateAmazon appName proxy+ = runTxSystemAmazon appName proxy+#endif --- | Returns the default Saver.+-- | Returns the default Saver. It will save the application state into+-- the _local directory. stdSaver :: IO Saver-stdSaver = do pn <- getProgName+stdSaver = do pn <- escape <$> getProgName return $ Queue (FileSaver ("_local/" ++pn++"_state"))+ where+ escape :: String -> String+ escape = concatMap escapeChar+ escapeChar :: Char -> String+ escapeChar c | isAlphaNum c = [c]+ | otherwise = showString "_" $ showHex (ord c) "" -- | Wait for a signal. -- On unix, a signal is sigINT or sigTERM. On windows, the signal@@ -77,12 +91,6 @@ loop 'c' #endif -mkTxConfig :: [Flag] -> TxConfig-mkTxConfig = foldr worker nullTxConfig- where worker (Cluster serv) c = c{txcOperationMode = ClusterMode (fromMaybe "" serv)}- worker (ClusterPort port) c = c{txcClusterPort = port}- worker _ c = c- data NullLogger instance SLH.LogHandler NullLogger where setLevel = error "Don't do this! This logger should not be used!"@@ -104,7 +112,6 @@ worker (LogLevel pri) = do updateGlobalLogger "Happstack" (setLevel pri) rlogger <- getLogger "Happstack" logM "" WARNING ("Set logging priority to " ++ show (getLevel rlogger))- worker _ = return () -- order should not matter, though it does now. -- we should ALSO allow multiple loggers at the same time! --log-target=stdout --log-target=syslog@@ -113,17 +120,11 @@ "Log level: DEBUG, INFO, NOTICE, WARNING, ERROR, CRITICAL, ALERT, EMERGENCY. Default: WARNING" ,Option "" ["log-target"] (ReqArg (LogTarget . readTarget) "target") "Log target: stdout, syslog, or a FilePath such as /home/foo/bar.log . Default: stdout"- -- FIXME: use a better flag name- ,Option "" ["cluster-mode"] (OptArg (Cluster) "servers")- "Start in multimaster mode. Will join cluster if optional arg is given."- ,Option "" ["cluster-port"] (ReqArg (ClusterPort . read) "port")- "Multimaster server will listen on this port." ] data Target = File FilePath | StdOut | SysLog deriving (Read,Show,Eq,Ord) -data Flag = LogLevel Priority | LogTarget Target- | Cluster (Maybe String) | ClusterPort Int deriving Show+data Flag = LogLevel Priority | LogTarget Target deriving Show readTarget :: String -> Target readTarget arg = case map toLower arg of@@ -137,8 +138,8 @@ worker (ReqArg _ f) = ReqArg (const ()) f worker (OptArg _ f) = OptArg (const ()) f -parseArgs :: IO TxConfig-parseArgs = do+processLoggingFlags :: IO a -> IO a+processLoggingFlags action = do args <- liftIO getArgs pn <- liftIO getProgName let err n ls = -- XXX these next lines should be written to stderr!@@ -147,8 +148,7 @@ putStrLn ("Usage "++usageInfo pn castOptions) exitWith (ExitFailure 1) case getOpt' Permute options args of- (flags,_fs,_args',[]) -> do setLoggingSettings flags- -- FIXME: replace system args with fs++args'- return (mkTxConfig flags)+ (flags,fs,args',[]) -> do setLoggingSettings flags+ withArgs (fs ++ args') action (_,_,_,es) -> err "errors" es
src/Happstack/State/Monad.hs view
@@ -9,7 +9,6 @@ import Control.Monad.State import Control.Monad.Reader-import Control.Monad instance (Monad m) => Monad (Ev m) where return x = Ev $ \_ -> return x@@ -36,14 +35,16 @@ setUpdateType :: Proxy t -> Update t () setUpdateType _ = return () -proxyUpdate :: Ev (StateT t STM) b -> Proxy t -> Ev (StateT t STM) b+-- | Forces the type of the proxy and update to match+proxyUpdate :: Update t b -> Proxy t -> Update t b proxyUpdate f prox = setUpdateType prox >> f -- | Use a proxy to force the type of a query action. setQueryType :: Proxy t -> Query t () setQueryType _ = return () -proxyQuery :: Ev (ReaderT t STM) b -> Proxy t -> Ev (ReaderT t STM) b+-- | Forces the type of proxy and query to match+proxyQuery :: Query t b -> Proxy t -> Query t b proxyQuery f prox = setQueryType prox >> f -- | Currying version of 'setUpdateType'.@@ -71,11 +72,26 @@ liftSTM :: STM a -> AnyEv a liftSTM = unsafeSTMToEv +{-+In stm >= 2.2.0.1 catchSTM always has the type:++catchSTM :: Exception e => STM a -> (e -> STM a) -> STM a++In earlier versions of stm the type of catchSTM depends on what is exported by GHC.Conc.catchSTM from base.+-} class CatchEv m where-#if __GLASGOW_HASKELL__ < 610- catchEv :: Ev m a -> (Exception -> a) -> Ev m a+#if (MIN_VERSION_stm(2,2,0))+ catchEv :: (Exception e) => Ev m a -> (e -> a) -> Ev m a #else+#if (MIN_VERSION_base(4,3,0))+ catchEv :: (Exception e) => Ev m a -> (e -> a) -> Ev m a+#else+#if (MIN_VERSION_base(4,0,0)) catchEv :: Ev m a -> (SomeException -> a) -> Ev m a+#else+ catchEv :: Ev m a -> (Exception -> a) -> Ev m a+#endif+#endif #endif instance CatchEv (ReaderT st STM) where catchEv (Ev cmd) fun = Ev $ \s -> ReaderT $ \r -> runReaderT (cmd s) r `catchSTM` (\a -> return (fun a))
+ src/Happstack/State/NetworkChan.hs view
@@ -0,0 +1,64 @@+module Happstack.State.NetworkChan+ ( BEncodeable(..)+ , NetworkChan+ , newNetworkChan+ , peek+ , poke+ ) where++import Data.BEncode+import Data.BEncode.Parser++import Control.Monad (when, forever, forM_ )+import System.IO+import qualified Data.ByteString.Lazy as L+import Data.Binary+import Control.Concurrent.STM+import Control.Concurrent (forkIO, ThreadId)++class BEncodeable a where+ bencode :: a -> BEncode+ bdecode :: BParser a+++data NetworkChan = NetworkChan { outgoingChan :: TVar [L.ByteString]+ , incomingHandle :: Handle+ , writerThread :: ThreadId }++newNetworkChan :: Handle -> Handle -> IO NetworkChan+newNetworkChan outgoing incoming+ = do cache <- newTVarIO []+ tid <- forkIO $ forever $ do ls <- atomically $ do ls <- readTVar cache+ when (null ls) retry+ writeTVar cache []+ return (reverse ls)+ forM_ ls $ \entry -> do L.hPut outgoing (encode (L.length entry))+ L.hPut outgoing entry+ hFlush outgoing+ return NetworkChan { outgoingChan = cache+ , incomingHandle = incoming+ , writerThread = tid }++peek :: BEncodeable a => NetworkChan -> IO a+peek chan+ = do size <- L.hGet (incomingHandle chan) 8+ when (L.length size < 8) $ error "NetworkChan.peek: Connection closed."+ body <- L.hGet (incomingHandle chan) (decode size)+ case bRead body of+ Nothing -> error "NetworkChan.peek: Malformed bencoded data."+ Just encoded -> case runParser bdecode encoded of+ Left err -> error "NetworkChan.peek: Parsing bencoded data failed."+ Right msg -> return msg++poke :: BEncodeable a => NetworkChan -> a -> IO ()+poke chan msg+ = atomically $+ do ls <- readTVar (outgoingChan chan)+ writeTVar (outgoingChan chan) (bPack (bencode msg):ls)+{-+do L.hPut (inputHandle chan) (encode (L.length body))+ L.hPut (inputHandle chan) body+ hFlush (inputHandle chan)+ where body = bPack (bencode msg)+-}+
src/Happstack/State/Saver.hs view
@@ -1,7 +1,12 @@ module Happstack.State.Saver ( module Happstack.State.Saver.Types+ , PrefixLock , Saver(..)- , createReader, createWriter ) where+ , createReader+ , createWriter+ , obtainLock+ , releaseLock+ ) where import Control.Concurrent import Happstack.State.Saver.Impl.File@@ -12,10 +17,11 @@ data Saver = NullSaver -- ^ A saver that discards all output | FileSaver String -- ^ A saver that operates on files. The parameter is the prefix for the files.- -- Creates the prefix directory. | Queue Saver -- ^ Enable queueing. | Memory (MVar Store) +-- | Dispatches over the Saver type provided to return a 'ReaderStream' for the inferred+-- type. createReader :: Serialize a => Saver -> String -> Int -> IO (ReaderStream a) createReader (FileSaver prefix) key cutoff = fileReader prefix key cutoff createReader (Memory store) key cutoff = memoryReader store key cutoff@@ -26,6 +32,8 @@ , readerGet = fail "NullSaver: readerGet" , readerGetUncut = fail "NullSaver: readerGetUncut" } +-- | Dispatches over the Saver type provided to return a WriterStream for the+-- inferred type. createWriter :: Serialize a => Saver -> String -> Int -> IO (WriterStream a) createWriter (FileSaver prefix) key cutoff = fileWriter prefix key cutoff createWriter (Memory store) key cutoff = memoryWriter store key cutoff@@ -36,4 +44,13 @@ , writerAdd = \_ io -> io , writerAtomicReplace = fail "NullSaver: writerAtomicReplace" , writerCut = fail "NullSaver: writerCut" }++obtainLock :: Saver -> IO (Maybe PrefixLock)+obtainLock (Queue saver) = obtainLock saver+obtainLock (FileSaver prefix) = fmap Just (obtainPrefixLock prefix)+obtainLock _ = return Nothing++releaseLock :: Maybe PrefixLock -> IO ()+releaseLock (Just lock) = releasePrefixLock lock+releaseLock Nothing = return ()
src/Happstack/State/Saver/Impl/File.hs view
@@ -1,6 +1,10 @@ {-# LANGUAGE CPP #-} module Happstack.State.Saver.Impl.File- ( fileReader, fileWriter+ ( PrefixLock+ , fileReader+ , fileWriter+ , obtainPrefixLock+ , releasePrefixLock ) where import Happstack.State.Saver.Types@@ -8,22 +12,43 @@ import Control.Concurrent import Control.Exception.Extensible as E-import qualified Data.ByteString.Lazy.Char8 as L-import qualified Data.ByteString.Char8 as B-import System.Directory ( createDirectoryIfMissing, renameFile, doesFileExist )+import qualified Data.ByteString.Lazy as L+import qualified Data.ByteString as B+import System.Directory ( createDirectoryIfMissing, removeFile, renameFile, doesFileExist ) import System.IO import System.Random ( randomIO ) import System.Log.Logger import Text.Printf import Control.Monad import System.FilePath+#ifdef UNIX+import Data.Maybe (listToMaybe)+import qualified System.IO.Error as SE+import System.Posix.IO (openFd, OpenMode(ReadWrite), defaultFileFlags, exclusive, trunc, fdToHandle)+import System.Posix.Process (getProcessID)+import System.Posix.Signals (nullSignal, signalProcess)+import System.Posix.Types (ProcessID)+#else+import Happstack.Util.OpenExclusively (openExclusively)+#endif +#ifdef UNIX+newtype PrefixLock = PrefixLock FilePath+#else+type PrefixLock = (FilePath, Handle)+#endif+ tryE :: IO a -> IO (Either SomeException a) tryE = E.try catchE :: IO a -> (SomeException -> IO a) -> IO a catchE = E.catch +#ifndef UNIX+catchIO :: IO a -> (IOException -> IO a) -> IO a+catchIO = E.catch+#endif+ logMF :: Priority -> String -> IO () logMF = logM "Happstack.State.Saver.Impl.File" @@ -35,7 +60,7 @@ = do let file = prefix </> formatFilePath cutoff key tryE $ createDirectoryIfMissing True prefix return $ ReaderStream- { readerClose = do return ()+ { readerClose = return () , readerGet = do logMF NOTICE "fileReader: readerGet" allFiles <- getAllFiles prefix key cutoff allData <- mapM B.readFile allFiles@@ -58,8 +83,7 @@ let getFileName = do cutoff <- readMVar cutoffVar return $ prefix </> formatFilePath cutoff key file <- getFileName- logMF NOTICE ("fileWrter: "++key++" @ "++prefix)- tryE $ createDirectoryIfMissing True prefix+ logMF NOTICE ("fileWriter: "++key++" @ "++prefix) hmv <- newMVar =<< openBinaryFile file WriteMode return $ WriterStream { writerClose = withMVar hmv hClose@@ -96,3 +120,121 @@ let p' = path ++ ".atomic-tmp-" ++ show (abs r) L.writeFile p' string renameFile p' path++#ifdef UNIX+obtainPrefixLock :: FilePath -> IO PrefixLock+obtainPrefixLock prefix = do+ checkLock fp >> takeLock fp+ where fp = prefix ++ ".lock"++-- |Read the lock and break it if the process is dead.+checkLock :: FilePath -> IO ()+checkLock fp = readLock fp >>= maybeBreakLock fp++-- |Read the lock and return the process id if possible.+readLock :: FilePath -> IO (Maybe ProcessID)+readLock fp = try (readFile fp) >>=+ return . either (checkReadFileError fp) (fmap (fromInteger . read) . listToMaybe . lines)++-- |Is this a permission error? If so we don't have permission to+-- remove the lock file, abort.+checkReadFileError :: [Char] -> IOError -> Maybe ProcessID+checkReadFileError fp e | SE.isPermissionError e = throw (userError ("Could not read lock file: " ++ show fp))+ | SE.isDoesNotExistError e = Nothing+ | True = throw e++maybeBreakLock :: FilePath -> Maybe ProcessID -> IO ()+maybeBreakLock fp Nothing =+ -- The lock file exists, but there's no PID in it. At this point,+ -- we will break the lock, because the other process either died+ -- or will give up when it failed to read its pid back from this+ -- file.+ breakLock fp+maybeBreakLock fp (Just pid) = do+ -- The lock file exists and there is a PID in it. We can break the+ -- lock if that process has died.+ -- getProcessStatus only works on the children of the calling process.+ -- exists <- try (getProcessStatus False True pid) >>= either checkException (return . isJust)+ exists <- doesProcessExist pid+ case exists of+ True -> throw (lockedBy fp pid)+ False -> breakLock fp++doesProcessExist :: ProcessID -> IO Bool+doesProcessExist pid =+ -- Implementation 1+ -- doesDirectoryExist ("/proc/" ++ show pid)+ -- Implementation 2+ try (signalProcess nullSignal pid) >>= return . either checkException (const True)+ where checkException e | SE.isDoesNotExistError e = False+ | True = throw e++-- |We have determined the locking process is gone, try to remove the+-- lock.+breakLock :: FilePath -> IO ()+breakLock fp = try (removeFile fp) >>= either checkBreakError (const (return ()))++-- |An exception when we tried to break a lock, if it says the lock+-- file has already disappeared we are still good to go.+checkBreakError :: IOError -> IO ()+checkBreakError e | SE.isDoesNotExistError e = return ()+ | True = throw e++-- |Try to create lock by opening the file with the O_EXCL flag and+-- writing our PID into it. Verify by reading the pid back out and+-- matching, maybe some other process slipped in before we were done+-- and broke our lock.+takeLock :: FilePath -> IO PrefixLock+takeLock fp = do+ createDirectoryIfMissing True (takeDirectory fp) + h <- openFd fp ReadWrite (Just 0o600) (defaultFileFlags {exclusive = True, trunc = True}) >>= fdToHandle+ pid <- getProcessID+ hPutStrLn h (show pid) >> hClose h+ -- Read back our own lock and make sure its still ours+ readLock fp >>= maybe (throw (cantLock fp pid))+ (\ pid' -> if pid /= pid'+ then throw (stolenLock fp pid pid')+ else return (PrefixLock fp))++-- |An exception saying the data is locked by another process.+lockedBy :: (Show a) => FilePath -> a -> SomeException+lockedBy fp pid = SomeException (SE.mkIOError SE.alreadyInUseErrorType ("Locked by " ++ show pid) Nothing (Just fp))++-- |An exception saying we don't have permission to create lock.+cantLock :: FilePath -> ProcessID -> SomeException+cantLock fp pid = SomeException (SE.mkIOError SE.alreadyInUseErrorType ("Process " ++ show pid ++ " could not create a lock") Nothing (Just fp))++-- |An exception saying another process broke our lock before we+-- finished creating it.+stolenLock :: FilePath -> ProcessID -> ProcessID -> SomeException+stolenLock fp pid pid' = SomeException (SE.mkIOError SE.alreadyInUseErrorType ("Process " ++ show pid ++ "'s lock was stolen by process " ++ show pid') Nothing (Just fp))++-- |Relinquish the lock by removing it and then verifying the removal.+releasePrefixLock :: PrefixLock -> IO ()+releasePrefixLock (PrefixLock fp) =+ dropLock >>= either checkDrop return+ where+ dropLock = try (removeFile fp)+ checkDrop e | SE.isDoesNotExistError e = return ()+ | True = throw e+#else+obtainPrefixLock :: FilePath -> IO PrefixLock+obtainPrefixLock prefix = do+ createDirectoryIfMissing True prefix+ -- catchIO obtainLock onError+ catchIO obtainLock onError+ where fp = prefix ++ ".lock" + obtainLock = do+ h <- openExclusively fp+ return (fp, h)+ onError e = do+ putStrLn "There may already be an instance of this application running, which could result in a loss of data."+ putStrLn ("Please make sure there is no other application attempting to access '" ++ prefix ++ "'")+ throw e++releasePrefixLock :: PrefixLock -> IO ()+releasePrefixLock (fp, h) = do+ tryE $ hClose h+ tryE $ removeFile fp+ return ()+#endif
src/Happstack/State/Saver/Types.hs view
@@ -14,3 +14,4 @@ , writerAtomicReplace :: a -> IO () , writerCut :: IO Int }+
− src/Happstack/State/Spread.hs
@@ -1,173 +0,0 @@-{-# LANGUAGE TemplateHaskell, DeriveDataTypeable #-}-module Happstack.State.Spread- ( connectToCluster- , changeEventMapping- ) where--import Happstack.State.Transaction-import Happstack.Data.Serialize-import Happstack.Data.SerializeTH--import qualified Data.ByteString.Char8 as B-import qualified Data.ByteString.Lazy.Char8 as L-import qualified Data.Map as Map--import Data.Typeable-import Spread.Client-import qualified Control.Concurrent.Chan.Closeable as Closeable--import System.Log.Logger-import Control.Monad hiding (join)-import Control.Concurrent-import System.Random--logSP :: Priority -> String -> IO ()-logSP = logM "Happstack.State.Spread"---data Cluster = Cluster- { clusterChan :: Closeable.Chan Closeable.R Message- , clusterConnection :: Connection- }--type EventId = Int--data ClusterMsg- = RequestState- | NewState B.ByteString -- A piece of state (map of all components)- | StateTransferred -- The entire state has been transmitted.- | ForeignEvent EventId Object- | EventResponse EventId Object- deriving (Typeable)-instance Version ClusterMsg -- version 0-$(deriveSerialize ''ClusterMsg)--{- | Connects the Happstack process to the local running spread daemon.- The spread daemon must already be running.--}--connectToCluster :: IO Cluster-connectToCluster- = do logSP NOTICE "Connecting to spread daemon on localhost"- uniq <- randomRIO (0,30000) -- FIXME: this is a hack to get unique names.- let name = mkPrivateName (B.pack $ "node" ++ show (uniq::Int))- (chan, conn) <- connect defaultConf{desiredName = name}- startReceive conn- join receiverGroup conn- return $ Cluster chan conn--receiverGroup :: Group-receiverGroup = let Just g = makeGroup "receiver"- in g--{- | This function modifies any event handlers in the given EventMap- to make them route the update information to the rest of the cluster---}--changeEventMapping :: MVar TxControl -> EventMap -> Cluster -> IO EventMap-changeEventMapping ctlVar localEventMap cluster- = do logSP NOTICE "Create new event mapper"- mems <- getClusterMembers cluster- responseIndex <- newMVar Map.empty- ready <- newEmptyMVar- eventQueue <- newChan- stateVar <- newMVar L.empty- eidStore <- newMVar 0 -- eid's are locally unique.- let newEID = modifyMVar eidStore (\store -> return (store+1,store+1))- pushStateBlock st = modifyMVar_ stateVar (\acc -> return $ acc `L.append` L.fromChunks [st])- insertEID eid = do mv <- newEmptyMVar- modifyMVar_ responseIndex $ \idx -> return $ Map.insert eid mv idx- return $ takeMVar mv- returnResponse eid object = modifyMVar_ responseIndex $ \idx ->- case Map.lookup eid idx of- Nothing -> return idx -- We're already received the result from another node.- Just mv -> do putMVar mv object -- Notify the caller about the new response.- return $ Map.delete eid idx- listener = forever $- do mbMsg <- Closeable.readChan (clusterChan cluster)- case mbMsg of- Nothing -> error "Disconnected from the spread daemon."- Just (Regular msg) ->- case fst (deserialize (L.fromChunks [inData msg])) of- RequestState -> sendState ctlVar (inSender msg) cluster- ForeignEvent eid object -> writeChan eventQueue (inSender msg, eid, object)- NewState st -> pushStateBlock st- -- FIXME: check that stateVar is non-empty?- StateTransferred -> do restoreState =<< takeMVar stateVar- putMVar ready ()- EventResponse eid object-> returnResponse eid object- Just _ -> return ()- responder = forever $- do (sender, eid, object) <- readChan eventQueue- response <- runObjectEventFunc object localEventMap- sendClusterMsg [sender] (EventResponse eid response) cluster- forkIO listener- forkIO responder- case mems of- [] -> putMVar ready () -- The cluster is empty, use state from disk.- (x:_) -> do logSP NOTICE $ "Requesting state from: " ++ show x- sendClusterMsg [x] RequestState cluster- logSP NOTICE "Waiting for ready signal"- takeMVar ready- logSP NOTICE "Ready signal received"- let newEventMap = flip Map.map localEventMap $ \handler ->- case handler of- UpdateHandler runCold _ parse- -> let runHot ev = do eid <- newEID- logSP NOTICE $ "New eid: " ++ show eid- wait <- insertEID eid- sendClusterMsg [receiverGroup] (ForeignEvent eid (mkObject ev)) cluster- response <- wait- logSP NOTICE $ "Received response for: " ++ show eid- return $ parseObject response- in UpdateHandler runCold runHot parse- QueryHandler{} -> handler- return newEventMap--sendState :: MVar TxControl -> Group -> Cluster -> IO ()-sendState ctlVar sender cluster- = withMVar ctlVar $ \ctl ->- do logSP NOTICE $ "Sending state to: " ++ show sender- allStates <- mapM getState (ctlAllComponents ctl)- let chunks = L.toChunks $ serialize $ zip (ctlAllComponents ctl) allStates- forM_ chunks $ \chunk ->- do sendClusterMsg [sender] (NewState chunk) cluster- sendClusterMsg [sender] StateTransferred cluster- logSP NOTICE $ "The state has been transmitted."--restoreState :: L.ByteString -> IO ()-restoreState stateObject- = do logSP NOTICE $ "Loading components from network."- forM_ state $ \(stateType, stateData) ->- do setNewState stateType stateData- logSP NOTICE $ "All components successfully loaded"- where state = fst (deserialize stateObject)--sendClusterMsg :: [Group] -> ClusterMsg -> Cluster -> IO ()-sendClusterMsg to msg cluster- = send outMsg (clusterConnection cluster)- where outMsg = Outgoing { outOrdering = Safe- , outDiscard = False- , outData = B.concat (L.toChunks (serialize msg))- , outGroups = to- , outMsgType = 0 }---- Cluster members not including self.-getClusterMembers :: Cluster -> IO [PrivateGroup]-getClusterMembers cluster- = do mbMsg <- Closeable.readChan (clusterChan cluster)- case mbMsg of- Just (Membership membership)- -> case membership of- Reg{members=m} -> return $ filter (/= privateGroup (clusterConnection cluster)) m- Transient{} -> error $ "Received a Transient message on connect."- SelfLeave{} -> error $ "Received a SelfLeave message on connect."- Just msg -> error $ "Expected membership message from cluster. Received a " ++ showMsgType msg ++ " message."- Nothing -> error "Cluster channel unexpectedly closed."--showMsgType :: Message -> String-showMsgType (Regular _) = "regular"-showMsgType (Membership _) = "membership"-showMsgType (Rejected _) = "rejected"-
src/Happstack/State/Transaction.hs view
@@ -22,7 +22,6 @@ import Happstack.Data.Serialize import Happstack.Data.SerializeTH import Happstack.State.Types-import Happstack.Util.Common (Seconds) import Data.Dynamic import GHC.Base@@ -40,9 +39,13 @@ getTime = sel (fromIntegral . txTime . evContext) getEventClockTime :: AnyEv ClockTime-getEventClockTime = do milliSeconds <- sel (txTime . evContext)- return $ TOD (fromIntegral milliSeconds) 0+getEventClockTime = do + milliSeconds <- sel (txTime . evContext)+ let (seconds, justMilliSeconds) = (fromIntegral milliSeconds) `divMod` 1000+ return $ TOD seconds (justMilliSeconds * 1000000000) ++ getEventId :: (Integral txId) => AnyEv txId getEventId = sel (fromIntegral . txId . evContext) @@ -87,7 +90,7 @@ data EventHandler where UpdateHandler :: UpdateEvent ev res =>- (TxContext -> ev -> IO ()) ->+ (TxContext -> ev -> IO res) -> (ev -> IO res) -> (Object -> ev) -> EventHandler@@ -121,7 +124,7 @@ EmitInternal eventMap -> emitFunc eventMap eventType ev emitEvent :: (Serialize ev, Typeable res) => ev -> IO res-emitEvent ev = emitEvent' (show (typeOf ev)) ev+emitEvent ev = emitEvent' (showQualifiedTypeRep (typeOf ev)) ev setNewEventMap :: EventMap -> IO () setNewEventMap eventMap@@ -166,10 +169,12 @@ return $ M.union (extraEvents tx) (M.map (eventHandler tx) m) return $ M.unions maps where (componentTree, _versions, _ioActions) = collectHandlers componentProxy+ eventHandler :: Serialize st => TxRun st -> Method st -> EventHandler eventHandler tx (Update fn)- = let updateCold' cxt ev+ = let updateCold' ev getCxt = do mv <- newEmptyMVar- let handler = do lastCxt <- readTVar (txLastTxContext tx)+ let handler = do cxt <- getCxt+ lastCxt <- readTVar (txLastTxContext tx) if txId lastCxt < txId cxt then do writeTVar (txLastTxContext tx) cxt writeTChan (txProcessQueue tx) $ IHR cxt ev $@@ -181,11 +186,12 @@ case me of Left e -> throwIO e Right e -> return e- updateCold cxt ev = do updateCold' cxt ev; return ()+ updateCold cxt ev = do updateCold' ev $ return cxt updateHot ev- = do cxt <- atomically . addTxId tx =<< newTxContext- updateCold' cxt ev+ = do cxt <- newTxContext+ updateCold' ev $ addTxId tx cxt in UpdateHandler updateCold updateHot parseObject+ eventHandler tx (Query fn) = let queryEmitter ev = do mv <- newEmptyMVar@@ -207,7 +213,7 @@ ] where t :: TxRun st -> st t _ = undefined- stateType = show (typeOf (t tx))+ stateType = showQualifiedTypeRep (typeOf (t tx)) getStateHandler tx' = let fn :: GetCheckpointState -> IO L.ByteString fn ev = do mv <- newEmptyMVar@@ -257,6 +263,10 @@ setNewState stateType state = emitEvent' (setNewStateType stateType) (SetCheckpointState state) +setNewState' :: EventMap -> TypeString -> L.ByteString -> IO ()+setNewState' eventMap stateType state+ = emitFunc eventMap (setNewStateType stateType) (SetCheckpointState state)+ getState :: TypeString -> IO L.ByteString getState stateType = emitEvent' (getStateType stateType) GetCheckpointState@@ -342,19 +352,19 @@ -> do res <- runHot (parse obj) return $ mkObject res -runColdEvent :: TxContext -> Object -> IO ()+runColdEvent :: TxContext -> Object -> IO Object runColdEvent cxt obj = do EmitInternal eventMap <- readIORef emitRef runColdEventFunc cxt obj eventMap -runColdEventFunc :: TxContext -> Object -> EventMap -> IO ()+runColdEventFunc :: TxContext -> Object -> EventMap -> IO Object runColdEventFunc cxt obj eventMap = do handler <- lookupEventHandler (objectType obj) eventMap case handler of QueryHandler{} -> error $ "Cold event was a query: " ++ objectType obj UpdateHandler runCold _runHotObj parse- -> do runCold cxt (parse obj)- return ()+ -> do res <- runCold cxt (parse obj)+ return $ mkObject res lookupEventHandler :: TypeString -> EventMap -> IO EventHandler lookupEventHandler eventType eventMap@@ -364,7 +374,7 @@ eventTString :: Serialize ev => ev -> TypeString-eventTString ev = show (typeOf ev)+eventTString ev = showQualifiedTypeRep (typeOf ev) @@ -423,20 +433,14 @@ globalRandomGen = unsafePerformIO (newMVar =<< getStdGen) -data TxConfig = TxConfig- { txcCheckpointSeconds :: Seconds, -- ^ Perform checkpoint at least every N seconds.- txcOperationMode :: OperationMode,- txcClusterSize :: Int, -- ^ Number of active nodes in the cluster (not counting this node).- txcClusterPort :: Int, --- txcCommitFrequency :: Int -- ^ Commits per second. Only applies to cluster mode.- }- data TxControl = TxControl { ctlSaver :: Saver -- ^ Saver given by the user. , ctlEventSaver :: MVar (WriterStream EventLogEntry) , ctlAllComponents :: [String] -- ^ Types of each component used. , ctlComponentVersions :: M.Map String [L.ByteString] -- ^ Map listing all versions of a component , ctlChildren :: [(ThreadId, MVar ())] -- + , ctlPrefixLock :: Maybe PrefixLock -- ^ Stores exclusive prefix lock (implemented in filesystem)+ , ctlCreateCheckpoint :: IO () } data EventLogEntry = EventLogEntry TxContext Object deriving (Typeable, Show)@@ -447,18 +451,7 @@ do (context, obj) <- safeGet return $ EventLogEntry context obj -data OperationMode- = SingleMode- | ClusterMode String -nullTxConfig :: TxConfig-nullTxConfig = TxConfig { txcCheckpointSeconds = 60*60*24,- txcOperationMode = SingleMode,- txcClusterSize = 0,- txcClusterPort = 8500,- txcCommitFrequency = 50- }- runTxLoop :: MVar (WriterStream EventLogEntry) -> ProcessQueue st -> st -> IO () runTxLoop eventSaverVar queue st0 = let loop st = do@@ -492,6 +485,10 @@ writerAdd eventSaver (EventLogEntry context (mkObject ev)) (logMT NOTICE "> disk " >> ra) loop st' in do forkIO $ (loop st0) `catch` (\ThreadKilled -> return ())+#if MIN_VERSION_base(4,2,0)+ `catch` (\BlockedIndefinitelyOnSTM -> return ())+#else `catch` (\BlockedIndefinitely -> return ())+#endif return ()
src/Happstack/State/TxControl.hs view
@@ -1,54 +1,117 @@+{-# LANGUAGE CPP #-} module Happstack.State.TxControl ( runTxSystem- , runTxSystem'+ , runTxSystemAmazon , shutdownSystem+ , createCheckpoint ) where import System.Log.Logger-import System.IO import Control.Monad import Control.Exception import Control.Concurrent -import Happstack.State.Checkpoint+import qualified Happstack.State.Checkpoint as Checkpoint import Happstack.State.Saver import Happstack.State.Transaction-import Happstack.State.Spread++#ifdef REPLICATION+import qualified Happstack.State.CentralLogServer as LogServer+#endif+ import Happstack.State.ComponentSystem import Happstack.Data.Proxy logMM :: Priority -> String -> IO () logMM = logM "Happstack.State.TxControl" ++-- | Given a Saver and a Proxy, createTxControl will +-- initialize a TxControl. This does not actually start the+-- state system.+createTxControl :: (Methods state, Component state) =>+ Saver -> Proxy state -> IO (MVar TxControl)+createTxControl saver prox+ = do ++ -- The state hasn't been loaded yet. Ignore events.+ eventSaverVar <- newMVar =<< createWriter NullSaver "events" 0+ -- obtain a prefix lock+ lock <- obtainLock saver+ newMVar $ TxControl+ { ctlSaver = saver+ , ctlEventSaver = eventSaverVar+ , ctlAllComponents = allStateTypes prox+ , ctlComponentVersions = componentVersions prox+ , ctlChildren = []+ , ctlPrefixLock = lock+ , ctlCreateCheckpoint = return () }+++-- | Saves the state and closes the serialization+closeTxControl :: MVar TxControl -> IO ()+closeTxControl ctlVar+ = do ctl <- takeMVar ctlVar+ writerClose =<< takeMVar (ctlEventSaver ctl)+ releaseLock (ctlPrefixLock ctl)+++ -- | Run the MACID system without multimaster support and with the given Saver. runTxSystem :: (Methods st, Component st) => Saver -> Proxy st -> IO (MVar TxControl)-runTxSystem = runTxSystem' False---- | Run the MACID system with multimaster support turned on if the first--- argument is True.-runTxSystem' :: (Methods st, Component st) => Bool -> Saver -> Proxy st -> IO (MVar TxControl)-runTxSystem' withMultimaster saver stateProxy =+runTxSystem saver stateProxy = do logMM NOTICE "Initializing system control." ctl <- createTxControl saver stateProxy+ -- insert code to lock based on the saver logMM NOTICE "Creating event mapper." localEventMap <- createEventMap ctl stateProxy setNewEventMap localEventMap logMM NOTICE "Restoring state."- enableLogging <- restoreState ctl- when (withMultimaster)- $ do logMM NOTICE "Multimaster mode"- cluster <- connectToCluster- eventMap <- changeEventMapping ctl localEventMap cluster- setNewEventMap eventMap+ enableLogging <- Checkpoint.restoreState ctl+ -- Multimaster support used to be here. -- enableLogging let ioActions = componentIO stateProxy logMM NOTICE "Forking children." children <- forM ioActions $ \action -> do mv <- newEmptyMVar tid <- forkIO (action `finally` putMVar mv ()) return (tid,mv)- modifyMVar_ ctl $ \c -> return c{ctlChildren = children}+ modifyMVar_ ctl $ \c -> return c{ ctlChildren = children+ , ctlCreateCheckpoint = Checkpoint.createCheckpoint ctl } return ctl +#ifdef REPLICATION+runTxSystemAmazon :: (Methods st, Component st) => LogServer.ApplicationName -> Proxy st -> IO (MVar TxControl)+runTxSystemAmazon appName stateProxy+ = do logMM NOTICE "Initializing system control"+ ctl <- createTxControl NullSaver stateProxy+ logMM NOTICE "Creating local event mapper."+ localEventMap <- createEventMap ctl stateProxy+ logMM NOTICE "Connecting to central log server."+ cluster <- LogServer.connectToCluster appName localEventMap+ logMM NOTICE "Modifying local event map."+ eventMap <- LogServer.changeEventMapping localEventMap cluster+ setNewEventMap eventMap+ let ioActions = componentIO stateProxy+ logMM NOTICE "Forking children."+ children <- forM ioActions $ \action -> do mv <- newEmptyMVar+ tid <- forkIO (action `finally` putMVar mv ())+ return (tid,mv)+ modifyMVar_ ctl $ \c -> return c{ ctlChildren = children+ , ctlCreateCheckpoint = LogServer.createCheckpoint ctl cluster }+ return ctl+#else+type ApplicationName = String -- Hm, this should actually be defined in CentralLogServer.hs++runTxSystemAmazon :: (Methods st, Component st) => ApplicationName -> Proxy st -> IO (MVar TxControl)+runTxSystemAmazon appName stateProxy+ = error "Happstack-state has been built without replication support."+#endif+++createCheckpoint :: MVar TxControl -> IO ()+createCheckpoint+ = join . fmap ctlCreateCheckpoint . readMVar+ -- | Shuts down a transaction system shutdownSystem :: MVar TxControl -> IO () shutdownSystem ctl@@ -59,3 +122,4 @@ mapM_ (takeMVar . snd) children -- FIXME: Use a timeout. logMM NOTICE "Shutdown complete" closeTxControl ctl+
src/Happstack/State/Types.hs view
@@ -1,7 +1,7 @@ {-# LANGUAGE UndecidableInstances, TemplateHaskell, ExistentialQuantification, RankNTypes, FlexibleInstances, DeriveDataTypeable,- ScopedTypeVariables, PatternSignatures+ ScopedTypeVariables #-} @@ -15,7 +15,6 @@ import Control.Monad.State import Control.Monad.Reader-import Control.Monad.Trans import Data.Generics -- Monad things @@ -29,6 +28,8 @@ instance Typeable StdGen where typeOf _ = mkTyConApp (mkTyCon "System.Random.StdGen") [] +#if (MIN_VERSION_random(1,0,1))+#else instance Random Word64 where randomR = integralRandomR random = randomR (minBound,maxBound)@@ -41,7 +42,7 @@ integralRandomR (a,b) g = case randomR (fromIntegral a :: Integer, fromIntegral b :: Integer) g of (x,g') -> (fromIntegral x, g')-+#endif data TxContext = TxContext { txId :: TxId,
src/Happstack/State/Util.hs view
@@ -7,7 +7,6 @@ ) where import Control.Concurrent.STM-import Control.Monad.State import System.Random import Happstack.State.Monad@@ -41,8 +40,10 @@ -- FIXME: Throw a decent error message if the input isn't a record.--- | Infer updating functions for a record @a_foo :: component -> record -> record@ and--- @withFoo = localState foo a_foo@.+-- | Infer updating functions for a record. Given a data declaration+-- of @data Foo = Foo {bar :: String, baz :: Int}@ then @$(inferRecordUpdaters ''Foo)@+-- will define functions @a_bar :: String -> Foo -> Foo@, @withBar :: Update String a -> Update Foo a@,+-- etc. that can be used as convenience updaters. inferRecordUpdaters :: Name -> Q [Dec] inferRecordUpdaters typeName = do con <- decToSimpleRecord =<< nameToDec typeName
tests/Happstack/State/Tests/CheckpointProperties.hs view
@@ -2,7 +2,6 @@ module Happstack.State.Tests.CheckpointProperties ( testCongestedCheckpoint , congestedCheckpoint- , checkCheckpointProperties , checkpointProperties , runRestoreCongestionKnownFailures ) where@@ -22,7 +21,6 @@ import Text.Printf import Test.QuickCheck-import Test.QuickCheck.Batch import Test.HUnit (Test(..),(~:), assertBool, assertFailure) --------------------------------------------------------------@@ -179,7 +177,7 @@ forkIO $ createCheckpoint ctl `finally` putMVar mv () takeMVar mv replicateM_ (length values) (waitQSem sem)-+{- checkCheckpointProperties :: IO () checkCheckpointProperties = tryTests ("runRestore") options [run (prop_runRestoreId saver)@@ -187,20 +185,20 @@ ,run (prop_runRestoreMultipleCheckpoint saver) ,run (prop_runRestoreAsync saver) ,run (prop_runRestoreCongestion saver)]- where options = defOpt{length_of_tests=5}+ where args = stdArgs {maxSize=5} saver = withQueueSaver withMemorySaver-+-} checkpointProperties :: Test checkpointProperties = "checkpointProperties" ~: - [ "prop_runRestoreId" ~: qccheck config (prop_runRestoreId saver)- , "prop_runRestoreCheckpoint" ~: qccheck config (prop_runRestoreCheckpoint saver)- , "prop_runRestoreMultipleCheckpoint" ~: qccheck config (prop_runRestoreMultipleCheckpoint saver)- , "prop_runRestoreAsync" ~: qccheck config (prop_runRestoreAsync saver)- , "prop_runRestoreCongestion" ~: qccheck config (prop_runRestoreCongestion saver)+ [ "prop_runRestoreId" ~: qccheck args (prop_runRestoreId saver)+ , "prop_runRestoreCheckpoint" ~: qccheck args (prop_runRestoreCheckpoint saver)+ , "prop_runRestoreMultipleCheckpoint" ~: qccheck args (prop_runRestoreMultipleCheckpoint saver)+ , "prop_runRestoreAsync" ~: qccheck args (prop_runRestoreAsync saver)+ , "prop_runRestoreCongestion" ~: qccheck args (prop_runRestoreCongestion saver) ] where- config = defaultConfig { configMaxTest = 100 }+ args = stdArgs { maxSize = 100 } saver = withQueueSaver withMemorySaver
tests/Happstack/State/Tests/Helpers.hs view
@@ -12,23 +12,16 @@ import Control.Exception import Control.Concurrent -import Test.QuickCheck-import Test.QuickCheck.Batch+import Test.QuickCheck (Arbitrary(arbitrary), choose) import Text.Printf -instance Arbitrary Char where- arbitrary = choose ('a','z')- coarbitrary = undefined- instance (Arbitrary a, Num a) => Arbitrary (Abs a) where arbitrary = liftM (Abs . abs) arbitrary- coarbitrary = undefined instance (Arbitrary a) => Arbitrary (NonEmpty a) where arbitrary = do x <- arbitrary xs <- arbitrary return $ NonEmpty (x:xs)- coarbitrary = undefined data Abs a = Abs a instance (Show a) => Show (Abs a) where@@ -88,7 +81,7 @@ , ("File", withFileSaver) , ("Queue File", \action -> withFileSaver (action . Queue)) ]-+{- tryTests :: String -> TestOptions -> [TestOptions -> IO TestResult] -> IO () tryTests name opt tests = do printf "%25s : " name@@ -106,3 +99,4 @@ putStrLn (" ** " ++ show exp) exitWith (ExitFailure 1) putChar '\n'+-}
tests/Happstack/State/Tests/SaverProperties.hs view
@@ -1,44 +1,43 @@ module Happstack.State.Tests.SaverProperties- ( checkSaverProperties- , saverProperties+ ( saverProperties ) where-+import Control.Monad+import Control.Exception+import Control.Concurrent+import Data.Char (isAlphaNum)+import Data.List import Happstack.State.Saver import Happstack.State.Saver.Impl.Memory import Happstack.State.Tests.Helpers-import Happstack.Util.Testing (qctest, qcrun)-+import Happstack.Util.Testing (qctest, qccheck) import System.Directory import System.Random import System.FilePath import System.IO.Unsafe-import Control.Monad-import Control.Exception-import Control.Concurrent-import Data.List -import Test.QuickCheck-import Test.QuickCheck.Batch-import Test.HUnit (Test(TestCase),(~:), assertFailure)+import Test.QuickCheck (Args(maxSize), Property, (==>), stdArgs)+import Test.HUnit (Test(TestCase),(~:)) -------------------------------------------------------------- -- Tests -------------------------------------------------------------- -- read (write values) == values-prop_getSetId withSaver key cutoff values- = not (null key) ==>- cutoff > 0 ==>+prop_getSetId :: ((Saver -> IO Bool) -> IO Bool) -> {- String -> -} Int -> [Int] -> Property+prop_getSetId withSaver {- (NonEmpty key) -} cutoff values+ = (abs cutoff) > 0 ==> ioProperty $ withSaver $ \saver ->- do writer <- createWriter saver key cutoff+ do writer <- createWriter saver key (abs cutoff) forM_ values $ \value -> writerAdd writer (value::Int) $ return () writerClose writer - reader <- createReader saver key cutoff+ reader <- createReader saver key (abs cutoff) out <- readerGetUncut reader return $ values == out+ where+ key = "key" -- not sure if randomly generating the key is useful. But it was causing trouble because it contain ~ / and other meaningful characters -prop_seqReadWrite withSaver (NonEmpty key) (Abs cutoff) valss+prop_seqReadWrite withSaver {-(NonEmpty key) -} (Abs cutoff) valss = ioProperty $ withSaver $ \saver -> do writer <- createWriter saver key cutoff forM_ valss $ \vals -> do forM_ vals $ \val -> writerAdd writer (val::Int) $ return ()@@ -48,8 +47,10 @@ reader <- createReader saver key cutoff (out,newCut) <- readerGet reader return $ out == concat valss && newCut == length valss + 1+ where+ key = "key" -prop_cutDrop withSaver (NonEmpty key) (Abs cutoff) a b+prop_cutDrop withSaver {- (NonEmpty key) -} (Abs cutoff) a b = ioProperty $ withSaver $ \saver -> do writer <- createWriter saver key cutoff forM_ a $ \val -> writerAdd writer (val::Int) $ return ()@@ -60,8 +61,10 @@ reader <- createReader saver key newCut (out,_) <- readerGet reader return $ b `isSuffixOf` out+ where+ key = "key" -prop_atomic withSaver (NonEmpty key) (Abs cutoff) value+prop_atomic withSaver {- (NonEmpty key) -} (Abs cutoff) value = ioProperty $ withSaver $ \saver -> do writer <- createWriter saver key cutoff writerAtomicReplace writer (value::Int)@@ -71,24 +74,26 @@ out <- readerGetUncut reader readerClose reader return $ out == [value]-+ where+ key = "key"+{- checkSaverProperties :: IO () checkSaverProperties = forEachSaver_ $ \name withSaver ->- tryTests (name ++ " saver") options [run (prop_getSetId withSaver)+ tryTests (name ++ " saver") args [run (prop_getSetId withSaver) ,run (prop_seqReadWrite withSaver) ,run (prop_cutDrop withSaver) ,run (prop_atomic withSaver)]- where options = defOpt{length_of_tests=5}-+ where args = stdArgs{maxSize=5}+-} saverProperties :: Test saverProperties = "saverProperties" ~: ((forEachSaver $ \name withSaver -> name ~:- [ "prop_getSetId" ~: qcrun (prop_getSetId withSaver) options- , "prop_seqReadWrite" ~: qcrun (prop_seqReadWrite withSaver) options- , "prop_cutDrop" ~: qcrun (prop_cutDrop withSaver) options- , "prop_atomic" ~: qcrun (prop_atomic withSaver) options+ [ "prop_getSetId" ~: qccheck args (prop_getSetId withSaver)+ , "prop_seqReadWrite" ~: qccheck args (prop_seqReadWrite withSaver)+ , "prop_cutDrop" ~: qccheck args (prop_cutDrop withSaver)+ , "prop_atomic" ~: qccheck args (prop_atomic withSaver) ]))- where options = defOpt{length_of_tests=5}+ where args = stdArgs{maxSize=5}