kiroku-store-0.13.0.0: test/Test/EventReferences.hs
module Test.EventReferences (spec) where
import Control.Concurrent (myThreadId)
import Control.Lens ((&), (.~), (^.))
import Control.Monad (forM_, when)
import Data.Aeson qualified as Aeson
import Data.Generics.Labels ()
import Data.IORef (modifyIORef', newIORef, readIORef)
import Data.Map.Strict qualified as Map
import Data.UUID qualified as UUID
import Data.Vector qualified as V
import Kiroku.Store
import Kiroku.Store.Effect (maxEventReferenceBatch)
import Test.Helpers
import Test.Hspec
spec :: Spec
spec = describe "event references" $ do
it "reports home and global memberships with actual global positions" $ withTestStore $ \store -> do
event <- firstEvent store
let eid = event ^. #eventId
Right refs <- runStoreIO store (lookupEventReferences [eid])
refs
`shouldBe` Map.singleton
eid
( EventReferenceInventory
(V.fromList [GlobalMembership (GlobalPosition 1), HomeMembership (StreamName "order-1") (event ^. #originalStreamId) (StreamVersion 1)])
0
0
)
it "reports all link targets, including a link of a link and soft-deleted targets" $ withTestStore $ \store -> do
event <- firstEvent store
let eid = event ^. #eventId
forM_ [(StreamName "hub"), (StreamName "mirror")] $ \name -> do
Right _ <- runStoreIO store (linkToStream name [eid])
pure ()
Right _ <- runStoreIO store (softDeleteStream (StreamName "hub"))
Right refs <- runStoreIO store (lookupEventReferences [eid])
let ms = (refs Map.! eid) ^. #memberships
V.length ms `shouldBe` 4
[name | LinkMembership name _ _ <- V.toList ms] `shouldBe` [(StreamName "hub"), (StreamName "mirror")]
[name | HomeMembership name _ _ <- V.toList ms] `shouldBe` [(StreamName "order-1")]
it "counts dead letters across subscriptions" $ withTestStore $ \store -> do
event <- firstEvent store
insertDeadLetterForEvent store "proj-a" event
insertDeadLetterForEvent store "proj-b" event
Right refs <- runStoreIO store (lookupEventReferences [event ^. #eventId])
(refs Map.! (event ^. #eventId)) ^. #deadLetterCount `shouldBe` 2
it "excludes requested causation dependents and deduplicates input ids" $ withTestStore $ \store -> do
event <- firstEvent store
let eid@(EventId uid) = event ^. #eventId
dependent = makeEvent "Dependent" (Aeson.object []) & #causationId .~ Just uid
Right _ <- runStoreIO store (appendToStream (StreamName "caused-1") NoStream [dependent, dependent])
Right caused <- runStoreIO store (readStreamForward (StreamName "caused-1") (StreamVersion 0) 10)
Right single <- runStoreIO store (lookupEventReferences [eid])
(single Map.! eid) ^. #causationDependentCount `shouldBe` 2
let ids = [eid, caused V.! 0 ^. #eventId]
Right refs <- runStoreIO store (lookupEventReferences (ids <> ids))
(refs Map.! eid) ^. #causationDependentCount `shouldBe` 1
V.length ((refs Map.! eid) ^. #memberships) `shouldBe` 2
runStoreIO store (runTransaction (lookupEventReferencesTx ids)) `shouldReturn` Right refs
it "omits unknown ids and agrees with the transaction combinator" $ withTestStore $ \store -> do
event <- firstEvent store
let ids = [event ^. #eventId, EventId UUID.nil]
Right refs <- runStoreIO store (lookupEventReferences ids)
Map.keys refs `shouldBe` [event ^. #eventId]
runStoreIO store (runTransaction (lookupEventReferencesTx ids)) `shouldReturn` Right refs
it "uses no checkout for empty input and one checkout across batches" $ do
owner <- myThreadId
checkouts <- newIORef (0 :: Int)
let tweak settings =
settings
{ observationHandler = Just $ \case
ConnectionObservation _ InUseConnectionStatus -> do
current <- myThreadId
when (current == owner) (modifyIORef' checkouts (+ 1))
_ -> pure ()
}
withTestStoreSettings tweak $ \store -> do
beforeEmpty <- readIORef checkouts
runStoreIO store (lookupEventReferences []) `shouldReturn` Right Map.empty
readIORef checkouts `shouldReturn` beforeEmpty
-- Last requested event depends on the first: the exclusion must span batches.
let firstId = UUID.fromWords64 0 1
events =
[ makeEvent "Batch" (Aeson.object [])
& #eventId .~ Just (EventId (UUID.fromWords64 0 (fromIntegral n)))
& #causationId .~ (if n == maxEventReferenceBatch + 1 then Just firstId else Nothing)
| n <- [1 .. maxEventReferenceBatch + 1]
]
forM_ (chunks 2000 events) $ \batch -> do
Right _ <- runStoreIO store (appendToStream (StreamName "batch-1") AnyVersion batch)
pure ()
let ids = [EventId (UUID.fromWords64 0 (fromIntegral n)) | n <- [1 .. maxEventReferenceBatch + 1]]
beforeRead <- readIORef checkouts
Right refs <- runStoreIO store (lookupEventReferences ids)
readIORef checkouts `shouldReturn` (beforeRead + 1)
Map.size refs `shouldBe` maxEventReferenceBatch + 1
forM_ (Map.elems refs) $ \inventory -> do
V.length (inventory ^. #memberships) `shouldBe` 2
inventory ^. #causationDependentCount `shouldBe` 0
firstEvent :: KirokuStore -> IO RecordedEvent
firstEvent store = do
Right _ <- runStoreIO store (appendToStream (StreamName "order-1") NoStream [makeEvent "Placed" (Aeson.object [])])
Right events <- runStoreIO store (readAllForward (GlobalPosition 0) 10)
pure (V.head events)
chunks :: Int -> [a] -> [[a]]
chunks _ [] = []
chunks size values = let (batch, rest) = splitAt size values in batch : chunks size rest