eventium-memory-0.2.1: tests/Eventium/ResilientSubscriptionSpec.hs
module Eventium.ResilientSubscriptionSpec (spec) where
import Control.Concurrent (forkIO, killThread, threadDelay)
import Control.Concurrent.STM
import Control.Exception (IOException, throwIO)
import Control.Monad.IO.Class (liftIO)
import Data.IORef
import Eventium
import Eventium.CheckpointStore.Memory
import Eventium.Store.Memory
import Test.Hspec
spec :: Spec
spec = describe "resilientPollingSubscription" $ do
it "recovers from a transient error and delivers events" $ do
tvar <- eventMapTVar
let writer = runEventStoreWriterUsing atomically (tvarEventStoreWriter tvar)
globalReader = runEventStoreReaderUsing atomically (tvarGlobalEventStoreReader tvar)
uuid = uuidFromInteger 1
_ <- writer.storeEvents uuid NoStream [10 :: Int, 20]
failRef <- newIORef True
let wrappedReader =
EventStoreReader $ \range -> do
shouldFail <- liftIO $ readIORef failRef
if shouldFail
then do
liftIO $ writeIORef failRef False
liftIO $ throwIO (userError "transient DB error" :: IOException)
else globalReader.getEvents range
checkpointRef <- newIORef (0 :: SequenceNumber)
let checkpoint = ioRefCheckpointStore checkpointRef
deliveredRef <- newIORef ([] :: [Int])
let handler = EventHandler $ \ge ->
modifyIORef deliveredRef (++ [ge.payload.payload])
callbackRef <- newIORef (0 :: Int)
let config =
defaultRetryConfig
{ initialDelayMs = 10,
maxDelayMs = 100,
onErrorCallback = \_ n -> writeIORef callbackRef n
}
let sub = resilientPollingSubscription id wrappedReader checkpoint 50 config
tid <- forkIO $ sub.runSubscription handler
threadDelay 500000
killThread tid
delivered <- readIORef deliveredRef
delivered `shouldBe` [10, 20]
callbackCount <- readIORef callbackRef
callbackCount `shouldBe` 1
it "re-throws when retryOnError returns False" $ do
let wrappedReader =
EventStoreReader $ \_ ->
liftIO $ throwIO (userError "fatal error" :: IOException)
checkpointRef <- newIORef (0 :: SequenceNumber)
let checkpoint = ioRefCheckpointStore checkpointRef
let config =
defaultRetryConfig
{ onError = const (return False)
}
let sub = resilientPollingSubscription id wrappedReader checkpoint 50 config
handler = EventHandler $ \_ -> return ()
sub.runSubscription handler `shouldThrow` anyIOException
it "invokes retryOnErrorCallback with correct consecutive count" $ do
tvar <- eventMapTVar
callCountRef <- newIORef (0 :: Int)
let fallbackReader = runEventStoreReaderUsing atomically (tvarGlobalEventStoreReader tvar)
wrappedReader =
EventStoreReader $ \_ -> do
n <- liftIO $ readIORef callCountRef
if n < 3
then do
liftIO $ modifyIORef callCountRef (+ 1)
liftIO $ throwIO (userError "error" :: IOException)
else
fallbackReader.getEvents (allEvents ())
checkpointRef <- newIORef (0 :: SequenceNumber)
let checkpoint = ioRefCheckpointStore checkpointRef
callbackCounts <- newIORef ([] :: [Int])
let config =
defaultRetryConfig
{ initialDelayMs = 10,
maxDelayMs = 100,
onErrorCallback = \_ n -> modifyIORef callbackCounts (++ [n])
}
let sub = resilientPollingSubscription id wrappedReader checkpoint 50 config
handler = EventHandler $ \_ -> return ()
tid <- forkIO $ sub.runSubscription handler
threadDelay 500000
killThread tid
counts <- readIORef callbackCounts
counts `shouldBe` [1, 2, 3]