kiroku-store-0.2.0.0: test/Test/CatchupDbErrorNoPrematureSwitch.hs
module Test.CatchupDbErrorNoPrematureSwitch (spec) where
import Control.Concurrent.MVar (newEmptyMVar, tryPutMVar)
import Control.Concurrent.STM (atomically, check, newTVarIO, readTVar, writeTVar)
import Control.Lens ((&), (.~), (^.))
import Data.Aeson qualified as Aeson
import Data.Generics.Labels ()
import Data.IORef (modifyIORef', newIORef, readIORef, writeIORef)
import Data.List qualified as List
import Data.Vector qualified as V
import Hasql.Pool qualified as Pool
import Kiroku.Store
import Kiroku.Store.Subscription.Types qualified as SubTypes
import Kiroku.Store.Subscription.Worker (withFetchBatchHookForTest)
import Test.Helpers (
caughtUpEventHandler,
makeEvent,
waitForPublisher,
waitForSubscriptionLive,
withTestStoreSettings,
)
import Test.Hspec
spec :: Spec
spec =
describe "catch-up DB error handling" $
it "retries a transient fetch error instead of switching to live mode at a stale cursor" $ \() -> do
deliveredRef <- newIORef ([] :: [GlobalPosition])
failedOnce <- newIORef False
eventRef <- newIORef ([] :: [KirokuEvent])
seenCount <- newTVarIO (0 :: Int)
caughtUp <- newEmptyMVar
let subName = SubscriptionName "catchup-db-error-no-gap"
targetTail = GlobalPosition 3
handler' evt = do
modifyIORef' deliveredRef ((evt ^. #globalPosition) :)
atomically $ do
n <- readTVar seenCount
writeTVar seenCount (n + 1)
pure Continue
cfg =
SubscriptionConfig
{ name = subName
, target = AllStreams
, handler = handler'
, batchSize = 100
, queueCapacity = 16
, overflowPolicy = DropSubscription
, consumerGroup = Nothing
, consumerGroupGuard = False
, retryPolicy = defaultRetryPolicy
, eventTypeFilter = AllEventTypes
, selector = Nothing
}
observe evt = do
modifyIORef' eventRef (evt :)
caughtUpEventHandler subName caughtUp Nothing evt
injectOnce config cursor
| SubTypes.name config == subName && cursor == GlobalPosition 0 = do
alreadyFailed <- readIORef failedOnce
if alreadyFailed
then pure Nothing
else do
writeIORef failedOnce True
pure (Just (Left Pool.AcquisitionTimeoutUsageError))
| otherwise = pure Nothing
withTestStoreSettings (& #eventHandler .~ Just observe) $ \store -> do
Right _ <- runStoreIO store $ appendToStream (StreamName "catchup-db-error-1") NoStream [makeEvent "One" (Aeson.object [])]
Right _ <- runStoreIO store $ appendToStream (StreamName "catchup-db-error-2") NoStream [makeEvent "Two" (Aeson.object [])]
Right _ <- runStoreIO store $ appendToStream (StreamName "catchup-db-error-3") NoStream [makeEvent "Three" (Aeson.object [])]
waitForPublisher store targetTail
withFetchBatchHookForTest injectOnce $ do
handle <- subscribe store cfg
waitForSubscriptionLive caughtUp
atomically $ do
n <- readTVar seenCount
check (n >= 3)
cancel handle
delivered <- readIORef deliveredRef
List.sort delivered `shouldBe` [GlobalPosition 1, GlobalPosition 2, GlobalPosition 3]
observed <- readIORef eventRef
let isFetchDbError = \case
KirokuEventSubscriptionDbError n FetchBatch Pool.AcquisitionTimeoutUsageError _
| n == subName -> True
_ -> False
caughtUpPositions =
[ pos
| KirokuEventSubscriptionCaughtUp n pos _ <- observed
, n == subName
]
any isFetchDbError observed `shouldBe` True
V.fromList caughtUpPositions `shouldBe` V.singleton targetTail