packages feed

festung-0.9.1.1: src/Festung/Vault/VaultManager.hs

{-# LANGUAGE NamedFieldPuns #-}

module Festung.Vault.VaultManager 
    ( newManager
    , stopManager
    , VaultManager
    , query
    , parametrizedQuery
    , deleteVault
    , ManagerError(..)
    ) where

import           Control.Concurrent
import           Control.Monad
import qualified Data.HashMap.Strict        as M
import           Data.Either                (isRight)
import           Control.Monad.Trans.Either
import           System.FilePath            ((</>), normalise)
import           System.Directory

import           Festung.Concurrency.Job
import           Festung.Concurrency.Utils
import           Festung.Config
import qualified Festung.Vault.VaultHandler as V
import qualified Festung.Vault.Persistence  as P
import           Festung.Utils              (hoistMEither, mapLeft, whenJust)


type VaultManager = Job VaultManagerCommand

data ManagerError = VaultError V.VaultError
                  | CouldNotReachManager
                    deriving (Show)

type TimeoutSubscriber = MVar ()

data VaultManagerCommand = OpenVault       (Command V.VaultOpener (Either ManagerError V.Vault))
                         | VaultTimedOut   (Command V.VaultOpener (Either ManagerError ()))
                         | GetSubscription (Command V.VaultOpener (Either ManagerError TimeoutSubscriber))
                         | DeleteVault     (Command V.VaultOpener (Either ManagerError ()))


type VaultMapKey = FilePath

data VaultMapValue = VaultMapValue { opener        :: V.VaultOpener
                                   , vault         :: V.Vault
                                   , subscriptions :: [TimeoutSubscriber]
                                   }

type VaultMap = M.HashMap VaultMapKey VaultMapValue

data ManagerState = ManagerState { registry :: VaultMap
                                 , timeouts :: Chan V.VaultOpener
                                 }


-- | Initial manager state (for internal use only)
newInitialState :: IO ManagerState
newInitialState = do
    timeouts <- newChan
    return ManagerState { registry = M.empty, timeouts = timeouts }


addSubscription :: TimeoutSubscriber -> VaultMapValue -> VaultMapValue
addSubscription s val@VaultMapValue{subscriptions} =
    let subscriptions' = s:subscriptions in
    val{subscriptions=subscriptions'}


-- | Create a timeout consumer
--
-- Process that reads a @'Chan' of vault opener and sends a VaultTimoedOut command
-- to the manager whan a new value is sent to the @'Chan' in question.
--
-- This is for internal use only
newTimeoutConsumer :: VaultManager -> Chan V.VaultOpener -> IO ThreadId
newTimeoutConsumer manager timeouts =
    let sendTimeout = sendMappedCommand VaultTimedOut in
    forkIOForSure $ forever $ do 
        opener <- readChan timeouts
        sendTimeout manager opener


newManager :: Config -> IO VaultManager
newManager config = do
    state           <- newInitialState
    manager         <- newJob state $ handlerLoop config
    timeoutConsumer <- newTimeoutConsumer manager (timeouts state)
    onExit manager $ killThread timeoutConsumer
    return manager


stopManager :: VaultManager -> IO ()
stopManager = killJob


updateStateRegistry :: (VaultMap -> VaultMap) -> ManagerState -> ManagerState
updateStateRegistry f ManagerState{registry,timeouts} =
    ManagerState { registry = f registry, timeouts = timeouts }


-- | Internal function to check the opener when
canAccess :: VaultMap -> VaultMapKey -> V.VaultOpener -> IO Bool
canAccess mapping key opener =
    case M.lookup key mapping of
        Just VaultMapValue{opener=opener'} -> isRight <$> V.checkOpener opener opener'
        Nothing -> return True


-- | Manager intenal loop (for internal use only)
handlerLoop :: Config -> ManagerState -> VaultManagerCommand -> IO (JobStatus, ManagerState)
handlerLoop config state cmd = do
    let opener@(path, _, _) = getOpener cmd
        key = normalise path
    canAccess' <- canAccess (registry state) key opener
    state'     <- if canAccess'
                      -- The opener is wrong, we stupidly say "vault does not exist" because you can't
                      -- access this vault with these parameters.
                      then handleCommand config state key cmd
                      else handleError cmd (VaultError V.CouldNotOpen) >> return state
    return (KeepGoing, state')


-- FIXME(Antoine): This code is totally copy pasted. This needs to use polymorphism.
handleError :: VaultManagerCommand -> ManagerError -> IO ()
handleError (OpenVault     (Command _ responder)) err = putMVar responder (Left err)
handleError (VaultTimedOut (Command _ responder)) err = putMVar responder (Left err)
handleError (DeleteVault   (Command _ responder)) err = putMVar responder (Left err)


getOpener :: VaultManagerCommand -> V.VaultOpener
getOpener (OpenVault       (Command o _)) = o
getOpener (VaultTimedOut   (Command o _)) = o
getOpener (DeleteVault     (Command o _)) = o
getOpener (GetSubscription (Command o _)) = o


-- XXX(Antoine): This is very very bad code
handleCommand :: Config -> ManagerState -> VaultMapKey -> VaultManagerCommand -> IO ManagerState
handleCommand config state key (OpenVault     (Command opener responder)) =
    case M.lookup key (registry state) of
        Just VaultMapValue{vault} -> do
            putMVar responder (Right vault)
            return state
        Nothing -> do
            res <- wrapManagerError <$> V.newHandler config opener
            -- FIXME(Antoine): Nesting!!!
            case res of
                Left err    -> do
                    putMVar responder (Left err)
                    return state
                Right vault -> do
                    onExit vault $ writeChan (timeouts state) opener
                    putMVar responder (Right vault)
                    let value = VaultMapValue {opener=opener, vault=vault, subscriptions=[]}
                    return $ updateStateRegistry (M.insert key value) state

handleCommand _config state key (VaultTimedOut (Command _opener responder)) = do
    whenJust (M.lookup key (registry state)) $ \ VaultMapValue{vault,subscriptions} -> do
        killJob vault -- Just making sure the vault is dead
        forM_ subscriptions $ \ s ->
            void $ tryPutMVar s ()
    putMVar responder $ Right ()
    return $ updateStateRegistry (M.delete key) state

handleCommand _config state key (GetSubscription (Command _opener responder)) =
    let noop = do subscription <- newMVar ()
                  putMVar responder $ Right subscription
                  return state
    in
    case M.lookup key (registry state) of
        Just VaultMapValue{vault} -> do
            running <- isJobRunning vault
            if running
                then noop
                else do
                    subscription <- newEmptyMVar
                    let addSubscription' = addSubscription subscription
                    putMVar responder $ Right subscription
                    return $ updateStateRegistry (M.adjust addSubscription' key) state
        Nothing -> noop

handleCommand Config{dataDirectory} state key (DeleteVault   (Command _opener responder)) = do
    let deleteVault = do filename <- canonicalizePath $ dataDirectory </> key
                         exists   <- doesFileExist filename
                         when exists $ removeFile filename
    case M.lookup key (registry state) of
        Just VaultMapValue{vault, subscriptions} -> do
            killJob vault
            forM_ subscriptions $ \s -> void $ tryPutMVar s ()
            deleteVault
        Nothing -> deleteVault
    putMVar responder $ Right ()
    return $ updateStateRegistry (M.delete key) state


wrapManagerError :: Either V.VaultError a -> Either ManagerError a
wrapManagerError = mapLeft VaultError


flattenError :: Maybe (Either ManagerError a) -> Either ManagerError a
flattenError Nothing  = Left CouldNotReachManager
flattenError (Just e) = e


-- | This is for internal use only
getVault :: V.VaultOpener -> VaultManager -> EitherT ManagerError IO V.Vault
getVault opener manager = hoistMEither $ flattenError <$> sendMappedCommand OpenVault manager opener


-- | This is for internal use only
waitForTimeout :: V.VaultOpener -> VaultManager -> IO (Either ManagerError ())
waitForTimeout opener manager =
    waitOnSubscription =<< flattenError <$> sendMappedCommand GetSubscription manager opener
        where waitOnSubscription (Left err)   = return $ Left err
              waitOnSubscription (Right mvar) = readMVar mvar >> return (Right ())


retryWhenTimedOut :: V.VaultOpener -> VaultManager -> IO (Either ManagerError a)
                  -> IO (Either ManagerError a)
retryWhenTimedOut opener manager action = do
    res <- action
    case res of
        Left (VaultError V.CouldNotReach) -> do
            timeout <- waitForTimeout opener manager
            case timeout of
                Left err -> return $ Left err
                Right _  -> action
        _                                 -> return res


-- | Execute a query on a vault
query :: V.VaultOpener -> VaultManager -> String -> IO (Either ManagerError P.QueryResult)
query opener manager query_ = retryWhenTimedOut opener manager $ runEitherT $ do
    vault <- getVault opener manager
    hoistMEither $ wrapManagerError <$> V.query opener vault query_


-- | Execute a parametrized query
parametrizedQuery :: V.VaultOpener -> VaultManager -> String -> [P.Value]
                  -> IO (Either ManagerError P.QueryResult)
parametrizedQuery opener manager query_ params = retryWhenTimedOut opener manager $ runEitherT $ do
    vault <- getVault opener manager
    hoistMEither $ wrapManagerError <$> V.parametrizedQuery opener vault query_ params


deleteVault :: V.VaultOpener -> VaultManager -> IO (Either ManagerError ())
deleteVault opener manager = flattenError <$> sendMappedCommand DeleteVault manager opener