ron-0.13: lib/RON/Event.hs
{-# LANGUAGE BinaryLiterals #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE NumericUnderscores #-}
{-# 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,
isEventUuid,
mkCalendarDate,
mkCalendarDateTime,
mkCalendarDateTimeNano,
mkCalendarEvent,
mkReplica,
mkTime,
timeValue,
timeVariety,
unsafeDecodeEvent,
) where
import RON.Prelude
import Data.Bits (shiftL, shiftR, (.&.), (.|.))
import Data.ByteString.Char8 qualified as BSC
import Data.Time (fromGregorianValid, makeTimeOfDayValid)
import Text.Show qualified
import RON.Base64 (encode60short)
import RON.UUID (UUID (..), UuidFields (..))
import RON.UUID qualified as UUID
import RON.Util.Word (
Word12,
Word2,
Word24,
Word6,
Word60,
leastSignificant12,
leastSignificant2,
leastSignificant24,
leastSignificant6,
ls12,
ls24,
ls6,
ls60,
safeCast,
pattern B00,
pattern B01,
pattern B10,
pattern B11,
)
{- | 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 (Eq, 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 ::
-- | number of needed timestamps
Word60 ->
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
isEventUuid :: UUID -> Bool
isEventUuid uuid =
uuidVariant == B00 && (uuidVersion == B10 || uuidVersion == B11)
where
UuidFields{uuidVariant, uuidVersion} = UUID.split uuid
-- | 'advance' variant for any UUID, but works only for event UUIDs
advanceToUuid :: (ReplicaClock clock) => UUID -> clock ()
advanceToUuid uuid = when (isEventUuid uuid) $ advance uuidValue
where
UuidFields{uuidValue} = UUID.split uuid
-- | Get a single event
getEvent :: (HasCallStack, ReplicaClock m) => m Event
getEvent =
getEvents 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 .&. 0x_CFFF_FFFF_FFFF_FFFF
encodeEvent :: Event -> UUID
encodeEvent Event{time, replica} =
UUID (varietyAndValue .|. originVariety) (eventVersion .|. origin)
where
Time varietyAndValue = time
(originVariety, origin) = encodeReplicaId replica
eventVersion = 0x_2000_0000_0000_0000
-- | Assume UUID is event
decodeEvent :: UUID -> Maybe Event
decodeEvent u = guard (isEventUuid u) $> unsafeDecodeEvent u
unsafeDecodeEvent :: UUID -> Event
unsafeDecodeEvent u@(UUID x _) =
Event{replica = decodeReplica u, time = decodeTime x}
decodeReplica :: UUID -> Replica
decodeReplica (UUID x y) =
Replica $ (x .&. 0x_3000_0000_0000_0000) .|. (y .&. 0x_0FFF_FFFF_FFFF_FFFF)
encodeReplicaId :: Replica -> (Word64, Word64)
encodeReplicaId (Replica r) =
( r .&. 0x_3000_0000_0000_0000
, r .&. 0x_0FFF_FFFF_FFFF_FFFF
)
-- | Make a calendar timestamp from a date
mkCalendarDate ::
-- | date as (year, month [1..12], day [1..])
(Word16, Word16, Word8) ->
Maybe CalendarTime
mkCalendarDate ymd = mkCalendarDateTime ymd (0, 0, 0)
-- | Make a calendar timestamp from a date and a day time
mkCalendarDateTime ::
-- | date as (year, month [1..12], day [1..])
(Word16, Word16, Word8) ->
-- | day time as (hours, minutes, seconds)
(Word8, Word8, Word8) ->
Maybe CalendarTime
mkCalendarDateTime ymd hms = mkCalendarDateTimeNano ymd hms 0
-- | Make a calendar timestamp from a date, a day time, and a second fraction
mkCalendarDateTimeNano ::
-- | date as (year, month [1..12], day [1..])
(Word16, Word16, Word8) ->
-- | day time as (hours, minutes, seconds)
(Word8, Word8, Word8) ->
-- | fraction of a second in hundreds of nanosecond
Word32 ->
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 < 10_000_000
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}