packages feed

acid-state-dist-0.1.0.0: src/Data/Acid/Centered/Slave.hs

{-# LANGUAGE DeriveDataTypeable, RecordWildCards, FlexibleContexts #-}
--------------------------------------------------------------------------------
{- |
  Module      :  Data.Acid.CenteredSlave.hs
  Copyright   :  MIT

  Maintainer  :  max.voit+hdv@with-eyes.net
  Portability :  non-portable (uses GHC extensions)

  The Slave part of a the Centered replication backend for acid state.

-}

--------------------------------------------------------------------------------
-- SLAVE part

module Data.Acid.Centered.Slave
    (
      enslaveState
    , enslaveStateFrom
    , enslaveRedState
    , enslaveRedStateFrom
    , SlaveState(..)
    )  where

import Data.Typeable
import Data.SafeCopy
import Data.Serialize (decode, encode, runPutLazy, runGetLazy)

import Data.Acid
import Data.Acid.Core
import Data.Acid.Abstract
import Data.Acid.Local
import Data.Acid.Log

import Data.Acid.Centered.Common

import Control.Concurrent (forkIO, ThreadId, myThreadId, killThread, threadDelay, forkIOWithUnmask)
import Control.Concurrent.MVar (MVar, newMVar, newEmptyMVar, isEmptyMVar,
                                withMVar, modifyMVar, modifyMVar_,
                                takeMVar, putMVar, tryPutMVar)
import Control.Concurrent.Chan (Chan, newChan, readChan, writeChan)
import Control.Concurrent.STM.TVar (readTVar, writeTVar)
import Data.IORef (writeIORef)
import qualified Control.Concurrent.Event as Event

import Control.Monad.STM (atomically)
import Control.Monad (void, when, unless)
import Control.Exception (handle, throwTo, SomeException, ErrorCall(..))

import System.ZMQ4 (Context, Socket, Dealer(..),
                    setReceiveHighWM, setSendHighWM, setLinger, restrict,
                    poll, Poll(..), Event(..),
                    context, term, socket, close,
                    connect, disconnect, send, receive)
import System.FilePath ( (</>) )

import Data.ByteString.Lazy.Char8 (ByteString)

import           Data.IntMap (IntMap)
import qualified Data.IntMap as IM

--------------------------------------------------------------------------------

-- | Slave state structure, for internal use.
data SlaveState st
    = SlaveState { slaveLocalState :: AcidState st
                 , slaveStateIsRed :: Bool
                 , slaveStateLock :: MVar ()
                 , slaveRepFinalizers :: MVar (IntMap (IO ()))
                 , slaveRepChan :: Chan SlaveRepItem
                 , slaveSyncDone :: Event.Event
                 , slaveRevision :: MVar NodeRevision
                 , slaveRequests :: MVar SlaveRequests
                 , slaveLastRequestID :: MVar RequestID
                 , slaveRepThreadId :: MVar ThreadId
                 , slaveReqThreadId :: MVar ThreadId
                 , slaveParentThreadId :: ThreadId
                 , slaveZmqContext :: Context
                 , slaveZmqAddr :: String
                 , slaveZmqSocket :: MVar (Socket Dealer)
                 } deriving (Typeable)

-- | Memory of own Requests sent to Master.
type SlaveRequests = IntMap (IO (IO ()),ThreadId)

-- | One Update + Metainformation to replicate.
data SlaveRepItem =
      SRIEnd
    | SRICheckpoint Revision
    | SRIArchive Revision
    | SRIUpdate Revision (Maybe RequestID) (Tagged ByteString)

-- | Open a local State as Slave for a Master.
--
-- The directory for the local state files is the default one ("state/[typeOf
-- state]").
enslaveState :: (IsAcidic st, Typeable st) =>
            String          -- ^ hostname of the Master
         -> PortNumber      -- ^ port to connect to
         -> st              -- ^ initial state
         -> IO (AcidState st)
enslaveState address port initialState =
    enslaveStateFrom ("state" </> show (typeOf initialState)) address port initialState

-- | Open a local State as Slave for a Master.
--
-- The directory for the local state files is the default one ("state/[typeOf
-- state]").
enslaveRedState :: (IsAcidic st, Typeable st) =>
            String          -- ^ hostname of the Master
         -> PortNumber      -- ^ port to connect to
         -> st              -- ^ initial state
         -> IO (AcidState st)
enslaveRedState address port initialState =
    enslaveRedStateFrom ("state" </> show (typeOf initialState)) address port initialState

-- | Open a local State as Slave for a Master. The directory of the local state
-- files can be specified.
enslaveStateFrom :: (IsAcidic st, Typeable st) =>
            FilePath        -- ^ location of the local state files.
         -> String          -- ^ hostname of the Master
         -> PortNumber      -- ^ port to connect to
         -> st              -- ^ initial state
         -> IO (AcidState st)
enslaveStateFrom = enslaveMayRedStateFrom False

-- | Open a local State as Slave for a _redundant_ Master. The directory of the local state
-- files can be specified.
enslaveRedStateFrom :: (IsAcidic st, Typeable st) =>
            FilePath        -- ^ location of the local state files.
         -> String          -- ^ hostname of the Master
         -> PortNumber      -- ^ port to connect to
         -> st              -- ^ initial state
         -> IO (AcidState st)
enslaveRedStateFrom = enslaveMayRedStateFrom True

-- | Open a local State as Slave for a Master, redundant or not.
--   The directory of the local state files can be specified.
enslaveMayRedStateFrom :: (IsAcidic st, Typeable st) =>
            Bool            -- ^ is redundant
         -> FilePath        -- ^ location of the local state files.
         -> String          -- ^ hostname of the Master
         -> PortNumber      -- ^ port to connect to
         -> st              -- ^ initial state
         -> IO (AcidState st)
enslaveMayRedStateFrom isRed directory address port initialState = do
    -- local
    lst <- openLocalStateFrom directory initialState
    lrev <- getLocalRevision lst
    rev <- newMVar lrev
    debug $ "Opening enslaved state at revision " ++ show lrev
    srs <- newMVar IM.empty
    lastReqId <- newMVar 0
    repChan <- newChan
    syncDone <- Event.new
    reqTid <- newEmptyMVar
    repTid <- newEmptyMVar
    parTid <- myThreadId
    repFin <- newMVar IM.empty
    sLock <- newEmptyMVar
    -- remote
    let addr = "tcp://" ++ address ++ ":" ++ show port
    ctx <- context
    sock <- socket ctx Dealer
    setReceiveHighWM (restrict (100*1000 :: Int)) sock
    setSendHighWM (restrict (100*1000 :: Int)) sock
    connect sock addr
    msock <- newMVar sock
    sendToMaster msock $ NewSlave lrev

    let slaveState = SlaveState { slaveLocalState = lst
                                , slaveStateIsRed = isRed
                                , slaveStateLock = sLock
                                , slaveRepFinalizers = repFin
                                , slaveRepChan = repChan
                                , slaveSyncDone = syncDone
                                , slaveRevision = rev
                                , slaveRequests = srs
                                , slaveLastRequestID = lastReqId
                                , slaveReqThreadId = reqTid
                                , slaveRepThreadId = repTid
                                , slaveParentThreadId = parTid
                                , slaveZmqContext = ctx
                                , slaveZmqAddr = addr
                                , slaveZmqSocket = msock
                                }
    void $ forkIOWithUnmask $ slaveRequestHandler slaveState
    void $ forkIO $ slaveReplicationHandler slaveState
    return $ slaveToAcidState slaveState
    where
        getLocalRevision =
            atomically . readTVar . logNextEntryId . localEvents . downcast

-- | Replication handler of the Slave.
slaveRequestHandler :: (IsAcidic st, Typeable st) => SlaveState st -> (IO () -> IO ()) -> IO ()
slaveRequestHandler slaveState@SlaveState{..} unmask = do
    mtid <- myThreadId
    putMVar slaveReqThreadId mtid
    let loop = handle (\e -> throwTo slaveParentThreadId (e :: SomeException)) $
            unmask $ handle killHandler $ do
            --waitRead =<< readMVar slaveZmqSocket
            -- FIXME: we needn't poll if not for strange zmq behaviour
            re <- withMVar slaveZmqSocket $ \sock -> poll 100 [Sock sock [In] Nothing]
            unless (null $ head re) $ do
                msg <- withMVar slaveZmqSocket receive
                case decode msg of
                    Left str -> error $ "Data.Serialize.decode failed on MasterMessage: " ++ show str
                    Right mmsg -> handleMessage mmsg
            loop
    loop
    where
        killHandler :: AcidException -> IO ()
        killHandler GracefulExit = return ()
        handleMessage m = do
            debug $ "Received: " ++ show m
            case m of
               -- We are sent an Update to replicate.
               DoRep r i d -> queueRepItem slaveState (SRIUpdate r i d)
               -- We are sent a Checkpoint for synchronization.
               DoSyncCheckpoint r d -> replicateSyncCp slaveState r d
               -- We are sent an Update to replicate for synchronization.
               DoSyncRep r d -> replicateSyncUpdate slaveState r d
               -- Master done sending all synchronization Updates.
               SyncDone c -> onSyncDone slaveState c
               -- We are sent a Checkpoint request.
               DoCheckpoint r -> queueRepItem slaveState (SRICheckpoint r)
               -- We are sent an Archive request.
               DoArchive r -> queueRepItem slaveState (SRIArchive r)
               -- Full replication of a revision
               FullRep r -> modifyMVar_ slaveRepFinalizers $ \rf -> do
                               rf IM.! r
                               return $ IM.delete r rf
               -- Full replication of events up to revision
               FullRepTo r -> modifyMVar_ slaveRepFinalizers $ \rf -> do
                               let (ef, nrf) = IM.partitionWithKey (\k _ -> k <= r) rf
                               sequence_ (IM.elems ef)
                               return nrf
               -- We are allowed to Quit.
               MayQuit -> writeChan slaveRepChan SRIEnd
               -- We are requested to Quit - shall be handled by
               -- 'bracket' usage by user.
               MasterQuit -> throwTo slaveParentThreadId $
                   ErrorCall "Data.Acid.Centered.Slave: Master quit."
               -- no other messages possible, enforced by type checker

-- | After sync check CRC
onSyncDone :: (IsAcidic st, Typeable st) => SlaveState st -> Crc -> IO ()
onSyncDone SlaveState{..} crc = do
    localCrc <- crcOfState slaveLocalState
    if crc /= localCrc then
        error "Data.Acid.Centered.Slave: CRC mismatch after sync."
    else do
        debug "Sync Done, CRC fine."
        Event.set slaveSyncDone

-- | Queue Updates into Chan for replication.
-- We use the Chan so Sync-Updates and normal ones can be interleaved.
queueRepItem :: SlaveState st -> SlaveRepItem -> IO ()
queueRepItem SlaveState{..} repItem = do
    debug "Queuing RepItem."
    writeChan slaveRepChan repItem

-- | Replicates content of Chan.
slaveReplicationHandler :: Typeable st => SlaveState st -> IO ()
slaveReplicationHandler slaveState@SlaveState{..} = do
    mtid <- myThreadId
    putMVar slaveRepThreadId mtid

    -- todo: timeout is magic variable, make customizable
    noTimeout <- Event.waitTimeout slaveSyncDone $ 10*1000*1000
    unless noTimeout $ throwTo slaveParentThreadId $
        ErrorCall "Data.Acid.Centered.Slave: Took too long to sync. Timeout."

    let loop = handle (\e -> throwTo slaveParentThreadId (e :: SomeException)) $ do
            mayRepItem <- readChan slaveRepChan
            case mayRepItem of
                SRIEnd          -> return ()
                SRICheckpoint r -> repCheckpoint slaveState r               >> loop
                SRIArchive r    -> repArchive slaveState r                  >> loop
                SRIUpdate r i d -> replicateUpdate slaveState r i d False   >> loop
    loop

    -- signal that we're done
    void $ takeMVar slaveRepThreadId

-- | Replicate Sync-Checkpoints directly.
replicateSyncCp :: (IsAcidic st, Typeable st) =>
        SlaveState st -> Revision -> ByteString -> IO ()
replicateSyncCp SlaveState{..} rev encoded = do
    st <- decodeCheckpoint encoded
    let lst = downcast slaveLocalState
    let core = localCore lst
    modifyMVar_ slaveRevision $ \sr -> do
        when (sr > rev) $ error "Data.Acid.Centered.Slave: Revision mismatch for checkpoint: Slave is newer."
        modifyCoreState_ core $ \_ -> do
            writeIORef (localCopy lst) st
            createCpFake lst encoded rev
            adjustEventLogId lst rev
            return st
        return rev
    where
        adjustEventLogId l r = do
            atomically $ writeTVar (logNextEntryId (localEvents l)) r
            void $ cutFileLog (localEvents l)
        createCpFake l e r = do
            mvar <- newEmptyMVar
            pushAction (localEvents l) $
                pushEntry (localCheckpoints l) (Checkpoint r e) (putMVar mvar ())
            takeMVar mvar
        decodeCheckpoint e =
            case runGetLazy safeGet e of
                Left msg  -> error $ "Data.Acid.Centered.Slave: Checkpoint could not be decoded: " ++ msg
                Right val -> return val

-- | Replicate Sync-Updates directly.
replicateSyncUpdate :: Typeable st => SlaveState st -> Revision -> Tagged ByteString -> IO ()
replicateSyncUpdate slaveState rev event = replicateUpdate slaveState rev Nothing event True

-- | Replicate an Update as requested by Master.
--   Updates that were requested by this Slave are run locally and the result
--   put into the MVar in SlaveRequests.
--   Other Updates are just replicated without using the result.
replicateUpdate :: Typeable st => SlaveState st -> Revision -> Maybe RequestID -> Tagged ByteString -> Bool -> IO ()
replicateUpdate SlaveState{..} rev reqId event syncing = do
    debug $ "Got an Update to replicate " ++ show rev
    modifyMVar_ slaveRevision $ \nr -> if rev - 1 == nr
        then do
            -- commit / run it locally
            case reqId of
                Nothing -> replicateForeign
                Just rid -> replicateOwn rid
            -- send reply: we're done
            unless syncing $ sendToMaster slaveZmqSocket $ RepDone rev
            return rev
        else do
            sendToMaster slaveZmqSocket RepError
            void $ error $
                "Data.Acid.Centered.Slave: Replication failed at revision "
                    ++ show nr ++ " -> " ++ show rev
            return nr
    where
        replicateForeign =
            if slaveStateIsRed then do
                act <- newEmptyMVar >>= scheduleLocalColdUpdate' (downcast slaveLocalState) event
                modifyMVar_ slaveRepFinalizers $ return . IM.insert rev act
            else
                void $ scheduleColdUpdate slaveLocalState event
        replicateOwn rid = do
            act <- modifyMVar slaveRequests $ \srs -> do
                debug $ "This is the Update for Request " ++ show rid
                let (icallback, timeoutId) = srs IM.! rid
                callback <- icallback
                killThread timeoutId
                let nsrs = IM.delete rid srs
                return (nsrs, callback)
            when slaveStateIsRed $
                modifyMVar_ slaveRepFinalizers $ return . IM.insert rev act

repCheckpoint :: SlaveState st -> Revision -> IO ()
repCheckpoint SlaveState{..} rev = do
    debug $ "Got Checkpoint request at revision: " ++ show rev
    withMVar slaveRevision $ \_ ->
        -- create checkpoint
        createCheckpoint slaveLocalState

repArchive :: SlaveState st -> Revision -> IO ()
repArchive SlaveState{..} rev = do
    debug $ "Got Archive request at revision: " ++ show rev
    withMVar slaveRevision $ \_ ->
        createArchive slaveLocalState


-- | Update on slave site.
--      The steps are:
--      - Request Update from Master
--      - Master issues Update with same RequestID
--      - repHandler replicates and puts result in MVar
scheduleSlaveUpdate :: (UpdateEvent e, Typeable (EventState e)) => SlaveState (EventState e) -> e -> IO (MVar (EventResult e))
scheduleSlaveUpdate slaveState@SlaveState{..} event = do
    unlocked <- isEmptyMVar slaveStateLock
    if not unlocked then error "State is locked."
    else do
        debug "Update by Slave."
        result <- newEmptyMVar
        reqId <- getNextRequestId slaveState
        modifyMVar_ slaveRequests $ \srs -> do
            let encoded = runPutLazy (safePut event)
            sendToMaster slaveZmqSocket $ ReqUpdate reqId (methodTag event, encoded)
            timeoutID <- forkIO $ timeoutRequest slaveState reqId result
            let callback = if slaveStateIsRed
                    then scheduleLocalUpdate' (downcast slaveLocalState) event result
                    else do
                        hd <- scheduleUpdate slaveLocalState event
                        void $ forkIO $ putMVar result =<< takeMVar hd
                        return (return ())      -- bogus finalizer
            return $ IM.insert reqId (callback, timeoutID) srs
        return result

-- | Cold Update on slave site. This enables for using Remote.
scheduleSlaveColdUpdate :: Typeable st => SlaveState st -> Tagged ByteString -> IO (MVar ByteString)
scheduleSlaveColdUpdate slaveState@SlaveState{..} encoded = do
    unlocked <- isEmptyMVar slaveStateLock
    if not unlocked then error "State is locked."
    else do
        debug "Cold Update by Slave."
        result <- newEmptyMVar
        -- slaveLastRequestID is only modified here - and used for locking the state
        reqId <- getNextRequestId slaveState
        modifyMVar_ slaveRequests $ \srs -> do
            sendToMaster slaveZmqSocket $ ReqUpdate reqId encoded
            timeoutID <- forkIO $ timeoutRequest slaveState reqId result
            let callback = if slaveStateIsRed
                    then scheduleLocalColdUpdate' (downcast slaveLocalState) encoded result
                    else do
                        hd <- scheduleColdUpdate slaveLocalState encoded
                        void $ forkIO $ putMVar result =<< takeMVar hd
                        return (return ())      -- bogus finalizer
            return $ IM.insert reqId (callback, timeoutID) srs
        return result

-- | Generate ID for another request.
getNextRequestId :: SlaveState st -> IO RequestID
getNextRequestId SlaveState{..} = modifyMVar slaveLastRequestID $ \x -> return (x+1,x+1)

-- | Ensures requests are actually answered or fail.
--   On timeout the Slave dies, not the thread that invoked the Update.
timeoutRequest :: SlaveState st -> RequestID -> MVar m -> IO ()
timeoutRequest SlaveState{..} reqId mvar = do
    threadDelay $ 5*1000*1000
    stillThere <- withMVar slaveRequests (return . IM.member reqId)
    when stillThere $ do
        putMVar mvar $ error "Data.Acid.Centered.Slave: Update-Request timed out."
        throwTo slaveParentThreadId $ ErrorCall "Data.Acid.Centered.Slave: Update-Request timed out."

-- | Send a message to Master.
sendToMaster :: MVar (Socket Dealer) -> SlaveMessage -> IO ()
sendToMaster msock smsg = withMVar msock $ \sock -> send sock [] (encode smsg)

-- | Close an enslaved State.
liberateState :: SlaveState st -> IO ()
liberateState SlaveState{..} =
    -- lock state against updates: disallow requests
    whenM (tryPutMVar slaveStateLock ()) $ do
        debug "Closing Slave state..."
        -- check / wait unprocessed requests
        debug "Waiting for Requests to finish."
        waitPoll 100 (withMVar slaveRequests (return . IM.null))
        -- send master quit message
        sendToMaster slaveZmqSocket SlaveQuit
        -- wait replication chan, only if sync done
        syncDone <- Event.isSet slaveSyncDone
        when syncDone $ do
            debug "Waiting for repChan to empty."
            mtid <- myThreadId
            putMVar slaveRepThreadId mtid
        -- kill handler threads
        debug "Killing request handler."
        withMVar slaveReqThreadId $ flip throwTo GracefulExit
        -- cleanup zmq
        debug "Closing down zmq."
        withMVar slaveZmqSocket $ \s -> do
            -- avoid the socket hanging around
            setLinger (restrict (1000 :: Int)) s
            disconnect s slaveZmqAddr
            close s
        term slaveZmqContext
        -- cleanup local state
        debug "Closing local state."
        closeAcidState slaveLocalState

slaveToAcidState :: (IsAcidic st, Typeable st)  => SlaveState st -> AcidState st
slaveToAcidState slaveState
  = AcidState { _scheduleUpdate    = scheduleSlaveUpdate slaveState
              , scheduleColdUpdate = scheduleSlaveColdUpdate slaveState
              , _query             = query $ slaveLocalState slaveState
              , queryCold          = queryCold $ slaveLocalState slaveState
              , createCheckpoint   = createCheckpoint $ slaveLocalState slaveState
              , createArchive      = createArchive $ slaveLocalState slaveState
              , closeAcidState     = liberateState slaveState
              , acidSubState       = mkAnyState slaveState
              }