packages feed

cql-io-2.0.0: src/lib/Database/CQL/IO/Cluster/Policies.hs

-- This Source Code Form is subject to the terms of the Mozilla Public
-- License, v. 2.0. If a copy of the MPL was not distributed with this
-- file, You can obtain one at http://mozilla.org/MPL/2.0/.

{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE RankNTypes #-}

module Database.CQL.IO.Cluster.Policies
    ( Policy (..)
    , random
    , roundRobin
    ) where

import Control.Applicative
import Control.Concurrent.STM
import Control.Lens ((^.), view, over, makeLenses)
import Control.Monad
import Data.Map.Strict (Map)
import Data.Maybe (isNothing)
import Data.Word
import Database.CQL.IO.Cluster.Host
import Database.CQL.IO.PrepQuery (ServerPrepQuery)
import Database.CQL.Protocol (Tuple)
import System.Random.MWC
import Prelude

import qualified Data.Map.Strict as Map

-- | A policy defines a load-balancing strategy and generally
-- handles host visibility.
data Policy = Policy
    { setup :: [Host] -> [Host] -> IO ()
      -- ^ Initialise the policy with two sets of hosts. The first
      -- parameter are hosts known to be available, the second are other
      -- nodes. Note that a policy may be re-initialised at any point
      -- through this function.
    , onEvent :: HostEvent -> IO ()
      -- ^ Event handler. Policies will be informed about cluster changes
      -- through this function.
    , select :: forall k a b. Tuple a => Maybe (ServerPrepQuery k a b, a) -> IO (Maybe Host)
      -- ^ Host selection. The driver will ask for a host to use in a query
      -- through this function. A policy which has no available nodes may
      -- return Nothing.
    , current :: IO [Host]
      -- ^ Return all currently alive hosts.
    , acceptable :: Host -> IO Bool
      -- ^ During startup and node discovery, the driver will ask the
      -- policy if a dicovered host should be ignored.
    , priority :: Host -> IO Int
      -- ^ During startup and node discovery, the driver will ask the
      -- policy if a dicovered host should be ignored, and if not, for a
      -- priority value for a control connection to that host. Lower values are
      -- preferred before later values. Non-positive values are ignored.
    , hostCount :: IO Word
      -- ^ During query processing, the driver will ask the policy for
      -- a rough estimate of alive hosts. The number is used to repeatedly
      -- invoke 'select' (with the underlying assumption that the policy
      -- returns mostly different hosts).
    , display :: IO String
      -- ^ Like having an effectful 'Show' instance for this policy.
    }

type HostMap = TVar Hosts

data Hosts = Hosts
    { _alive :: !(Map InetAddr Host)
    , _other :: !(Map InetAddr Host)
    } deriving Show

makeLenses ''Hosts

-- | Iterate over hosts one by one.
roundRobin :: IO Policy
roundRobin = do
    h <- newTVarIO emptyHosts
    c <- newTVarIO 0
    return (defPolicy h)
      { select = pickHost h c }
  where
    pickHost h c _ = atomically $ do
        m <- view alive <$> readTVar h
        if Map.null m then
            return Nothing
        else do
            k <- readTVar c
            writeTVar c $ succ k `mod` Map.size m
            return . Just . snd $ Map.elemAt (k `mod` Map.size m) m

-- | Return hosts in random order.
random :: IO Policy
random = do
    h <- newTVarIO emptyHosts
    g <- createSystemRandom
    return (defPolicy h)
      { select = pickHost h g }
  where
    pickHost h g _ = do
        m <- view alive <$> readTVarIO h
        if Map.null m then
            return Nothing
        else do
            let i = uniformR (0, Map.size m - 1) g
            Just . snd . flip Map.elemAt m <$> i

-----------------------------------------------------------------------------
-- Defaults

defPolicy :: TVar Hosts -> Policy
defPolicy h = Policy
  { setup = defSetup h
  , onEvent = defOnEvent h
  , select = const $ pure Nothing
  , current = defCurrent h
  , acceptable = defAcceptable
  , priority = defPriority
  , hostCount = defHostCount h
  , display = defDisplay h
  }

emptyHosts :: Hosts
emptyHosts = Hosts Map.empty Map.empty

defDisplay :: HostMap -> IO String
defDisplay h = show <$> readTVarIO h

defAcceptable :: Host -> IO Bool
defAcceptable = fmap (/= 0) . defPriority

defPriority :: Host -> IO Int
defPriority = const $ pure 1

defSetup :: HostMap -> [Host] -> [Host] -> IO ()
defSetup r a b = do
    let ha = Map.fromList $ zip (map (view hostAddr) a) a
    let hb = Map.fromList $ zip (map (view hostAddr) b) b
    let hosts = Hosts ha hb
    atomically $ writeTVar r hosts

defHostCount :: HostMap -> IO Word
defHostCount r = fromIntegral . Map.size . view alive <$> readTVarIO r

defCurrent :: HostMap -> IO [Host]
defCurrent r = Map.elems . view alive <$> readTVarIO r

defOnEvent :: HostMap -> HostEvent -> IO ()
defOnEvent r (HostNew h) = atomically $ do
    m <- readTVar r
    when (isNothing $ get (h^.hostAddr) m) $
        writeTVar r (over alive (Map.insert (h^.hostAddr) h) m)
defOnEvent r (HostGone h) = atomically $ do
    m <- readTVar r
    let a = (h^.hostAddr)
    if Map.member a (m^.alive) then
        writeTVar r (over alive (Map.delete a) m)
    else
        writeTVar r (over other (Map.delete a) m)
defOnEvent r (HostUp h) = atomically $ do
    m <- readTVar r
    let a = (h^.hostAddr)
    case get a m of
        Nothing -> return ()
        Just _ -> writeTVar r
            $ over alive (Map.insert a h)
            . over other (Map.delete a)
            $ m
defOnEvent r (HostDown h) = atomically $ do
    m <- readTVar r
    let a = (h^.hostAddr)
    case get a m of
        Nothing -> return ()
        Just _ -> writeTVar r
            $ over other (Map.insert a h)
            . over alive (Map.delete a)
            $ m
defOnEvent r (AddrDown a) = atomically $ do
    m <- readTVar r
    case get a m of
        Nothing -> return ()
        Just h -> writeTVar r
            $ over other (Map.insert a h)
            . over alive (Map.delete a)
            $ m

get :: InetAddr -> Hosts -> Maybe Host
get a m = Map.lookup a (m^.alive) <|> Map.lookup a (m^.other)