crdt-7.0: lib/CRDT/LamportClock.hs
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE StrictData #-}
module CRDT.LamportClock
( Pid (..)
-- * Lamport timestamp (for a single process)
, Clock (..)
, LamportTime (..)
, LocalTime
, Process (..)
-- * Real Lamport clock
, LamportClock
, runLamportClock
-- * Helpers
, getRealLocalTime
) where
import Control.Concurrent.STM (TVar, atomically, modifyTVar',
readTVar, writeTVar)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.Reader (ReaderT, ask, runReaderT)
import Control.Monad.Trans (lift)
import Data.Binary (decode)
import qualified Data.ByteString.Lazy as BSL
import Data.Time.Clock.POSIX (getPOSIXTime)
import Data.Word (Word64)
import Network.Info (MAC (MAC), getNetworkInterfaces, mac)
import Numeric.Natural (Natural)
import Safe (headDef)
-- | Unix time in 10^{-7} seconds (100 ns), as in RFC 4122 and Swarm RON.
type LocalTime = Natural
data LamportTime = LamportTime LocalTime Pid
deriving (Eq, Ord)
instance Show LamportTime where
show (LamportTime time (Pid pid)) = show time ++ '.' : show pid
-- | Unique process identifier
newtype Pid = Pid Word64
deriving (Eq, Ord, Show)
class Monad m => Process m where
getPid :: m Pid
getRealLocalTime :: IO LocalTime
getRealLocalTime = round . (* 10000000) <$> getPOSIXTime
getPidByMac :: IO Pid
getPidByMac = Pid . decodeMac <$> getMac
where
getMac :: IO MAC
getMac =
headDef (error "Can't get any non-zero MAC address of this machine")
. filter (/= minBound)
. map mac
<$> getNetworkInterfaces
decodeMac :: MAC -> Word64
decodeMac (MAC b5 b4 b3 b2 b1 b0) =
decode $ BSL.pack [0, 0, b5, b4, b3, b2, b1, b0]
class Process m => Clock m where
getTime :: m LamportTime
advance :: LocalTime -> m ()
-- TODO(cblp, 2018-01-06) benchmark and compare with 'atomicModifyIORef'
newtype LamportClock a = LamportClock (ReaderT (TVar LocalTime) IO a)
deriving (Applicative, Functor, Monad, MonadIO)
runLamportClock :: TVar LocalTime -> LamportClock a -> IO a
runLamportClock var (LamportClock action) = runReaderT action var
instance Process LamportClock where
getPid = liftIO getPidByMac
instance Clock LamportClock where
advance time = LamportClock $ do
timeVar <- ask
lift $ atomically $ modifyTVar' timeVar $ max time
getTime = LamportClock $ do
timeVar <- ask
lift $ do
realTime <- getRealLocalTime
time1 <- atomically $ do
time0 <- readTVar timeVar
let time1 = max realTime (time0 + 1)
writeTVar timeVar time1
pure time1
pid <- getPidByMac
pure $ LamportTime time1 pid
instance Process m => Process (ReaderT r m) where
getPid = lift getPid
instance Clock m => Clock (ReaderT r m) where
advance = lift . advance
getTime = lift getTime