packages feed

haskoin-node-2.4.0: src/Haskoin/Node/Peer/Lock.hs

-- |
-- Module      : Haskoin.Node.Peer.Lock
-- Copyright   : (c) Haskoin Developers
-- License     : MIT
-- Maintainer  : jprupp@protonmail.ch
-- Stability   : experimental
-- Portability : POSIX
--
-- Exclusive use of a peer, as an explicit state machine.
--
-- Some conversations with a peer span many messages, such as downloading a
-- long run of headers or blocks. Two tasks holding the same conversation with
-- one peer would confuse each other's replies, so a task reserves the peer
-- first. The header chain does this, and so do applications that download
-- blocks, such as Haskoin Store.
--
-- = Why it works this way
--
-- * __Only the holder can release.__ The lock used to be a flag that anyone
--   could clear. A task that released a peer it did not hold, by mistake or
--   after its own reservation had already ended, would free the peer in the
--   middle of another task's conversation. Here a peer is 'Free' or held by an
--   'Owner', and 'release' only succeeds for the holder.
--
-- * __Pure and polymorphic.__ The transitions are plain functions over any
--   owner type with equality, so the rules can be tested with integers.
--   "Haskoin.Node.Peer" keeps a 'Lock' 'Owner' in each 'Haskoin.Node.Peer.Peer'
--   and applies them atomically.
module Haskoin.Node.Peer.Lock
  ( Lock (..),
    acquire,
    release,
    holder,
    Owner,
    newOwner,
    legacyOwner,
    ownerName,
  )
where

import Data.Function (on)
import Data.Text (Text)
import Data.Unique (Unique, newUnique)

-- | Whether a peer is reserved, and by whom.
data Lock o
  = -- | nobody is using the peer for a long conversation
    Free
  | -- | the owner is using the peer
    HeldBy !o
  deriving stock (Eq, Show)

-- | Reserve a free peer. 'Nothing' if it is held, even by the same owner:
-- reserving twice is a mistake that would hide a missing release.
acquire :: o -> Lock o -> Maybe (Lock o)
acquire o = \case
  Free -> Just (HeldBy o)
  HeldBy _ -> Nothing

-- | Release a peer. 'Nothing' unless the owner holds it.
release :: (Eq o) => o -> Lock o -> Maybe (Lock o)
release o = \case
  HeldBy o' | o == o' -> Just Free
  _ -> Nothing

-- | Who holds the peer, if anyone.
holder :: Lock o -> Maybe o
holder = \case
  Free -> Nothing
  HeldBy o -> Just o

-- | A task that reserves peers. Owners are compared by identity, and named
-- for logs.
data Owner
  = Owner !Unique !Text
  | -- | the owner behind the deprecated 'Haskoin.Node.Peer.setBusy' and
    -- 'Haskoin.Node.Peer.setFree'
    Legacy

instance Eq Owner where
  (==) = (==) `on` key
    where
      key (Owner u _) = Just u
      key Legacy = Nothing

instance Show Owner where
  show = show . ownerName

-- | A new owner with a name for logs. Each call gives a different owner.
newOwner :: Text -> IO Owner
newOwner name = (`Owner` name) <$> newUnique

-- | The single owner shared by every caller of the deprecated
-- 'Haskoin.Node.Peer.setBusy' and 'Haskoin.Node.Peer.setFree'.
legacyOwner :: Owner
legacyOwner = Legacy

-- | Name of an owner.
ownerName :: Owner -> Text
ownerName = \case
  Owner _ name -> name
  Legacy -> "legacy"