cachix-1.9.2: src/Cachix/Daemon/Subscription.hs
module Cachix.Daemon.Subscription
( SubscriptionManager (..),
Subscription (..),
newSubscriptionManager,
subscribeTo,
subscribeToAll,
subscribeToSTM,
subscribeToAllSTM,
queueUnsubscribe,
getSubscriptionsFor,
getSubscriptionsForSTM,
pushEvent,
pushEventSTM,
runSubscriptionManager,
stopSubscriptionManager,
)
where
import Control.Concurrent.STM.TBMQueue
import Control.Concurrent.STM.TMChan
import Control.Concurrent.STM.TVar
import Data.Aeson as Aeson (ToJSON)
import Data.HashMap.Strict (HashMap)
import Data.HashMap.Strict qualified as HashMap
import Data.Sequence qualified as Seq
import Network.Socket qualified as Socket
import Protolude
data SubscriptionManager k v = SubscriptionManager
{ managerSubscriptions :: TVar (HashMap k (Seq.Seq (Subscription v))),
managerGlobalSubscriptions :: TVar (Seq.Seq (Subscription v)),
managerMessages :: TBMQueue (ManagerMessage k v)
}
data ManagerMessage k v
= -- | An event to be dispatched to subscribers
EventMessage k v
| -- | Close subscriptions for a key (processed in order with events)
UnsubscribeMessage k
data Subscription v
= -- | A subscriber that listens on a socket.
SubSocket (TBMQueue v) Socket.Socket
| -- | A subscriber that listens on a channel.
SubChannel (TMChan v)
newSubscriptionManager :: IO (SubscriptionManager k v)
newSubscriptionManager = do
subscriptions <- newTVarIO HashMap.empty
globalSubscriptions <- newTVarIO Seq.empty
messages <- newTBMQueueIO 10000
pure $ SubscriptionManager subscriptions globalSubscriptions messages
-- Subscriptions
subscribeTo :: (Hashable k, MonadIO m) => SubscriptionManager k v -> k -> Subscription v -> m ()
subscribeTo manager key subscription =
liftIO $ atomically $ subscribeToSTM manager key subscription
subscribeToAll :: (MonadIO m) => SubscriptionManager k v -> Subscription v -> m ()
subscribeToAll manager subscription =
liftIO $ atomically $ subscribeToAllSTM manager subscription
getSubscriptionsFor :: (Hashable k, MonadIO m) => SubscriptionManager k v -> k -> m (Seq.Seq (Subscription v))
getSubscriptionsFor manager key =
liftIO $ atomically $ getSubscriptionsForSTM manager key
subscribeToSTM :: (Hashable k) => SubscriptionManager k v -> k -> Subscription v -> STM ()
subscribeToSTM manager key subscription = do
subscriptions <- readTVar $ managerSubscriptions manager
let subscriptions' = HashMap.insertWith (<>) key (Seq.singleton subscription) subscriptions
writeTVar (managerSubscriptions manager) subscriptions'
subscribeToAllSTM :: SubscriptionManager k v -> Subscription v -> STM ()
subscribeToAllSTM manager subscription = do
subscriptions <- readTVar $ managerGlobalSubscriptions manager
let subscriptions' = subscription Seq.<| subscriptions
writeTVar (managerGlobalSubscriptions manager) subscriptions'
getSubscriptionsForSTM :: (Hashable k) => SubscriptionManager k v -> k -> STM (Seq.Seq (Subscription v))
getSubscriptionsForSTM manager key = do
subscriptions <- readTVar $ managerSubscriptions manager
pure $ HashMap.lookupDefault Seq.empty key subscriptions
-- | Queue an unsubscribe command to close all subscription channels for a key.
-- This is processed in order with events, ensuring pending events are delivered first.
queueUnsubscribe :: (MonadIO m) => SubscriptionManager k v -> k -> m ()
queueUnsubscribe manager key =
liftIO $ atomically $ writeTBMQueue (managerMessages manager) (UnsubscribeMessage key)
-- Events
pushEvent :: (MonadIO m) => SubscriptionManager k v -> k -> v -> m ()
pushEvent manager key event =
liftIO $ atomically $ pushEventSTM manager key event
pushEventSTM :: SubscriptionManager k v -> k -> v -> STM ()
pushEventSTM manager key event =
writeTBMQueue (managerMessages manager) (EventMessage key event)
sendEventToSub :: Subscription v -> v -> STM ()
-- TODO: implement socket subscriptions.
sendEventToSub (SubSocket _queue _) _ = pure () -- writeTBMQueue queue
sendEventToSub (SubChannel chan) event = writeTMChan chan event
runSubscriptionManager :: (Show k, Show v, Hashable k, ToJSON v, MonadIO m) => SubscriptionManager k v -> m ()
runSubscriptionManager manager = do
isDone <- liftIO $ atomically $ do
mmsg <- readTBMQueue (managerMessages manager)
case mmsg of
Nothing -> return True
Just (EventMessage key event) -> do
subscriptions <- getSubscriptionsForSTM manager key
globalSubscriptions <- readTVar $ managerGlobalSubscriptions manager
mapM_ (`sendEventToSub` event) (subscriptions <> globalSubscriptions)
return False
Just (UnsubscribeMessage key) -> do
subscriptions <- readTVar $ managerSubscriptions manager
case HashMap.lookup key subscriptions of
Nothing -> return ()
Just subs -> do
forM_ subs $ \case
SubSocket queue _ -> closeTBMQueue queue
SubChannel chan -> closeTMChan chan
writeTVar (managerSubscriptions manager) $ HashMap.delete key subscriptions
return False
unless isDone $
runSubscriptionManager manager
stopSubscriptionManager :: SubscriptionManager k v -> IO ()
stopSubscriptionManager manager = do
liftIO $ atomically $ closeTBMQueue (managerMessages manager)
globalSubscriptions <- liftIO $ readTVarIO $ managerGlobalSubscriptions manager
subscriptions <- liftIO $ readTVarIO $ managerSubscriptions manager
forM_ (fold subscriptions <> globalSubscriptions) $ \case
SubSocket queue sock -> do
atomically $ closeTBMQueue queue
Socket.close sock
SubChannel channel -> atomically $ closeTMChan channel