packages feed

zoovisitor-0.2.3.0: src/ZooKeeper/Recipe/Election.hs

module ZooKeeper.Recipe.Election
  ( election
  ) where

import           Control.Exception      (catch, throwIO)
import           Control.Monad
import           Z.Data.CBytes          (CBytes)

import           ZooKeeper
import           ZooKeeper.Exception    (ZNODEEXISTS)
import           ZooKeeper.Recipe.Utils (ZkRecipeException (..),
                                         SequenceNumWithGUID (..),
                                         createSeqEphemeralZNode,
                                         mkSequenceNumWithGUID)
import           ZooKeeper.Types

-- | Run a leader election process.
-- __IMPORTANT__: This function may run endlessly until it is selected
-- as the leader.
election :: ZHandle
         -- ^ The zookeeper handle obtained by a call to 'zookeeperResInit'
         -> CBytes
         -- ^ The path to start the election from. Ephemeral znodes will be
         -- put on it
         -> CBytes
         -- ^ The GUID for this zookeeper session. To handle recoverable execptions
         -- correctly, it should be distinct from different sessions.
         -> IO ()
         -- ^ The action to be executed when an leader is elected.
         -> (DataCompletion -> IO ())
         -- ^ The action to be executed when a watcher is set. It can be used to
         -- remind the user that one 'step' is finished.
         -> IO ()
-- TODO: Use user-configurable logger instead
election zk electionPath guid leaderApp watchSetApp = do
  let electionSeqPath = electionPath <> "/" <> guid <> "_"

  -- Check persistent paths
  do electionExists <- zooExists zk electionPath
     case electionExists of
       Just _  -> return ()
       Nothing -> void $ zooCreate zk electionPath Nothing zooOpenAclUnsafe ZooPersistent
     `catch` (\(_ :: ZNODEEXISTS) -> return ())

  -- Create Ephemeral and Sequece znode, and get the seq number i
  (StringCompletion this) <- createSeqEphemeralZNode zk electionPath guid
  let thisSeqNumWithGUID = mkSequenceNumWithGUID this
  -- TODO: Use zookeeper log
  -- Log.debug . B.stringUTF8 $ "Created SEQUENTIAL|EPHEMERAL ZNode " <> show thisSeqNumWithGUID

  -- Get the child that has the max seq number j < i
  (StringsCompletion (StringVector children)) <- zooGetChildren zk electionPath
  let childrenSeqNumWithGUID = mkSequenceNumWithGUID <$> children
  -- TODO: Use zookeeper log
  -- Log.debug . B.stringUTF8 $ "Children now: " <> show childrenSeqNumWithGUID

  -- find max j < i
  case filter (< thisSeqNumWithGUID) childrenSeqNumWithGUID of
    [] -> do
      let _smallest = minimum childrenSeqNumWithGUID
      -- TODO: Use zookeeper log
      -- Log.debug . B.stringUTF8 $ "Leader elected: " <> show smallest
      leaderApp
    xs -> do
      let toWatch = electionPath <> "/" <> unSequenceNumWithGUID (maximum xs)
      -- TODO: Use zookeeper log
      -- Log.debug . B.stringUTF8 $ "Now watching: " <> show toWatch
      -- add watch
      zooWatchGet zk toWatch (callback electionSeqPath thisSeqNumWithGUID) watchSetApp
  where
    callback electionSeqPath thisSeqNumWithGUID HsWatcherCtx{..} = do
      -- TODO: Use zookeeper log
      --Log.debug . B.stringUTF8 $ "Watch triggered, some node failed."
      (StringsCompletion (StringVector children)) <- zooGetChildren watcherCtxZHandle electionPath
      let childrenSeqNumWithGUID = mkSequenceNumWithGUID <$> children
      let smallest = minimum childrenSeqNumWithGUID
      if smallest == thisSeqNumWithGUID
         then do
           -- TODO: Use zookeeper log
           --Log.debug . B.stringUTF8 $ "Leader elected: " <> show smallest
           leaderApp
         else do
           -- find max j < i
           case filter (< thisSeqNumWithGUID) childrenSeqNumWithGUID of
             [] -> throwIO $ ZkRecipeException "The 'impossible' happened!"
             xs -> do
               let toWatch = electionPath <> "/" <> unSequenceNumWithGUID (maximum xs )
               -- Log.debug . B.stringUTF8 $ "Now watching: " <> show toWatch
               -- add watch
               zooWatchGet zk toWatch (callback electionSeqPath thisSeqNumWithGUID) watchSetApp