kiroku-store 0.9.0.0 → 0.9.0.1
raw patch · 4 files changed
+53/−5 lines, 4 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
Files
- CHANGELOG.md +10/−0
- kiroku-store.cabal +2/−2
- src/Kiroku/Store/Subscription/EventPublisher.hs +4/−1
- test/Test/PublisherIdleAdvance.hs +37/−2
CHANGELOG.md view
@@ -1,5 +1,15 @@ # Changelog +## 0.9.0.1 — 2026-09-25++### Bug Fixes++* The publisher now forces its scalar global position before storing it when+ no all-stream queue subscribers are registered (BUG-3). This prevents a+ chain of unevaluated position updates from retaining earlier Hasql results+ and large objects during sustained appends. A post-major-GC regression test+ covers the idle publisher path.+ ## 0.9.0.0 — 2026-09-25 ### Breaking Changes
kiroku-store.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: kiroku-store-version: 0.9.0.0+version: 0.9.0.1 synopsis: High-performance PostgreSQL event store description: Kiroku is a PostgreSQL-backed event store for Haskell applications. It@@ -139,7 +139,7 @@ Test.VisibleGlobalHeadPosition Test.VisibleGlobalHeadPositionMock - ghc-options: -threaded -rtsopts -with-rtsopts=-N+ ghc-options: -threaded -rtsopts "-with-rtsopts=-N -T" build-depends: , aeson >=2.1 && <2.3 , async >=2.2 && <2.3
src/Kiroku/Store/Subscription/EventPublisher.hs view
@@ -248,7 +248,10 @@ if IntMap.null subs' then do GlobalPosition cur <- readTVar posVar- writeTVar posVar (GlobalPosition (max cur tailPos))+ let nextPos = max cur tailPos+ -- Do not store a chain of lazy max expressions that+ -- retains earlier tail-query results.+ nextPos `seq` writeTVar posVar (GlobalPosition nextPos) pure False else pure True when raced fullFetch
test/Test/PublisherIdleAdvance.hs view
@@ -8,16 +8,18 @@ import Control.Concurrent.STM (readTVarIO) import Control.Exception (bracket) import Control.Lens ((&), (.~), (^.))-import Control.Monad (when)+import Control.Monad (forM, when) import Data.Aeson qualified as Aeson import Data.Generics.Labels () import Data.IORef (IORef, atomicModifyIORef', newIORef, readIORef) import Data.IntMap.Strict qualified as IntMap import Data.List (sort) import Data.String (fromString)+import GHC.Stats (GCDetails (..), RTSStats (..), getRTSStats, getRTSStatsEnabled) import Kiroku.Store import Kiroku.Store.Subscription.EventPublisher qualified as Pub-import Test.Helpers (caughtUpEventHandler, makeEvent, waitForPublisher, waitForSubscriptionLive, withTestStoreSettings)+import System.Mem (performMajorGC)+import Test.Helpers (caughtUpEventHandler, makeEvent, waitForPublisher, waitForSubscriptionLive, withTestStore, withTestStoreSettings) import Test.Hspec timeoutMicros :: Int@@ -62,8 +64,41 @@ publisherSubscriberCount store = IntMap.size <$> readTVarIO (Pub.subscribers (store ^. #publisher)) +sampleLargeObjects :: IO Integer+sampleLargeObjects = do+ enabled <- getRTSStatsEnabled+ when (not enabled) (fail "publisher retention test requires RTS statistics")+ performMajorGC+ fromIntegral . gcdetails_large_objects_bytes . gc <$> getRTSStats+ spec :: Spec spec = describe "publisher idle advance" $ do+ it "does not retain append results while advancing with no queue subscribers" $ do+ withTestStore $ \store -> do+ let blockSize :: Int+ blockSize = 2_000+ blocks :: Int+ blocks = 6+ event = makeEvent "Retention" (Aeson.object [])+ appendOne = do+ result <- runStoreIO store $ appendToStream (StreamName "pubidle-retention") AnyVersion [event]+ case result of+ Left err -> fail ("append failed: " <> show err)+ Right _ -> pure ()+ samples <- forM [1 .. blocks] $ \_ -> do+ sequence_ (replicate blockSize appendOne)+ -- Allow the notification-driven publisher to finish its tail+ -- query without reading (and thereby forcing) its position.+ threadDelay 100_000+ sampleLargeObjects+ publisherSubscriberCount store `shouldReturn` 0+ case samples of+ firstSample : _ -> do+ let growth = last samples - firstSample+ when (growth >= 3 * 1024 * 1024) $+ expectationFailure ("large-object bytes grew by " <> show growth <> "; samples: " <> show samples)+ [] -> expectationFailure "publisher retention test collected no heap samples"+ it "advances lastPublished without decoding any rows when no subscriber is registered" $ do counter <- newIORef 0 withTestStoreSettings (countingSettings counter) $ \store -> do