nqe-0.6.9: src/Control/Concurrent/NQE/Publisher.hs
{-# LANGUAGE FlexibleContexts #-}
{-|
Module : Control.Concurrent.NQE.Publisher
Copyright : Haskoin Developers
License : MIT
Maintainer : jprupp@protonmail.ch
Stability : experimental
Portability : POSIX
A publisher is a process that forwards messages to subscribers. NQE publishers
are simple, and do not implement filtering directly, although that can be done
on the 'STM' 'Listen' actions that forward messages to subscribers.
If a subscriber has been added to a publisher using the 'subscribe' function, it
needs to be removed later using 'unsubscribe' when it is no longer needed, or
the publisher will continue calling its 'Listen' action in the future, likely
causing memory leaks. Prefer 'withSubscription', which unsubscribes
automatically.
= Why a publisher process
Broadcasting is common: many parts of a program want to hear about the same
events, such as new blocks or connected peers, and the code producing them
should not need to know who is listening. A publisher decouples the two. The
producer sends each event once; subscribers come and go without the producer
noticing.
= Design choices
* __Subscribers are 'Listen' actions, not inboxes.__ That leaves every policy
decision to the subscriber: it can filter events, transform them, drop them
when its queue is full, or write them anywhere an 'STM' action can.
* __Delivery is a single transaction.__ Each event is handed to all current
subscribers in one 'STM' transaction, so they all see the same events in the
same order. The cost is that one slow 'Listen' action slows the whole
publisher, which is why those actions must be quick and must not block.
* __The loop is flat.__ The publisher loop sequences with @>>=@ rather than
'Control.Monad.forever', so that it runs in constant memory even in monads
like @LoggingT@, where @forever@ leaks.
= Example
@
'withPublisher' $ \\pub ->
'withSubscription' pub $ \\inbox -> do
'publish' "hello" pub
msg <- 'receive' inbox
print msg
@
-}
module Control.Concurrent.NQE.Publisher
( Subscriber
, PublisherMessage(..)
, Publisher
, withSubscription
, subscribe
, unsubscribe
, withPublisher
, publisher
, publisherProcess
, publish
, publishSTM
) where
import Control.Concurrent.NQE.Process
import Control.Concurrent.Unique
import Control.Monad
import Control.Monad.Reader
import Data.Function
import Data.Hashable
import Data.List
import UnliftIO
-- | Handle of a subscriber to a process. Should be kept in order to
-- unsubscribe. Subscribers are compared by a 'Unique' identifier assigned by the
-- publisher when they subscribe.
data Subscriber msg = Subscriber (Listen msg) Unique
instance Eq (Subscriber msg) where
(==) = (==) `on` f
where
f (Subscriber _ u) = u
instance Hashable (Subscriber msg) where
hashWithSalt i (Subscriber _ u) = hashWithSalt i u
-- | Messages that a publisher will take. Normally these are not built
-- directly; use 'subscribe', 'unsubscribe' and 'publish' instead.
data PublisherMessage msg
= Subscribe !(Listen msg) !(Listen (Subscriber msg))
-- ^ add a 'Listen' action to the subscriber list, and reply with the
-- resulting 'Subscriber' handle
| Unsubscribe !(Subscriber msg)
-- ^ remove a subscriber from the list
| Event msg
-- ^ forward a message to every subscriber
-- | Alias for a publisher process. Since it is a 'Process', it can be stopped
-- by cancelling its 'getProcessAsync' handle.
type Publisher msg = Process (PublisherMessage msg)
-- | Send a message to all subscribers of a publisher. The message is queued in
-- the publisher's mailbox and forwarded asynchronously.
publish :: MonadIO m => msg -> Publisher msg -> m ()
publish = send . Event
-- | 'STM' version of 'publish'. Useful to publish a message atomically together
-- with other 'STM' actions.
publishSTM :: msg -> Publisher msg -> STM ()
publishSTM = sendSTM . Event
-- | Create a mailbox, subscribe it to a publisher and pass it to the supplied
-- function. End subscription when function returns, even if it throws an
-- exception. The inbox is unbounded, so the caller should consume messages from
-- it regularly to avoid unbounded memory growth.
--
-- This is the safe way to subscribe, because the subscription cannot outlive
-- the scope. For a bounded or filtered subscription, write your own bracket
-- around 'subscribe' and 'unsubscribe' with your own 'Listen' action.
withSubscription ::
MonadUnliftIO m => Publisher msg -> (Inbox msg -> m a) -> m a
withSubscription pub f = do
inbox <- newInbox
let sub = subscribe pub (`sendSTM` inbox)
unsub = unsubscribe pub
bracket sub unsub $ \_ -> f inbox
-- | 'Listen' to events from a publisher. Blocks until the publisher has
-- registered the subscriber, and returns a handle that must later be passed to
-- 'unsubscribe'. The 'Listen' action runs inside the publisher's 'STM'
-- transaction for every event, so it should be fast and must not retry.
subscribe :: MonadIO m => Publisher msg -> Listen msg -> m (Subscriber msg)
subscribe pub sub = Subscribe sub `query` pub
-- | Stop listening to events from a publisher. Must provide 'Subscriber' that
-- was returned from corresponding 'subscribe' action.
unsubscribe :: MonadIO m => Publisher msg -> Subscriber msg -> m ()
unsubscribe pub sub = Unsubscribe sub `send` pub
-- | Start a publisher in the background and pass it to a function. The
-- publisher will be stopped when the function function returns.
withPublisher :: MonadUnliftIO m => (Publisher msg -> m a) -> m a
withPublisher = withProcess publisherProcess
-- | Start a publisher in the background. It must be stopped manually by
-- cancelling 'getProcessAsync'. Prefer 'withPublisher' where possible.
publisher :: MonadUnliftIO m => m (Publisher msg)
publisher = process publisherProcess
-- | Start a publisher in the current thread. Runs forever, processing
-- 'PublisherMessage' values from the inbox.
publisherProcess :: MonadUnliftIO m => Inbox (PublisherMessage msg) -> m ()
publisherProcess inbox = newTVarIO [] >>= runReaderT go
where
-- Sequenced with (>>=) only: 'forever' uses (*>), which for a monad
-- that is only known through its dictionary (LoggingT in particular)
-- leaves a continuation frame and two thunks per message, forever.
go = receive inbox >>= publisherMessage >>= \_ -> go
-- | Internal function to dispatch a publisher message.
publisherMessage ::
(MonadIO m, MonadReader (TVar [Subscriber msg]) m)
=> PublisherMessage msg
-> m ()
publisherMessage (Subscribe sub r) =
ask >>= \box -> do
u <- liftIO newUnique
let s = Subscriber sub u
atomically $ do
modifyTVar box (`union` [s])
r s
publisherMessage (Unsubscribe sub) =
ask >>= \box -> atomically (modifyTVar box (delete sub))
publisherMessage (Event event) =
ask >>= \box ->
atomically $
readTVar box >>= \subs ->
forM_ subs $ \(Subscriber sub _) -> sub event