kiroku-store-0.3.0.0: test/Test/StreamBridgeTermination.hs
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE OverloadedStrings #-}
module Test.StreamBridgeTermination (spec) where
import Control.Concurrent (threadDelay)
import Control.Concurrent.Async qualified as Async
import Control.Concurrent.STM (atomically, putTMVar)
import Control.Exception (Exception, SomeException, finally, fromException, throwIO, try)
import Control.Lens ((^.))
import Data.Aeson qualified as Aeson
import Data.Data (Typeable)
import Data.Generics.Labels ()
import Hasql.Pool qualified as Pool
import Kiroku.Store
import Kiroku.Store.Subscription.Stream (AckItem (..), InvalidStreamBufferSize (..), subscriptionAckStream)
import Kiroku.Store.Subscription.Worker (withFetchBatchHookForTest)
import Streamly.Data.Stream qualified as Stream
import Test.Helpers (makeEvent, waitForPublisher, withTestStore)
import Test.Hspec
data TestBoom = TestBoom
deriving stock (Show, Typeable)
deriving anyclass (Exception)
timeoutMicros :: Int
timeoutMicros = 10_000_000
within :: String -> IO a -> IO a
within label action = do
result <- Async.race (threadDelay timeoutMicros) action
case result of
Left () -> fail ("timed out waiting for " <> label)
Right a -> pure a
appendOne :: KirokuStore -> StreamName -> EventType -> IO GlobalPosition
appendOne store streamName (EventType eventType') = do
Right r <- runStoreIO store $ appendToStream streamName NoStream [makeEvent eventType' (Aeson.object [])]
pure (r ^. #globalPosition)
spec :: Spec
spec = describe "stream bridge termination" $ do
it "rejects a zero-sized bridge buffer" $
withTestStore $ \store -> do
let cfg = defaultSubscriptionConfig (SubscriptionName "bridge-zero-buffer-sub") AllStreams (\_ -> pure Continue)
subscriptionAckStream store cfg 0
`shouldThrow` ( \e ->
case e of
InvalidStreamBufferSize 0 -> True
_ -> False
)
it "rethrows the worker exception to the consumer when the worker dies" $
withTestStore $ \store -> do
pos <- appendOne store (StreamName "bridge-crash") (EventType "BridgeCrash")
waitForPublisher store pos
let cfg = defaultSubscriptionConfig (SubscriptionName "bridge-crash-sub") AllStreams (\_ -> pure Continue)
injectBoom _ _ = throwIO TestBoom
withFetchBatchHookForTest injectBoom $ do
(stream, cancelStream) <- subscriptionAckStream store cfg 16
pulled <- within "stream pull to throw TestBoom" (try (Stream.uncons stream))
cancelStream
case pulled of
Left e
| Just TestBoom <- fromException (e :: SomeException) -> pure ()
Left e -> expectationFailure ("expected TestBoom, got: " <> show e)
Right _ -> expectationFailure "expected stream pull to throw TestBoom"
it "ends the stream after a clean worker stop" $
withTestStore $ \store -> do
let cfg = defaultSubscriptionConfig (SubscriptionName "bridge-clean-stop-sub") AllStreams (\_ -> pure Continue)
(stream0, cancelStream) <- subscriptionAckStream store cfg 16
finally
( do
pos <- appendOne store (StreamName "bridge-clean-stop") (EventType "BridgeCleanStop")
waitForPublisher store pos
mFirst <- within "first bridge item" (Stream.uncons stream0)
(stream1, item) <- case mFirst of
Nothing -> fail "expected one bridge item before clean stop"
Just (firstItem, rest) -> pure (rest, firstItem)
atomically (putTMVar (ackReply item) Stop)
mNext <- within "stream end after clean stop" (Stream.uncons stream1)
case mNext of
Nothing -> pure ()
Just _ -> expectationFailure "expected stream to end after clean stop"
)
cancelStream
it "cancelAction returns promptly even when the bridge queue is full" $
withTestStore $ \store -> do
let cfg = defaultSubscriptionConfig (SubscriptionName "bridge-full-cancel-sub") AllStreams (\_ -> pure Continue)
(stream0, cancelStream) <- subscriptionAckStream store cfg 1
pos1 <- appendOne store (StreamName "bridge-full-cancel-1") (EventType "BridgeFullCancel1")
pos2 <- appendOne store (StreamName "bridge-full-cancel-2") (EventType "BridgeFullCancel2")
waitForPublisher store pos2
within "non-blocking cancelAction with a full queue" cancelStream
mFirst <- within "drained item after cancel" (Stream.uncons stream0)
case mFirst of
Nothing -> pure ()
Just (item, stream1) -> do
atomically (putTMVar (ackReply item) Continue)
mNext <- within "stream end after cancel" (Stream.uncons stream1)
case mNext of
Nothing -> pure ()
Just _ -> expectationFailure "expected stream to end after cancel"