packages feed

shibuya-core 0.8.0.0 → 0.8.0.1

raw patch · 5 files changed

+61/−4 lines, 5 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

Files

CHANGELOG.md view
@@ -1,5 +1,17 @@ # Changelog +## 0.8.0.1 — 2026-07-04++### Other Changes++- Reduced per-message allocation on the `Async` and `Ahead` concurrency paths+  by widening the streamly dispatch buffer to `2 * n` (the thread bound stays+  at `n`). A buffer equal to the thread count throttled worker dispatch and+  caused dispatch churn that allocated up to +90% on the `Async`+  concurrency-levels benchmarks (EP-30). No API or behavior change.+- Documented the accepted ~126 bytes/message cost of the separate handler and+  finalizer exception frames that guarantee "always finalize" (EP-31).+ ## 0.8.0.0 — 2026-07-04  ### Breaking Changes
shibuya-core.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.12 name: shibuya-core-version: 0.8.0.0+version: 0.8.0.1 synopsis: Supervised queue processing framework for Haskell description:   A supervised queue processing framework inspired by Broadway (Elixir).
src/Shibuya/Internal/Runner/Finalize.hs view
@@ -29,6 +29,12 @@  -- | Call a message finalizer until it succeeds or the bounded retry budget is -- exhausted. The resolved decision is never recomputed between attempts.+--+-- The 'catchAny' here is a per-message exception frame distinct from the+-- handler catch in 'processOne'; together they cost ~126 bytes/message more than+-- 0.7.1.0's single combined catch (measured, EP-31 S3). This cost is accepted as+-- the price of bounded finalizer retry + the always-finalize guarantee. See+-- docs/plans/31-investigate-and-reduce-the-shared-per-message-metrics-tracing-finalize-allocation-regression.md. finalizeWithRetry ::   (IOE :> es, Tracing :> es) =>   OTel.Span ->
src/Shibuya/Internal/Runner/Supervised.hs view
@@ -518,12 +518,21 @@         partitioned n       (PartitionedInOrder, Async n) ->         partitioned n+      -- 'maxThreads n' is the hard concurrency bound (at most n handlers run at+      -- once); do not remove it. The output buffer is set to '2 * n', not 'n':+      -- a buffer equal to the thread count throttles streamly's worker dispatch+      -- (a worker is only forked when both the thread AND buffer checks pass),+      -- causing dispatch churn that allocated up to +90% on the Async+      -- concurrency-levels benchmarks. Enlarging the buffer to 2n relieves the+      -- churn while keeping the n-thread bound, and matches the pending-item+      -- limit the partitioned path already uses (see 'partitioned' above). See+      -- docs/plans/30-investigate-and-reduce-the-async-ahead-concurrency-allocation-regression.md.       (_, Ahead n) ->         Stream.fold Fold.drain $-          StreamP.parMapM (StreamP.maxThreads n . StreamP.maxBuffer n . StreamP.ordered True) processAction inboxStream+          StreamP.parMapM (StreamP.maxThreads n . StreamP.maxBuffer (2 * n) . StreamP.ordered True) processAction inboxStream       (_, Async n) ->         Stream.fold Fold.drain $-          StreamP.parMapM (StreamP.maxThreads n . StreamP.maxBuffer n) processAction inboxStream+          StreamP.parMapM (StreamP.maxThreads n . StreamP.maxBuffer (2 * n)) processAction inboxStream      -- After draining, check if we halted     maybeHalt <- readIORef haltRef@@ -581,6 +590,15 @@       -- Call handler and finalizer separately. A handler exception is       -- substituted with immediate retry so the adapter always observes a       -- finalization decision for an ingested message.+      --+      -- This is a *separate* per-message 'catchAny' from the one inside+      -- 'finalizeWithRetry' below. 0.7.1.0 used a single combined catch around+      -- handler+finalize, which skipped finalization when the handler threw;+      -- splitting them is what makes "always finalize" hold. The extra+      -- per-message exception frame costs ~126 bytes/message (measured, EP-31+      -- S3: docs/plans/31-…-shared-per-message-…-allocation-regression.md) and is+      -- accepted as the price of the always-finalize guarantee — do not merge the+      -- two catches back together to reclaim it.       handlerResult <-         catchAny           (Right <$> handler (toMessage ingested))
test/Shibuya/RunnerSpec.hs view
@@ -8,7 +8,7 @@ import Effectful (Eff, IOE, liftIO, runEff, (:>)) import Shibuya.Adapter (Adapter (..)) import Shibuya.Adapter.Mock (TrackingAck (..), newTrackingAck, trackingAckHandle)-import Shibuya.App (AppConfig (..), AppError (..), QueueProcessor (..), defaultAppConfig, mkProcessor, runApp, waitApp)+import Shibuya.App (AppConfig (..), AppError (..), QueueProcessor (..), defaultAppConfig, mkProcessor, runApp, stopApp, waitApp) import Shibuya.Core.Ack (AckDecision (..)) import Shibuya.Core.AckHandle (AckHandle (..)) import Shibuya.Core.Error (ConfigError (..), PolicyError (..))@@ -70,6 +70,12 @@           Left err -> pure $ Left err           Right appHandle -> do             waitApp appHandle+            -- Stop the app to cancel the (always-linked) master coordinator.+            -- Without this the idle master blocks forever on its mailbox and the+            -- RTS eventually raises BlockedIndefinitelyOnSTM, which propagates+            -- through the link as a flaky ExceptionInLinkedThread landing on+            -- whichever test happens to be running when GC fires.+            stopApp appHandle             pure $ Right ()        -- Verify result@@ -101,6 +107,9 @@             pure (decs, Left err)           Right appHandle -> do             waitApp appHandle+            -- See note in "processes messages from mock adapter": stop the app so+            -- the linked master does not deadlock and flake a later test.+            stopApp appHandle             decs <- liftIO $ readIORef tracking.trackedDecisions             pure (decs, Right ()) @@ -132,6 +141,12 @@           Left err -> pure $ Left err           Right appHandle -> do             waitApp appHandle+            -- Stop the app to cancel the (always-linked) master coordinator.+            -- Without this the idle master blocks forever on its mailbox and the+            -- RTS eventually raises BlockedIndefinitelyOnSTM, which propagates+            -- through the link as a flaky ExceptionInLinkedThread landing on+            -- whichever test happens to be running when GC fires.+            stopApp appHandle             pure $ Right ()        result `shouldBe` Right ()@@ -185,6 +200,12 @@           Left err -> pure $ Left err           Right appHandle -> do             waitApp appHandle+            -- Stop the app to cancel the (always-linked) master coordinator.+            -- Without this the idle master blocks forever on its mailbox and the+            -- RTS eventually raises BlockedIndefinitelyOnSTM, which propagates+            -- through the link as a flaky ExceptionInLinkedThread landing on+            -- whichever test happens to be running when GC fires.+            stopApp appHandle             pure $ Right ()        result `shouldBe` Right ()