packages feed

acid-state 0.8.3 → 0.10.0

raw patch · 6 files changed

+503/−175 lines, 6 filesdep ~basePVP ok

version bump matches the API change (PVP)

Dependency ranges changed: base

API changes (from Hackage documentation)

+ Data.Acid.Remote: AcidStateClosed :: AcidRemoteException
+ Data.Acid.Remote: AuthenticationError :: String -> AcidRemoteException
+ Data.Acid.Remote: CommChannel :: (ByteString -> IO ()) -> (Int -> IO (ByteString)) -> IO () -> CommChannel
+ Data.Acid.Remote: RemoteConnectionError :: AcidRemoteException
+ Data.Acid.Remote: SerializeError :: String -> AcidRemoteException
+ Data.Acid.Remote: ccClose :: CommChannel -> IO ()
+ Data.Acid.Remote: ccGetSome :: CommChannel -> Int -> IO (ByteString)
+ Data.Acid.Remote: ccPut :: CommChannel -> ByteString -> IO ()
+ Data.Acid.Remote: data AcidRemoteException
+ Data.Acid.Remote: data CommChannel
+ Data.Acid.Remote: instance Eq AcidRemoteException
+ Data.Acid.Remote: instance Exception AcidRemoteException
+ Data.Acid.Remote: instance Show AcidRemoteException
+ Data.Acid.Remote: instance Typeable AcidRemoteException
+ Data.Acid.Remote: process :: SafeCopy st => CommChannel -> AcidState st -> IO ()
+ Data.Acid.Remote: processRemoteState :: IsAcidic st => IO CommChannel -> IO (AcidState st)
+ Data.Acid.Remote: sharedSecretCheck :: Set ByteString -> (CommChannel -> IO Bool)
+ Data.Acid.Remote: sharedSecretPerform :: ByteString -> (CommChannel -> IO ())
+ Data.Acid.Remote: skipAuthenticationCheck :: CommChannel -> IO Bool
+ Data.Acid.Remote: skipAuthenticationPerform :: CommChannel -> IO Bool
- Data.Acid.Remote: acidServer :: SafeCopy st => AcidState st -> PortID -> IO ()
+ Data.Acid.Remote: acidServer :: SafeCopy st => (CommChannel -> IO Bool) -> PortID -> AcidState st -> IO ()
- Data.Acid.Remote: openRemoteState :: IsAcidic st => HostName -> PortID -> IO (AcidState st)
+ Data.Acid.Remote: openRemoteState :: IsAcidic st => (CommChannel -> IO ()) -> HostName -> PortID -> IO (AcidState st)

Files

acid-state.cabal view
@@ -1,71 +1,50 @@--- acid-state.cabal auto-generated by cabal init. For additional--- options, see--- http://www.haskell.org/cabal/release/cabal-latest/doc/users-guide/authors.html#pkg-descr.--- The name of the package. Name:                acid-state---- The package version. See the Haskell package versioning policy--- (http://www.haskell.org/haskellwiki/Package_versioning_policy) for--- standards guiding when and how versions should be incremented.-Version:             0.8.3---- A short (one-line) description of the package.+Version:             0.10.0 Synopsis:            Add ACID guarantees to any serializable Haskell data structure.---- A longer description of the package. Description:         Use regular Haskell data structures as your database and get stronger ACID guarantees than most RDBMS offer.---- URL for the project homepage or repository. Homepage:            http://acid-state.seize.it/---- The license under which the package is released. License:             PublicDomain---- The package author(s). Author:              David Himmelstrup---- An email address to which users can send suggestions, bug reports,--- and patches. Maintainer:          Lemmih <lemmih@gmail.com>---- A copyright notice.--- Copyright:           -+-- Copyright: Category:            Database- Build-type:          Simple---- Extra files to be distributed with the package, such as examples or--- a README.-Extra-source-files:  examples/*.hs, examples/errors/*.hs, src-win32/*.hs, src-unix/*.hs---- Constraint on the version of Cabal needed to build this package. Cabal-version:       >=1.6+Extra-source-files:+        examples/*.hs+        examples/errors/*.hs+        src-win32/*.hs+        src-unix/*.hs  Source-repository head   type:          darcs   location:      http://hub.darcs.net/Lemmih/acid-state - Library-  -- Modules exported by the library.   Exposed-Modules:     Data.Acid,                        Data.Acid.Local, Data.Acid.Memory,                        Data.Acid.Memory.Pure, Data.Acid.Remote,                        Data.Acid.Advanced -  -- Modules not exported by this package.   Other-modules:       Data.Acid.Log, Data.Acid.Archive,                        Data.Acid.CRC, Paths_acid_state,                        Data.Acid.TemplateHaskell, Data.Acid.Common, FileIO,                        Data.Acid.Abstract, Data.Acid.Core-  -  -- Packages needed in order to build this package.-  -- We need hGetSome from bytestring, added in 0.9.1.8-  Build-depends:       base >= 4 && < 5, cereal >= 0.3.2.0, safecopy >= 0.6,-                       bytestring >= 0.9.1.8, stm, extensible-exceptions,-                       filepath, directory, mtl, array, containers, template-haskell, network +  Build-depends:       array,+                       base >= 4 && < 5,+                       bytestring >= 0.9.1.8,+                       cereal >= 0.3.2.0,+                       containers,+                       extensible-exceptions,+                       safecopy >= 0.6,+                       stm,+                       directory,+                       filepath,+                       mtl,+                       network,+                       template-haskell+   if os(windows)      Build-depends:       Win32   else@@ -78,9 +57,4 @@   else      Hs-Source-Dirs:   src-unix/ -   GHC-Options:         -fwarn-unused-imports -fwarn-unused-binds--  -- Extra tools (e.g. alex, hsc2hs, ...) needed to build the source.-  -- Build-tools:         -  
examples/RemoteClient.hs view
@@ -1,48 +1,79 @@ {-# LANGUAGE DeriveDataTypeable, TypeFamilies, TemplateHaskell #-} module Main (main) where -import Data.Acid-import Data.Acid.Advanced-import Data.Acid.Remote+import Control.Monad         ( replicateM_ )+import Data.Acid             ( AcidState, closeAcidState, createCheckpoint, query, update )+import Data.Acid.Advanced    ( scheduleUpdate )+import Data.Acid.Remote      ( openRemoteState, sharedSecretPerform )+import Data.ByteString.Char8 ( pack )+import Network               ( PortID(..) )+import RemoteCommon          ( StressState(..), ClearState(..), PokeState(..), QueryState(..) )+import System.Environment    ( getArgs )+import System.IO             ( hFlush, stdout ) -import Control.Monad.State-import Control.Monad.Reader-import System.Environment-import System.IO-import Data.SafeCopy-import Data.Typeable-import Network+------------------------------------------------------+-- printHelp -import RemoteCommon+printHelp :: IO ()+printHelp+  = do putStrLn $ "Commands:"+       putStrLn $ "  query            Prints out the current state."+       putStrLn $ "  poke             Spawn 100k transactions."+       putStrLn $ "  checkpoint       Create a new checkpoint."+       putStrLn $ "  clear            Clear the state and create a new checkpoint."+       putStrLn $ "  quit             Exit with out creating a checkpoint."  --------------------------------------------------------- This is how AcidState is used:--open :: IO (AcidState StressState)-open = openRemoteState "localhost" (PortNumber 8080)+-- interactive command loop -main :: IO ()-main = do args <- getArgs-          case args of-            ["checkpoint"]-              -> do acid <- open -                    createCheckpoint acid-            ["query"]-              -> do acid <- open-                    n <- query acid QueryState+commandLoop :: AcidState StressState -> IO ()+commandLoop acid+  = do printHelp+       go+    where+      go = do+        putStr "> "+        hFlush stdout+        cmd <- getLine+        case cmd of+          "checkpoint"+              -> do createCheckpoint acid+                    go+          "query"+              -> do n <- query acid QueryState                     putStrLn $ "State value: " ++ show n-            ["poke"]-              -> do acid <- open-                    putStr "Issuing 100k transactions... "+                    go+          "poke"+              -> do putStr "Issuing 100k transactions... "                     hFlush stdout                     replicateM_ (100000-1) (scheduleUpdate acid PokeState)                     update acid PokeState                     putStrLn "Done"-            ["clear"]-              -> do acid <- open-                    update acid ClearState+                    go+          "clear"+              -> do update acid ClearState                     createCheckpoint acid-            _ -> do putStrLn $ "Commands:"-                    putStrLn $ "  query            Prints out the current state."-                    putStrLn $ "  poke             Spawn 100k transactions."-                    putStrLn $ "  checkpoint       Create a new checkpoint."+                    go+          "quit"+              -> do closeAcidState acid+                    return ()++          _+              -> do printHelp+                    go++------------------------------------------------------+-- connect to remote server and start command-loop++main :: IO ()+main+  = do args <- getArgs+       case args of+         [] ->+             do acid <- openRemoteState (sharedSecretPerform $ (pack "12345")) "localhost" (PortNumber $ fromIntegral 8080)+                commandLoop acid++         [hostname, port] ->+             do acid <- openRemoteState (sharedSecretPerform $ (pack "12345")) hostname (PortNumber $ fromIntegral $ read port)+                commandLoop acid+         _ -> putStrLn "Usage: RemoteClientTLS [<hostname> <port>]"
examples/RemoteServer.hs view
@@ -1,21 +1,18 @@ {-# LANGUAGE DeriveDataTypeable, TypeFamilies, TemplateHaskell #-} module Main (main) where -import Data.Acid-import Data.Acid.Remote (acidServer)--import Control.Exception (bracket)-import Data.Typeable--import Network--import RemoteCommon---- open a server on port 8080+import Control.Exception     ( bracket )+import Data.Acid             ( closeAcidState, openLocalState )+import Data.Acid.Remote      ( acidServer, sharedSecretCheck )+import Data.ByteString.Char8 ( pack )+import Data.Set              ( singleton )+import Network               ( PortID(PortNumber) )+import RemoteCommon          ( StressState(..) ) +-- | open a server on port 8080 main :: IO () main =     bracket       (openLocalState $ StressState 0)       closeAcidState-      (\s -> acidServer s (PortNumber 8080))+      (acidServer (sharedSecretCheck (singleton $ pack "12345")) (PortNumber 8080))
src/Data/Acid/Abstract.hs view
@@ -38,12 +38,12 @@                    (both those caused by hardware and software). -} data AcidState st-  = AcidState { +  = AcidState {                 _scheduleUpdate :: forall event. (UpdateEvent event, EventState event ~ st) => event -> IO (MVar (EventResult event))               , scheduleColdUpdate :: Tagged ByteString -> IO (MVar ByteString)               , _query :: (QueryEvent event, EventState event ~ st)  => event -> IO (EventResult event)               , queryCold :: Tagged ByteString -> IO ByteString-              , +              , -- | Take a snapshot of the state and save it to disk. Creating checkpoints --   makes it faster to resume AcidStates and you're free to create them as --   often or seldom as fits your needs. Transactions can run concurrently@@ -51,7 +51,7 @@ -- --   This call will not return until the operation has succeeded.                 createCheckpoint :: IO ()-              , +              , -- | Close an AcidState and associated resources. --   Any subsequent usage of the AcidState will throw an exception.                 closeAcidState :: IO ()@@ -107,10 +107,10 @@  downcast :: Typeable1 sub => AcidState st -> sub st downcast AcidState{acidSubState = AnyState sub}-  = r +  = r  where    r = case gcast1 (Just sub) of          Just (Just x) -> x-         Nothing ->+         _ ->            error $-            "Data.Acid: Invalid subtype cast: " ++ show (typeOf1 sub) ++ " -> " ++ show (typeOf1 r)  +            "Data.Acid: Invalid subtype cast: " ++ show (typeOf1 sub) ++ " -> " ++ show (typeOf1 r)
src/Data/Acid/Common.hs view
@@ -15,15 +15,19 @@  import Control.Monad.State import Control.Monad.Reader+import Data.ByteString.Lazy  ( ByteString ) import Data.SafeCopy-import Data.Serialize        (runGet, runGetLazy)+import Data.Serialize        ( Get, runGet, runGetLazy ) import Control.Applicative import qualified Data.ByteString as Strict  -- Silly fix for bug in cereal-0.3.3.0's version of runGetLazy.-runGetLazyFix getter inp        +runGetLazyFix :: Get a+           -> ByteString+           -> Either String a+runGetLazyFix getter inp   = case runGet getter Strict.empty of-      Left msg  -> runGetLazy getter inp+      Left _msg  -> runGetLazy getter inp       Right val -> Right val  class (SafeCopy st) => IsAcidic st where
src/Data/Acid/Remote.hs view
@@ -1,5 +1,4 @@-{-# LANGUAGE DeriveDataTypeable #-}-{-# LANGUAGE CPP #-}+{-# LANGUAGE CPP, DeriveDataTypeable, RankNTypes, RecordWildCards, ScopedTypeVariables #-} ----------------------------------------------------------------------------- {- |  Module      :  Data.Acid.Remote@@ -8,47 +7,256 @@  Maintainer  :  lemmih@gmail.com  Portability :  non-portable (uses GHC extensions) - Network backend.+ This module provides the ability perform 'update' and 'query' calls+from a remote process. +On the server-side you:++ 1. open your 'AcidState' normally++ 2. then use 'acidServer' to share the state++On the client-side you:++ 1. use 'openRemoteState' to connect to the remote state++ 2. use the returned 'AcidState' like any other 'AcidState' handle++'openRemoteState' and 'acidServer' communicate over an unencrypted+socket. If you need an encrypted connection, see @acid-state-tls@.++On Unix®-like systems you can use 'UnixSocket' to create a socket file for+local communication between the client and server. Access can be+controlled by setting the permissions of the parent directory+containing the socket file.++It is also possible to perform some simple authentication using+'sharedSecretCheck' and 'sharedSecretPerform'. Keep in mind that+secrets will be sent in plain-text if you do not use+@acid-state-tls@. If you are using a 'UnixSocket' additional+authentication may not be required, so you can use+'skipAuthenticationCheck' and 'skipAuthenticationPerform'.++Working with a remote 'AcidState' is nearly identical to working with+a local 'AcidState' with a few important differences.++The connection to the remote 'AcidState' can be lost. The client will+automatically attempt to reconnect every second. Because 'query'+events do not affect the state, an aborted 'query' will be retried+automatically after the server is reconnected.++If the connection was lost during an 'update' event, the event will+not be retried. Instead 'RemoteConnectionError' will be raised. This+is because it is impossible for the client to know if the aborted+update completed on the server-side or not.++When using a local 'AcidState', an update event in one thread does not+block query events taking place in other threads. With a remote+connection, all queries and requests are channeled over a single+connection. As a result, updates and queries are performed in the+order they are executed and do block each other. In the rare case+where this is an issue, you could create one remote connection per+thread.++When working with local state, a query or update which returns the+whole state is not usually a problem due to memory sharing. The+update/query event basically just needs to return a pointer to the+data already in memory. But, when working remotely, the entire result+will be serialized and sent to the remote client. Hence, it is good+practice to create queries and updates that will only return the+required data.++This module is designed to be extenible. You can easily add your own+authentication methods by creating a suitable pair of functions and+passing them to 'acidServer' and 'openRemoteState'.++It is also possible to create alternative communication layers using+'CommChannel', 'process', and 'processRemoteState'.+ -} module Data.Acid.Remote-    ( acidServer+    (+    -- * Server/Client+      acidServer     , openRemoteState+    -- * Authentication+    , skipAuthenticationCheck+    , skipAuthenticationPerform+    , sharedSecretCheck+    , sharedSecretPerform+    -- * Exception type+    , AcidRemoteException(..)+    -- * Low-Level functions needed to implement additional communication channels+    , CommChannel(..)+    , process+    , processRemoteState     ) where -+import Control.Concurrent.STM                        ( atomically )+import Control.Concurrent.STM.TMVar                  ( newEmptyTMVar, readTMVar, takeTMVar, tryTakeTMVar, putTMVar )+import Control.Concurrent.STM.TQueue+import Control.Exception                             ( AsyncException(ThreadKilled)+                                                     , Exception(fromException), IOException, Handler(..)+                                                     , SomeException, catch, catches, throw )+import Control.Exception                             ( throwIO, finally )+import Control.Monad                                 ( forever, liftM, join, when )+import Control.Concurrent                            ( ThreadId, forkIO, threadDelay, killThread, myThreadId )+import Control.Concurrent.MVar                       ( MVar, newEmptyMVar, putMVar, takeMVar )+import Control.Concurrent.Chan                       ( newChan, readChan, writeChan ) import Data.Acid.Abstract import Data.Acid.Core import Data.Acid.Common--import Network-import qualified Data.ByteString as Strict-import qualified Data.ByteString.Lazy as Lazy-import Control.Exception                             ( throwIO, ErrorCall(..), finally )-import Control.Monad                                 ( forever, liftM, join )-import Control.Concurrent+import qualified Data.ByteString                     as Strict+import Data.ByteString.Char8                         ( pack )+import qualified Data.ByteString.Lazy                as Lazy import Data.IORef                                    ( newIORef, readIORef, writeIORef ) import Data.Serialize import Data.SafeCopy                                 ( SafeCopy, safeGet, safePut )-import System.Directory                              ( removeFile )-import System.IO                                     ( Handle, hFlush, hClose )-import qualified Data.Sequence as Seq+import Data.Set                                      ( Set, member ) import Data.Typeable                                 ( Typeable )+import GHC.IO.Exception                              ( IOErrorType(..) )+import Network                                       ( HostName, PortID(..), connectTo, listenOn, withSocketsDo )+import Network.Socket                                ( Socket, accept, sClose )+import Network.Socket.ByteString                     ( recv, sendAll )+import System.Directory                              ( removeFile )+import System.IO                                     ( Handle, hPrint, hFlush, hClose, stderr )+import System.IO.Error                               ( ioeGetErrorType, isFullError, isDoesNotExistError ) -{- | Accept connections on @port@ and serve requests using the given 'AcidState'.+debugStrLn :: String -> IO ()+debugStrLn s =+    do -- putStrLn s -- uncomment to enable debugging+       return ()++-- | 'CommChannel' is a record containing the IO functions we need for communication between the server and client.+--+-- We abstract this out of the core processing function so that we can easily add support for SSL/TLS and Unit testing.+data CommChannel = CommChannel+    { ccPut     :: Strict.ByteString -> IO ()+    , ccGetSome :: Int -> IO (Strict.ByteString)+    , ccClose   :: IO ()+    }++data AcidRemoteException+    = RemoteConnectionError+    | AcidStateClosed+    | SerializeError String+    | AuthenticationError String+      deriving (Eq, Show, Typeable)+instance Exception AcidRemoteException+++-- | create a 'CommChannel' from a 'Handle'. The 'Handle' should be+-- some two-way communication channel, such as a socket+-- connection. Passing in a 'Handle' to a normal is file is unlikely+-- to do anything useful.+handleToCommChannel :: Handle -> CommChannel+handleToCommChannel handle =+    CommChannel { ccPut     = \bs -> Strict.hPut handle bs >> hFlush handle+                , ccGetSome = Strict.hGetSome handle+                , ccClose   = hClose handle+                }++{- | create a 'CommChannel' from a 'Socket'. The 'Socket' should be+     an accepted socket, not a listen socket.+-}+socketToCommChannel :: Socket -> CommChannel+socketToCommChannel socket =+    CommChannel { ccPut     = sendAll socket+                , ccGetSome = recv    socket+                , ccClose   = sClose  socket+                }++{- | skip server-side authentication checking entirely. -}+skipAuthenticationCheck :: CommChannel -> IO Bool+skipAuthenticationCheck _ = return True++{- | skip client-side authentication entirely. -}+skipAuthenticationPerform :: CommChannel -> IO Bool+skipAuthenticationPerform _ = return True++{- | check that the client knows a shared secret.++The function takes a 'Set' of shared secrets. If a client knows any+of them, it is considered to be trusted.++The shared secret is any 'ByteString' of your choice.++If you give each client a different shared secret then you can+revoke access individually.++see also: 'sharedSecretPerform'+-}+sharedSecretCheck :: Set Strict.ByteString -- ^ set of shared secrets+                  -> (CommChannel -> IO Bool)+sharedSecretCheck secrets cc =+    do bs <- ccGetSome cc 1024+       if member bs secrets+          then do ccPut cc (pack "OK")+                  return True+          else do ccPut cc (pack "FAIL")+                  return False++-- | attempt to authenticate with the server using a shared secret.+sharedSecretPerform :: Strict.ByteString -- ^ shared secret+                    -> (CommChannel -> IO ())+sharedSecretPerform pw cc =+    do ccPut cc pw+       r <- ccGetSome cc 1024+       if r == (pack "OK")+          then return ()+          else throwIO (AuthenticationError "shared secret authentication failed.")++{- | Accept connections on @port@ and handle requests using the given 'AcidState'.      This call doesn't return.++     On Unix®-like systems you can use 'UnixSocket' to communicate+     using a socket file. To control access, you can set the permissions of+     the parent directory which contains the socket file.++     see also: 'openRemoteState' and 'sharedSecretCheck'.  -}-acidServer :: SafeCopy st => AcidState st -> PortID -> IO ()-acidServer acidState port-  = do socket <- listenOn port-       forever (do (handle, _host, _port) <- accept socket-                   forkIO (process acidState handle))-         `finally` do sClose socket-                      case port of+acidServer :: SafeCopy st =>+              (CommChannel -> IO Bool) -- ^ check authentication, see 'sharedSecretPerform'+           -> PortID                   -- ^ Port to listen on+           -> AcidState st             -- ^ state to serve+           -> IO ()+acidServer checkAuth port acidState+  = withSocketsDo $+    do listenSocket <- listenOn port+       let loop = forever $+             do (socket, _sockAddr) <- accept listenSocket+                let commChannel = socketToCommChannel socket+                forkIO $ do authorized <- checkAuth commChannel+                            when authorized $+                                 process commChannel acidState+                            ccClose commChannel -- FIXME: `finally` ?+           infi = loop `catchSome` logError >> infi+       infi `finally` (cleanup listenSocket)+    where+      logError :: (Show e) => e -> IO ()+      logError e = hPrint stderr e++      isResourceVanishedError :: IOException -> Bool+      isResourceVanishedError = isResourceVanishedType . ioeGetErrorType++      isResourceVanishedType :: IOErrorType -> Bool+      isResourceVanishedType ResourceVanished = True+      isResourceVanishedType _                = False++      catchSome :: IO () -> (Show e => e -> IO ()) -> IO ()+      catchSome op _h =+          op `catches` [ Handler $ \(e :: IOException)    ->+                           if isFullError e || isDoesNotExistError e || isResourceVanishedError e+                            then return () -- h (toException e) -- we could log the exception, but there could be thousands of them+                            else throw e+                       ]+      cleanup socket =+          do sClose socket+             case port of #if !defined(mingw32_HOST_OS) && !defined(cygwin32_HOST_OS) && !defined(_WIN32)-                        UnixSocket path -> removeFile path+               UnixSocket path -> removeFile path #endif-                        _               -> return ()+               _               -> return ()  data Command = RunQuery (Tagged Lazy.ByteString)              | RunUpdate (Tagged Lazy.ByteString)@@ -64,30 +272,40 @@              0 -> liftM RunQuery get              1 -> liftM RunUpdate get              2 -> return CreateCheckpoint+             _ -> error $ "Serialize.get for Command, invalid tag: " ++ show tag -data Response = Result Lazy.ByteString | Acknowledgement+data Response = Result Lazy.ByteString | Acknowledgement | ConnectionError  instance Serialize Response where   put resp = case resp of                Result result -> do putWord8 0; put result                Acknowledgement -> putWord8 1+               ConnectionError -> putWord8 2   get = do tag <- getWord8            case tag of              0 -> liftM Result get              1 -> return Acknowledgement+             2 -> return ConnectionError+             _ -> error $ "Serialize.get for Response, invalid tag: " ++ show tag -process :: SafeCopy st => AcidState st -> Handle -> IO ()-process acidState handle+{- | Server inner-loop++     This function is generally only needed if you are adding a new communication channel.+-}+process :: SafeCopy st =>+           CommChannel  -- ^ a connected, authenticated communication channel+        -> AcidState st -- ^ state to share+        -> IO ()+process CommChannel{..} acidState   = do chan <- newChan        forkIO $ forever $ do response <- join (readChan chan)-                             Strict.hPut handle (encode response)-                             hFlush handle+                             ccPut (encode response)        worker chan (runGetPartial get Strict.empty)   where worker chan inp           = case inp of-              Fail msg      -> return () -- error msg-              Partial cont  -> do inp <- Strict.hGetSome handle 1024-                                  worker chan (cont inp)+              Fail msg      -> throwIO (SerializeError msg)+              Partial cont  -> do bs <- ccGetSome 1024+                                  worker chan (cont bs)               Done cmd rest -> do processCommand chan cmd; worker chan (runGetPartial get rest)         processCommand chan cmd =           case cmd of@@ -102,46 +320,145 @@ data RemoteState st = RemoteState (Command -> IO (MVar Response)) (IO ())                     deriving (Typeable) -{- | Connect to a remotely running 'AcidState'. -}-openRemoteState :: IsAcidic st => HostName -> PortID -> IO (AcidState st)-openRemoteState host port-  = do handle <- connectTo host port-       writeLock <- newMVar ()-       -- callbacks are added to the right and read from the left-       callbacks <- newMVar (Seq.empty :: Seq.Seq (Response -> IO ()))-       isClosed <- newIORef False-       let getCallback =-               modifyMVar callbacks $ \s -> return $-               case Seq.viewl s of-                 Seq.EmptyL -> noCallback-                 (cb Seq.:< s') -> (s', cb)-           noCallback = error "openRemote: Internal error: Missing callback."-           newCallback cb = modifyMVar_ callbacks (\s -> return (s Seq.|> cb))-           -           listener inp-             = case inp of-                 Fail msg       -> error msg-                 Partial cont   -> do inp <- Strict.hGetSome handle 1024-                                      listener (cont inp)-                 Done resp rest -> do callback <- getCallback-                                      callback (resp :: Response)-                                      listener (runGetPartial get rest)-           actor cmd = do readIORef isClosed >>= closedError-                          ref <- newEmptyMVar-                          withMVar writeLock $ \() -> do-                            newCallback (putMVar ref)-                            Strict.hPut handle (encode cmd) >> hFlush handle-                          return ref+{- | Connect to an acid-state server which is sharing an 'AcidState'. -}+openRemoteState :: IsAcidic st =>+                   (CommChannel -> IO ()) -- ^ authentication function, see 'sharedSecretPerform'+                -> HostName               -- ^ remote host to connect to (ignored when 'PortID' is 'UnixSocket')+                -> PortID                 -- ^ remote port to connect to+                -> IO (AcidState st)+openRemoteState performAuthorization host port+  = withSocketsDo $+    do processRemoteState reconnect+    where+      -- | reconnect+      reconnect :: IO CommChannel+      reconnect+          = (do debugStrLn "Reconnecting."+                handle <- connectTo host port+                let cc = handleToCommChannel handle+                performAuthorization cc+                debugStrLn "Reconnected."+                return cc+            )+            `catch`+            ((\_ -> threadDelay 1000000 >> reconnect) :: IOError -> IO CommChannel) -           closedError False = return ()-           closedError True  = throwIO $ ErrorCall "The AcidState has been closed" -       tid <- forkIO (listener (runGetPartial get Strict.empty))-       let shutdown = do writeIORef isClosed True-                         killThread tid-                         hClose handle-       return (toAcidState $ RemoteState actor shutdown)+{- | Client inner-loop +     This function is generally only needed if you are adding a new communication channel.+-}+processRemoteState :: IsAcidic st =>+                      IO CommChannel -- ^ (re-)connect function+                   -> IO (AcidState st)+processRemoteState reconnect+  = do cmdQueue    <- atomically newTQueue+       ccTMV       <- atomically newEmptyTMVar+       isClosed    <- newIORef False++       let actor :: Command -> IO (MVar Response)+           actor command =+               do debugStrLn "actor: begin."+                  readIORef isClosed >>= flip when (throwIO AcidStateClosed)+                  ref <- newEmptyMVar+                  atomically $ writeTQueue cmdQueue (command, ref)+                  debugStrLn "actor: end."+                  return ref++           expireQueue listenQueue =+               do mCallback <- atomically $ tryReadTQueue listenQueue+                  case mCallback of+                    Nothing         -> return ()+                    (Just callback) ->+                        do callback ConnectionError+                           expireQueue listenQueue++           handleReconnect :: SomeException -> IO ()+           handleReconnect e+             = case fromException e of+                 (Just ThreadKilled) ->+                     do debugStrLn "handleReconnect: ThreadKilled. Not attempting to reconnect."+                        return ()+                 _ ->+                   do debugStrLn $ "handleReconnect begin."+                      tmv <- atomically $ tryTakeTMVar ccTMV+                      case tmv of+                        Nothing ->+                            do debugStrLn $ "handleReconnect: error handling already in progress."+                               debugStrLn $ "handleReconnect end."+                               return ()+                        (Just (oldCC, oldListenQueue, oldListenerTID)) ->+                            do thisTID <- myThreadId+                               when (thisTID /= oldListenerTID) (killThread oldListenerTID)+                               ccClose oldCC+                               expireQueue oldListenQueue+                               cc <- reconnect+                               listenQueue <- atomically $ newTQueue+                               listenerTID <- forkIO $ listener cc listenQueue+                               atomically $ putTMVar ccTMV (cc, listenQueue, listenerTID)+                               debugStrLn $ "handleReconnect end."+                               return ()++           listener :: CommChannel -> TQueue (Response -> IO ()) -> IO ()+           listener cc listenQueue+             = getResponse Strict.empty `catch` handleReconnect+               where+                 getResponse leftover =+                     do debugStrLn $ "listener: listening for Response."+                        let go inp = case inp of+                                   Fail msg       -> error msg+                                   Partial cont   -> do debugStrLn $ "listener: ccGetSome"+                                                        bs <- ccGetSome cc 1024+                                                        go (cont bs)+                                   Done resp rest -> do debugStrLn $ "listener: getting callback"+                                                        callback <- atomically $ readTQueue listenQueue+                                                        debugStrLn $ "listener: passing Response to callback"+                                                        callback (resp :: Response)+                                                        return rest+                        rest <- go (runGetPartial get leftover) -- `catch` (\e -> do handleReconnect e+                                                                --                   throwIO e+                                                                 --        )+                        getResponse rest++           actorThread :: IO ()+           actorThread = forever $+             do debugStrLn "actorThread: waiting for something to do."+                (cc, cmd) <- atomically $+                  do (cmd, ref)        <- readTQueue cmdQueue+                     (cc, listenQueue, _) <- readTMVar ccTMV+                     writeTQueue listenQueue (putMVar ref)+                     return (cc, cmd)+                debugStrLn "actorThread: sending command."+                ccPut cc (encode cmd) `catch` handleReconnect+                debugStrLn "actorThread: sent."+                return ()++           shutdown :: ThreadId -> IO ()+           shutdown actorTID =+               do debugStrLn "shutdown: update isClosed IORef to True."+                  writeIORef isClosed True+                  debugStrLn "shutdown: killing actor thread."+                  killThread actorTID+                  debugStrLn "shutdown: taking ccTMV."+                  (cc, listenQueue, listenerTID) <- atomically $ takeTMVar ccTMV -- FIXME: or should this by tryTakeTMVar+                  debugStrLn "shutdown: killing listener thread."+                  killThread listenerTID+                  debugStrLn "shutdown: expiring listen queue."+                  expireQueue  listenQueue+                  debugStrLn "shutdown: closing connection."+                  ccClose cc+                  return ()++       cc <- reconnect+       listenQueue <- atomically $ newTQueue++       actorTID    <- forkIO $ actorThread+       listenerTID <- forkIO $ listener cc listenQueue++       atomically $ putTMVar ccTMV (cc, listenQueue, listenerTID)++       return (toAcidState $ RemoteState actor (shutdown actorTID))+ remoteQuery :: QueryEvent event => RemoteState (EventState event) -> event -> IO (EventResult event) remoteQuery acidState event   = do let encoded = runPutLazy (safePut event)@@ -151,9 +468,14 @@                  Right result -> result)  remoteQueryCold :: RemoteState st -> Tagged Lazy.ByteString -> IO Lazy.ByteString-remoteQueryCold (RemoteState fn _shutdown) event-  = do Result resp <- takeMVar =<< fn (RunQuery event)-       return resp+remoteQueryCold rs@(RemoteState fn _shutdown) event+  = do resp <- takeMVar =<< fn (RunQuery event)+       case resp of+         (Result result) -> return result+         ConnectionError -> do debugStrLn "retrying query event."+                               remoteQueryCold rs event+         Acknowledgement    -> error "remoteQueryCold got Acknowledgement. That should never happen."+  scheduleRemoteUpdate :: UpdateEvent event => RemoteState (EventState event) -> event -> IO (MVar (EventResult event)) scheduleRemoteUpdate (RemoteState fn _shutdown) event