natskell-1.1.0.0: test/Unit/SubscriptionStoreSpec.hs
{-# LANGUAGE OverloadedStrings #-}
module SubscriptionStoreSpec (spec) where
import Control.Concurrent
import Control.Concurrent.STM
import Control.Exception
import Control.Monad
import Data.IORef
import Subscription.Store
import Subscription.Types
import Test.Hspec
import Types.Msg
spec :: Spec
spec = do
describe "terminal cleanup" $ do
it "clears active and expiry state while preserving queued callbacks" $ do
delivered <- newEmptyMVar
stopping <- newTVarIO False
store <- newSubscriptionStore defaultPendingLimits (pure ())
register
store
"1"
(SubscriptionMeta "A" Nothing StandardSubscription)
(SubscribeConfig Nothing Nothing)
(const (putMVar delivered ()))
(pure ())
register
store
"2"
(SubscriptionMeta "B" Nothing OneShotSubscription)
(SubscribeConfig (Just 60) Nothing)
(const (pure ()))
(pure ())
dispatchMessage store (message "1" "one") `shouldReturn` DispatchQueued
closeStore store
active store `shouldReturn` []
hasTrackedExpiries store `shouldReturn` False
_ <- startWorkers
1
store
(readTVar stopping >>= check)
(const (pure ()))
takeMVar delivered
atomically (awaitCallbackDrain store)
atomically (writeTVar stopping True)
describe "global pending delivery limits" $ do
it "shares the message limit across subscriptions and coalesces notification" $ do
slowEvents <- newIORef (0 :: Int)
firstOverflow <- newIORef (0 :: Int)
secondOverflow <- newIORef (0 :: Int)
store <-
newSubscriptionStore
(PendingLimits 1 1024)
(modifyIORef' slowEvents (+ 1))
registerSubscription store "1" "A" (modifyIORef' firstOverflow (+ 1))
registerSubscription store "2" "B" (modifyIORef' secondOverflow (+ 1))
dispatchMessage store (message "1" "one")
`shouldReturn` DispatchQueued
dispatchMessage store (message "2" "two")
`shouldReturn` DispatchDropped True
dispatchMessage store (message "2" "three")
`shouldReturn` DispatchDropped False
readIORef slowEvents `shouldReturn` 0
readIORef firstOverflow `shouldReturn` 0
readIORef secondOverflow `shouldReturn` 2
it "rejects a delivery that exceeds the global byte limit" $ do
store <- newSubscriptionStore (PendingLimits 10 4) (pure ())
registerSubscription store "1" "A" (pure ())
dispatchMessage store (message "1" "12345")
`shouldReturn` DispatchDropped True
it "re-enables notification after the backlog recovers" $ do
slowEvents <- newIORef (0 :: Int)
callbackStarted <- newEmptyMVar
releaseCallback <- newEmptyMVar
stopping <- newTVarIO False
store <-
newSubscriptionStore
(PendingLimits 1 1024)
(modifyIORef' slowEvents (+ 1))
register
store
"1"
(SubscriptionMeta "A" Nothing StandardSubscription)
(SubscribeConfig Nothing Nothing)
(\_ -> putMVar callbackStarted () >> takeMVar releaseCallback)
(pure ())
startWorkers
1
store
(readTVar stopping >>= check)
(const (pure ()))
dispatchMessage store (message "1" "one")
`shouldReturn` DispatchQueued
takeMVar callbackStarted
dispatchMessage store (message "1" "two")
`shouldReturn` DispatchDropped True
putMVar releaseCallback ()
atomically (awaitCallbackDrain store)
readIORef slowEvents `shouldReturn` 1
dispatchMessage store (message "1" "three")
`shouldReturn` DispatchQueued
takeMVar callbackStarted
dispatchMessage store (message "1" "four")
`shouldReturn` DispatchDropped True
putMVar releaseCallback ()
atomically (awaitCallbackDrain store)
readIORef slowEvents `shouldReturn` 2
atomically (writeTVar stopping True)
it "releases capacity when a callback throws" $ do
callbackFailed <- newEmptyMVar
stopping <- newTVarIO False
store <- newSubscriptionStore (PendingLimits 1 1024) (pure ())
register
store
"1"
(SubscriptionMeta "A" Nothing StandardSubscription)
(SubscribeConfig Nothing Nothing)
(const (throwIO (userError "callback failed")))
(pure ())
startWorkers
1
store
(readTVar stopping >>= check)
(putMVar callbackFailed)
dispatchMessage store (message "1" "one")
`shouldReturn` DispatchQueued
void (takeMVar callbackFailed)
atomically (awaitCallbackDrain store)
dispatchMessage store (message "1" "two")
`shouldReturn` DispatchQueued
void (takeMVar callbackFailed)
atomically (awaitCallbackDrain store)
atomically (writeTVar stopping True)
it "removes a one-shot subscription whose delivery is dropped" $ do
store <- newSubscriptionStore (PendingLimits 1 1024) (pure ())
registerSubscription store "1" "A" (pure ())
register
store
"2"
(SubscriptionMeta "B" Nothing OneShotSubscription)
(SubscribeConfig Nothing Nothing)
(const (pure ()))
(pure ())
dispatchMessage store (message "1" "one")
`shouldReturn` DispatchQueued
dispatchMessage store (message "2" "two")
`shouldReturn` DispatchDropped True
dispatchMessage store (message "2" "two")
`shouldReturn` DispatchMissing
it "accepts a dropped one-shot before reporting overflow" $ do
accepted <- newTVarIO (0 :: Int)
store <- newSubscriptionStore (PendingLimits 1 1024) (pure ())
registerSubscription store "1" "A" (pure ())
registerWithAcceptance
store
"2"
(SubscriptionMeta "B" Nothing OneShotSubscription)
(SubscribeConfig Nothing Nothing)
(modifyTVar' accepted (+ 1))
(const (pure ()))
(pure ())
dispatchMessage store (message "1" "one")
`shouldReturn` DispatchQueued
dispatchMessage store (message "2" "two")
`shouldReturn` DispatchDropped True
readTVarIO accepted `shouldReturn` 1
dispatchMessage store (message "2" "duplicate")
`shouldReturn` DispatchMissing
readTVarIO accepted `shouldReturn` 1
it "commits request acceptance and overflow atomically" $ do
accepted <- newTVarIO False
rejected <- newTVarIO False
throwOnRejection <- newTVarIO True
store <- newSubscriptionStore (PendingLimits 1 1024) (pure ())
registerSubscription store "1" "A" (pure ())
registerWithDispatchHooks
store
"2"
(SubscriptionMeta "B" Nothing RequestReplySubscription)
(SubscribeConfig Nothing Nothing)
(writeTVar accepted True)
(do
shouldThrow <- readTVar throwOnRejection
when shouldThrow (throwSTM (userError "rejection failed"))
writeTVar rejected True)
(const (pure ()))
(pure ())
dispatchMessage store (message "1" "one")
`shouldReturn` DispatchQueued
dispatchMessage store (message "2" "two")
`shouldThrow` anyIOException
atomically ((,) <$> readTVar accepted <*> readTVar rejected)
`shouldReturn` (False, False)
map fst <$> active store `shouldReturn` ["1", "2"]
atomically (writeTVar throwOnRejection False)
dispatchMessage store (message "2" "two")
`shouldReturn` DispatchDropped True
atomically ((,) <$> readTVar accepted <*> readTVar rejected)
`shouldReturn` (True, True)
registerSubscription
:: SubscriptionStore
-> SID
-> Subject
-> IO ()
-> IO ()
registerSubscription store sidValue subjectValue =
register
store
sidValue
(SubscriptionMeta subjectValue Nothing StandardSubscription)
(SubscribeConfig Nothing Nothing)
(const (pure ()))
message :: SID -> Payload -> Msg
message sidValue payloadValue =
Msg "A" sidValue Nothing (Just payloadValue) Nothing