packages feed

ron-0.12: lib/RON/Event.hs

{-# LANGUAGE BinaryLiterals #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE RecordWildCards #-}

module RON.Event (
  CalendarTime (..),
  Event (..),
  Time (..),
  TimeVariety (.., Calendar, Logical, Epoch, Unknown),
  OriginVariety
    (.., TrieForked, CryptoForked, RecordForked, ApplicationSpecific),
  ReplicaClock (..),
  Replica (..),
  advanceToUuid,
  decodeCalendar,
  decodeEvent,
  decodeReplica,
  encodeCalendar,
  encodeEvent,
  getEvent,
  getEventUuid,
  getEventUuids,
  mkCalendarDate,
  mkCalendarDateTime,
  mkCalendarDateTimeNano,
  mkCalendarEvent,
  mkReplica,
  mkTime,
  timeValue,
  timeVariety,
) where

import           RON.Prelude

import           Data.Bits (shiftL, shiftR, (.&.), (.|.))
import qualified Data.ByteString.Char8 as BSC
import           Data.Time (fromGregorianValid, makeTimeOfDayValid)
import qualified Text.Show

import           RON.Base64 (encode60short)
import           RON.Util.Word (pattern B00, pattern B01, pattern B10,
                                pattern B11, Word12, Word2, Word24, Word6,
                                Word60, leastSignificant12, leastSignificant2,
                                leastSignificant24, leastSignificant6, ls12,
                                ls24, ls6, ls60, safeCast)
import           RON.UUID (UUID (..), UuidFields (..))
import qualified RON.UUID as UUID

-- | Calendar format. See https://github.com/gritzko/ron/issues/19.
-- Year range is 2010—2350.
-- Precision is 100 ns.
data CalendarTime = CalendarTime
    { months          :: Word12
    , days            :: Word6
    , hours           :: Word6
    , minutes         :: Word6
    , seconds         :: Word6
    , nanosecHundreds :: Word24
    }
    deriving (Eq, Ord, Show)

newtype TimeVariety = TimeVariety Word2

pattern Calendar :: TimeVariety
pattern Calendar = TimeVariety B00

pattern Logical :: TimeVariety
pattern Logical = TimeVariety B01

-- | RFC 4122 epoch, hundreds of nanoseconds since 1582.
-- Year range is 1582—5235.
pattern Epoch :: TimeVariety
pattern Epoch = TimeVariety B10

pattern Unknown :: TimeVariety
pattern Unknown = TimeVariety B11

{-# COMPLETE Calendar, Logical, Epoch, Unknown #-}

instance Show TimeVariety where
  show = \case
    Calendar -> "Calendar"
    Logical  -> "Logical"
    Epoch    -> "Epoch"
    Unknown  -> "Unknown"

-- | Clock type is encoded in 2 higher bits of variety, value in uuidValue
newtype Time = Time Word64
  deriving (Eq, Ord)

instance Show Time where
  show t =
    show (timeVariety t) ++ '/' : BSC.unpack (encode60short $ timeValue t)

timeVariety :: Time -> TimeVariety
timeVariety (Time w64) = TimeVariety $ leastSignificant2 $ w64 `shiftR` 62

timeValue :: Time -> Word60
timeValue (Time w64) = ls60 w64

-- | Replica id assignment style
newtype OriginVariety = OriginVariety Word2
  deriving newtype (Hashable)

pattern TrieForked :: OriginVariety
pattern TrieForked = OriginVariety B00

pattern CryptoForked :: OriginVariety
pattern CryptoForked = OriginVariety B01

pattern RecordForked :: OriginVariety
pattern RecordForked = OriginVariety B10

pattern ApplicationSpecific :: OriginVariety
pattern ApplicationSpecific = OriginVariety B11

{-# COMPLETE TrieForked, CryptoForked, RecordForked, ApplicationSpecific #-}

instance Show OriginVariety where
  show = \case
    TrieForked          -> "Trie"
    CryptoForked        -> "Crypto"
    RecordForked        -> "Record"
    ApplicationSpecific -> "App"

-- | Replica identifier.
-- Implementation: naming (62-61) and origin (60-0 bits) fields from UUID
newtype Replica = Replica Word64
  deriving newtype (Eq, Hashable, Ord)

instance Show Replica where
  show (Replica w64) =
    show (OriginVariety $ leastSignificant2 $ w64 `shiftR` 60)
    ++ '/' : BSC.unpack (encode60short $ ls60 w64)

-- | Generic Lamport time event.
-- Cannot be 'Ord' because we can't compare different types of clocks.
-- If you want comparable events, use specific 'EpochEvent'.
data Event = Event
  { time    :: !Time
  , replica :: !Replica
  }
  deriving (Eq, Generic, Show)

class Monad m => ReplicaClock m where

    -- | Get current replica id
    getPid :: m Replica

    -- | Get sequential timestamps.
    --
    -- Laws:
    --
    -- 1. @
    --t <- getEvents n
    --(t !! i) == head t + i
    -- @
    --
    -- 2. @
    --t1 <- 'getEvent'
    --t2 <- 'getEvent'
    --t2 >= t1 + 1
    -- @
    --
    -- 3. @getEvents 0 == getEvents 1@
    getEvents
        :: Word60 -- ^ number of needed timestamps
        -> m [Event]

    -- | Make local time not less than this
    advance :: Word60 -> m ()

instance ReplicaClock m => ReplicaClock (ExceptT e m) where
    getPid    = lift   getPid
    getEvents = lift . getEvents
    advance   = lift . advance

instance ReplicaClock m => ReplicaClock (ReaderT r m) where
    getPid    = lift   getPid
    getEvents = lift . getEvents
    advance   = lift . advance

instance ReplicaClock m => ReplicaClock (StateT s m) where
    getPid    = lift   getPid
    getEvents = lift . getEvents
    advance   = lift . advance

instance (Monoid s, ReplicaClock m) => ReplicaClock (WriterT s m) where
    getPid    = lift   getPid
    getEvents = lift . getEvents
    advance   = lift . advance

-- | 'advance' variant for any UUID
advanceToUuid :: ReplicaClock clock => UUID -> clock ()
advanceToUuid uuid =
  when (uuidVariant == B00 && uuidVersion == B10) $ advance uuidValue
  where
    UuidFields{uuidValue, uuidVariant, uuidVersion} = UUID.split uuid

-- | Get a single event
getEvent :: (HasCallStack, ReplicaClock m) => m Event
getEvent = getEvents (ls60 1) >>= \case
    e:_ -> pure e
    []  -> error "getEvents returned no events"

-- | Get a single event as UUID
getEventUuid :: ReplicaClock m => m UUID
getEventUuid = encodeEvent <$> getEvent

-- | Get event sequence as UUIDs
getEventUuids :: ReplicaClock m => Word60 -> m [UUID]
getEventUuids = fmap (map encodeEvent) . getEvents

encodeCalendar :: CalendarTime -> Word60
encodeCalendar CalendarTime{..} = ls60 $
    (safeCast months     `shiftL` 48) .|.
    (safeCast days       `shiftL` 42) .|.
    (safeCast hours      `shiftL` 36) .|.
    (safeCast minutes    `shiftL` 30) .|.
    (safeCast seconds    `shiftL` 24) .|.
    safeCast  nanosecHundreds

decodeCalendar :: Word60 -> CalendarTime
decodeCalendar w = CalendarTime
    { months          = leastSignificant12 $ v `shiftR` 48
    , days            = leastSignificant6  $ v `shiftR` 42
    , hours           = leastSignificant6  $ v `shiftR` 36
    , minutes         = leastSignificant6  $ v `shiftR` 30
    , seconds         = leastSignificant6  $ v `shiftR` 24
    , nanosecHundreds = leastSignificant24   v
    }
  where
    v = safeCast w :: Word64

decodeTime :: Word64 -> Time
decodeTime value = Time $ value .&. 0xCFFFFFFFFFFFFFFF

encodeEvent :: Event -> UUID
encodeEvent Event{time, replica} =
  UUID (varietyAndValue .|. originVariety) (eventVersion .|. origin)
  where
    Time varietyAndValue = time
    (originVariety, origin) = encodeReplicaId replica
    eventVersion = 0x2000000000000000

decodeEvent :: UUID -> Event
decodeEvent u@(UUID x _) = Event{replica = decodeReplica u, time = decodeTime x}

decodeReplica :: UUID -> Replica
decodeReplica (UUID x y) =
  Replica $ (x .&. 0x3000000000000000) .|. (y .&. 0x0FFFFFFFFFFFFFFF)

encodeReplicaId :: Replica -> (Word64, Word64)
encodeReplicaId (Replica r) =
  ( r .&. 0x3000000000000000
  , r .&. 0x0FFFFFFFFFFFFFFF
  )

-- | Make a calendar timestamp from a date
mkCalendarDate
    :: (Word16, Word16, Word8)  -- ^ date as (year, month [1..12], day [1..])
    -> Maybe CalendarTime
mkCalendarDate ymd = mkCalendarDateTime ymd (0, 0, 0)

-- | Make a calendar timestamp from a date and a day time
mkCalendarDateTime
    :: (Word16, Word16, Word8)  -- ^ date as (year, month [1..12], day [1..])
    -> (Word8, Word8, Word8)    -- ^ day time as (hours, minutes, seconds)
    -> Maybe CalendarTime
mkCalendarDateTime ymd hms = mkCalendarDateTimeNano ymd hms 0

-- | Make a calendar timestamp from a date, a day time, and a second fraction
mkCalendarDateTimeNano
    :: (Word16, Word16, Word8)  -- ^ date as (year, month [1..12], day [1..])
    -> (Word8, Word8, Word8)    -- ^ day time as (hours, minutes, seconds)
    -> Word32                   -- ^ fraction of a second in hundreds of
                                -- nanosecond
    -> Maybe CalendarTime
mkCalendarDateTimeNano (y, m, d) (hh, mm, ss) hns = do
    guard $ y >= 2010
    let months = (y - 2010) * 12 + m - 1
    guard $ months < 4096
    _ <- fromGregorianValid (fromIntegral y) (fromIntegral m) (fromIntegral d)
    _ <-
        makeTimeOfDayValid (fromIntegral hh) (fromIntegral mm) (fromIntegral ss)
    guard $ hns < 10000000
    pure CalendarTime
        { months          = ls12 months
        , days            = ls6  $ d - 1
        , hours           = ls6  hh
        , minutes         = ls6  mm
        , seconds         = ls6  ss
        , nanosecHundreds = ls24 hns
        }

-- | Make a replica id from 'OriginVariety' and arbitrary number
mkReplica :: OriginVariety -> Word60 -> Replica
mkReplica (OriginVariety variety) origin =
  Replica $ (safeCast variety `shiftL` 60) .|. safeCast origin

mkTime :: TimeVariety -> Word60 -> Time
mkTime (TimeVariety variety) value =
  Time $ (safeCast variety `shiftL` 62) .|. safeCast value

mkCalendarEvent :: CalendarTime -> Replica -> Event
mkCalendarEvent time replica =
  Event{time = mkTime Calendar $ encodeCalendar time, replica}