packages feed

libraft-0.1.0.0: app/Main.hs

{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Main where

import Protolude hiding
  ( MVar, putMVar, takeMVar, newMVar, newEmptyMVar, readMVar
  , atomically, STM(..), Chan, newTVar, readTVar, writeTVar
  , newChan, writeChan, readChan
  , threadDelay, killThread, TVar(..)
  , catch, handle, takeWhile, takeWhile1, (<|>)
  , lift
  )

import Control.Concurrent.Classy hiding (catch)
import Control.Monad.Catch
import Control.Monad.Trans.Class

import Data.Sequence ((><))
import qualified Data.Map as Map
import qualified Data.List as L
import qualified Data.Set as Set
import qualified Data.Sequence as Seq
import qualified Data.Serialize as S
import qualified Network.Simple.TCP as NS
import Network.Simple.TCP
import qualified Network.Socket as N
import qualified Network.Socket.ByteString as NSB
import Numeric.Natural
import System.Console.Repline
import System.Console.Haskeline.MonadException hiding (handle)
import Text.Read hiding (lift)
import System.Random
import qualified System.Directory as Directory

import qualified Examples.Raft.Socket.Client as RS
import qualified Examples.Raft.Socket.Node as RS
import Examples.Raft.Socket.Node
import qualified Examples.Raft.Socket.Common as RS

import Examples.Raft.FileStore
import Raft

------------------------------
-- State Machine & Commands --
------------------------------

-- State machine with two basic operations: set a variable to a value and
-- increment value

type Var = ByteString

data StoreCmd
  = Set Var Natural
  | Incr Var
  deriving (Show, Generic)

instance S.Serialize StoreCmd

type Store = Map Var Natural

instance RSMP Store StoreCmd where
  data RSMPError Store StoreCmd = StoreError Text deriving (Show)
  type RSMPCtx Store StoreCmd = ()

  applyCmdRSMP _ store cmd =
    Right $ case cmd of
      Set x n -> Map.insert x n store
      Incr x -> Map.adjust succ x store

instance (sm ~ Store, v ~ StoreCmd, RSMP sm v) => RSM sm v (RaftExampleM sm v) where
  validateCmd _ = pure (Right ())
  askRSMPCtx = pure ()

--------------------
-- Raft instances --
--------------------

data NodeEnv sm = NodeEnv
  { nEnvStore :: TVar (STM IO) sm
  , nEnvNodeId :: NodeId
  }

newtype RaftExampleM sm v a = RaftExampleM { unRaftExampleM :: ReaderT (NodeEnv sm) (RaftSocketT v (RaftFileStoreT IO)) a }
  deriving (Functor, Applicative, Monad, MonadIO, MonadReader (NodeEnv sm), Alternative, MonadPlus)

deriving instance MonadThrow (RaftExampleM sm v)
deriving instance MonadCatch (RaftExampleM sm v)
deriving instance MonadMask (RaftExampleM sm v)
deriving instance MonadConc (RaftExampleM sm v)

runRaftExampleM :: NodeEnv sm -> NodeSocketEnv v -> NodeFileStoreEnv -> RaftExampleM sm v a -> IO a
runRaftExampleM nodeEnv nodeSocketEnv nodeFileStoreEnv raftExampleM =
  runReaderT (unRaftFileStoreT $
    runReaderT (unRaftSocketT $
      runReaderT (unRaftExampleM raftExampleM) nodeEnv) nodeSocketEnv)
        nodeFileStoreEnv

instance RaftSendClient (RaftExampleM Store StoreCmd) Store where
  sendClient cid msg = (RaftExampleM . lift) $ sendClient cid msg

instance RaftRecvClient (RaftExampleM Store StoreCmd) StoreCmd where
  type RaftRecvClientError (RaftExampleM Store StoreCmd) StoreCmd = Text
  receiveClient = RaftExampleM $ lift receiveClient

instance RaftSendRPC (RaftExampleM Store StoreCmd) StoreCmd where
  sendRPC nid msg = (RaftExampleM . lift) $ sendRPC nid msg

instance RaftRecvRPC (RaftExampleM Store StoreCmd) StoreCmd where
  type RaftRecvRPCError (RaftExampleM Store StoreCmd) StoreCmd = Text
  receiveRPC = RaftExampleM $ lift receiveRPC

instance RaftWriteLog (RaftExampleM Store StoreCmd) StoreCmd where
  type RaftWriteLogError (RaftExampleM Store StoreCmd) = NodeEnvError
  writeLogEntries entries = RaftExampleM $ lift $ RaftSocketT (lift $ writeLogEntries entries)

instance RaftPersist (RaftExampleM Store StoreCmd) where
  type RaftPersistError (RaftExampleM Store StoreCmd) = NodeEnvError
  writePersistentState ps = RaftExampleM $ lift $ RaftSocketT (lift $ writePersistentState ps)
  readPersistentState = RaftExampleM $ lift $ RaftSocketT (lift $ readPersistentState)

instance RaftReadLog (RaftExampleM Store StoreCmd) StoreCmd where
  type RaftReadLogError (RaftExampleM Store StoreCmd) = NodeEnvError
  readLogEntry idx = RaftExampleM $ lift $ RaftSocketT (lift $ readLogEntry idx)
  readLastLogEntry = RaftExampleM $ lift $ RaftSocketT (lift readLastLogEntry)

instance RaftDeleteLog (RaftExampleM Store StoreCmd) StoreCmd where
  type RaftDeleteLogError (RaftExampleM Store StoreCmd) = NodeEnvError
  deleteLogEntriesFrom idx = RaftExampleM $ lift $ RaftSocketT (lift $ deleteLogEntriesFrom idx)

--------------------
-- Client console --
--------------------

-- Clients interact with the nodes from a terminal:
-- Accepted operations are:
-- - addNode <host:port>
--      Add nodeId to the set of nodeIds that the client will communicate with
-- - getNodes
--      Return the node ids that the client is aware of
-- - read
--      Return the state of the leader
-- - set <var> <val>
--      Set variable to a specific value
-- - incr <var>
--      Increment the value of a variable

data ConsoleState = ConsoleState
  { csNodeIds :: NodeIds -- ^ Set of node ids that the client is aware of
  , csSocket :: Socket -- ^ Client's socket
  , csHost :: HostName -- ^ Client's host
  , csPort :: ServiceName -- ^ Client's port
  , csLeaderId :: TVar (STM IO) (Maybe LeaderId) -- ^ Node id of the leader in the Raft network
  }

newtype ConsoleT m a = ConsoleT
  { unConsoleT :: StateT ConsoleState m a
  } deriving (Functor, Applicative, Monad, MonadIO, MonadState ConsoleState)

newtype ConsoleM a = ConsoleM
  { unConsoleM :: HaskelineT (ConsoleT IO) a
  } deriving (Functor, Applicative, Monad, MonadIO, MonadState ConsoleState)

instance MonadException m => MonadException (ConsoleT m) where
  controlIO f =
    ConsoleT $ StateT $ \s ->
      controlIO $ \(RunIO run) ->
        let run' = RunIO (fmap (ConsoleT . StateT . const) . run . flip runStateT s . unConsoleT)
        in flip runStateT s . unConsoleT <$> f run'

-- | Evaluate and handle each line user inputs
handleConsoleCmd :: [Char] -> ConsoleM ()
handleConsoleCmd input = do
  nids <- gets csNodeIds
  clientSocket <- gets csSocket
  clientHost <- gets csHost
  clientPort <- gets csPort
  leaderIdT <-  gets csLeaderId
  leaderIdM <- liftIO $ atomically $ readTVar leaderIdT
  let clientSocketEnv = RS.ClientSocketEnv clientPort clientHost clientSocket
  case L.words input of
    ["addNode", nid] -> modify (\st -> st { csNodeIds = Set.insert (toS nid) (csNodeIds st) })
    ["getNodes"] -> print nids
    ["read"] -> if nids == Set.empty
      then putText "Please add some nodes to query first. Eg. `addNode localhost:3001`"
      else do
        respE <- liftIO $ RS.runRaftSocketClientM clientSocketEnv $ case leaderIdM of
          Nothing -> RS.sendReadRndNode (Proxy :: Proxy StoreCmd) nids
          Just (LeaderId nid) -> RS.sendRead (Proxy :: Proxy StoreCmd) nid
        handleClientResponseE input respE leaderIdT
    ["incr", cmd] -> do
      respE <- liftIO $ RS.runRaftSocketClientM clientSocketEnv $ case leaderIdM of
        Nothing -> RS.sendWriteRndNode (Incr (toS cmd)) nids
        Just (LeaderId nid) -> RS.sendWrite (Incr (toS cmd)) nid
      handleClientResponseE input respE leaderIdT
    ["set", var, val] -> do
      respE <- liftIO $ RS.runRaftSocketClientM clientSocketEnv $ case leaderIdM of
        Nothing -> RS.sendWriteRndNode (Set (toS var) (read val)) nids
        Just (LeaderId nid) -> RS.sendWrite (Set (toS var) (read val)) nid
      handleClientResponseE input respE leaderIdT
    _ -> print "Invalid command. Press <TAB> to see valid commands"

  where
    handleClientResponseE :: [Char] -> Either [Char] (ClientResponse Store) -> TVar (STM IO) (Maybe LeaderId) -> ConsoleM ()
    handleClientResponseE input eMsgE leaderIdT =
      case eMsgE of
        Left err -> panic $ toS err
        Right (ClientRedirectResponse (ClientRedirResp leader)) ->
          case leader of
            NoLeader -> do
              putText "Sorry, the system doesn't have a leader at the moment"
              liftIO $ atomically $ writeTVar leaderIdT Nothing
            -- If the message was not sent to the leader, that node will
            -- point to the current leader
            CurrentLeader lid -> do
              putText $ "New leader found: " <> show lid
              liftIO $ atomically $ writeTVar leaderIdT (Just lid)
              handleConsoleCmd input
        Right (ClientReadResponse (ClientReadResp sm)) -> putText $ "Received sm: " <> show sm
        Right (ClientWriteResponse writeResp) -> print writeResp


main :: IO ()
main = do
    args <- (toS <$>) <$> getArgs
    case args of
      ["client"] -> clientMainHandler
      (nid:nids) -> do
        removeExampleFiles nid
        createExampleFiles nid

        nSocketEnv <- initSocketEnv nid
        nPersistentEnv <- initRaftFileStoreEnv nid
        nEnv <- initNodeEnv nid
        runRaftExampleM nEnv nSocketEnv nPersistentEnv $ do
          let allNodeIds = Set.fromList (nid : nids)
          let nodeConfig = NodeConfig
                            { configNodeId = toS nid
                            , configNodeIds = allNodeIds
                            , configElectionTimeout = (1500000, 3000000)
                            , configHeartbeatTimeout = 200000
                            }
          RaftExampleM $ lift acceptForkNode :: RaftExampleM Store StoreCmd ()
          electionTimerSeed <- liftIO randomIO
          runRaftNode nodeConfig LogStdout electionTimerSeed (mempty :: Store)
  where
    initPersistentFile :: NodeId -> IO ()
    initPersistentFile nid = do
      psPath <- persistentFile nid
      writeFile psPath (toS $ S.encode initPersistentState)

    persistentFile :: NodeId -> IO FilePath
    persistentFile nid = do
      tmpDir <- Directory.getTemporaryDirectory
      pure $ tmpDir ++ "/" ++ toS nid ++ "/" ++ "persistent"

    initLogsFile :: NodeId -> IO ()
    initLogsFile nid = do
      logsPath <- logsFile nid
      writeFile logsPath (toS $ S.encode (mempty :: Entries StoreCmd))

    logsFile :: NodeId -> IO FilePath
    logsFile nid = do
      tmpDir <- Directory.getTemporaryDirectory
      pure (tmpDir ++ "/" ++ toS nid ++ "/" ++ "logs")

    createExampleFiles :: NodeId -> IO ()
    createExampleFiles nid = void $ do
      tmpDir <- Directory.getTemporaryDirectory
      Directory.createDirectory (tmpDir ++ "/" ++ toS nid)
      initPersistentFile nid
      initLogsFile nid

    removeExampleFiles :: NodeId -> IO ()
    removeExampleFiles nid = handle (const (pure ()) :: SomeException -> IO ()) $ do
      tmpDir <- Directory.getTemporaryDirectory
      Directory.removeDirectoryRecursive (tmpDir ++ "/" ++ toS nid)


    initNodeEnv :: NodeId -> IO (NodeEnv Store)
    initNodeEnv nid = do
      let (host, port) = RS.nidToHostPort (toS nid)
      storeTVar <- atomically (newTVar mempty)
      pure NodeEnv
        { nEnvStore = storeTVar
        , nEnvNodeId = toS host <> ":" <> toS port
        }

    initRaftFileStoreEnv :: NodeId -> IO NodeFileStoreEnv
    initRaftFileStoreEnv nid = do
      psPath <- persistentFile nid
      psLogs <- logsFile nid
      pure NodeFileStoreEnv
            { nfsPersistentState = psPath
            , nfsLogEntries = psLogs
            }

    initSocketEnv :: NodeId -> IO (NodeSocketEnv v)
    initSocketEnv nid = do
      let (host, port) = RS.nidToHostPort (toS nid)
      nodeSocket <- newSock host port
      socketPeersTVar <- atomically (newTVar mempty)
      msgQueue <- atomically newTChan
      clientReqQueue <- atomically newTChan
      pure NodeSocketEnv
            { nsPeers = socketPeersTVar
            , nsSocket = nodeSocket
            , nsMsgQueue = msgQueue
            , nsClientReqQueue = clientReqQueue
            }

    clientMainHandler :: IO ()
    clientMainHandler = do
      clientPort <- RS.getFreePort
      clientSocket <- RS.newSock "localhost" clientPort
      leaderIdT <- atomically (newTVar Nothing)
      let initClientState = ConsoleState
                                  { csNodeIds = mempty
                                  , csSocket = clientSocket
                                  , csHost = "localhost"
                                  , csPort = clientPort
                                  , csLeaderId = leaderIdT
                                  }
      runConsoleT initClientState $
        evalRepl (pure ">>> ") (unConsoleM . handleConsoleCmd) [] Nothing (Word completer) (pure ())

    runConsoleT :: Monad m => ConsoleState -> ConsoleT m a -> m a
    runConsoleT consoleState = flip evalStateT consoleState . unConsoleT



    -- Tab Completion: return a completion for partial words entered
    completer :: Monad m => WordCompleter m
    completer n = do
      let cmds = ["addNode <host:port>", "getNodes", "incr <var>", "set <var> <val>"]
      return $ filter (isPrefixOf n) cmds