shibuya-core-0.10.0.0: src/Shibuya/Internal/Runner/Halt.hs
-- | __Internal module.__ Exposed for the test suite and benchmarks only.
-- No PVP guarantees: anything here may change or disappear in any release.
-- Application authors should import "Shibuya" instead.
--
-- Halt exception for processor termination.
-- Thrown when a handler returns AckHalt to stop processing.
module Shibuya.Internal.Runner.Halt
( ProcessorHalt (..),
ProcessorFailure (..),
ProcessorExit (..),
ProcessorSignal,
ProcessorExitPublisher,
newProcessorSignal,
newProcessorExitPublisher,
newProcessorExitPublisherWithWake,
readProcessorExit,
requestProcessorExit,
throwProcessorExit,
)
where
import Control.Concurrent.STM (TVar, atomically, writeTVar)
import Control.Exception (Exception, mask_)
import Data.IORef (IORef, atomicModifyIORef', newIORef, readIORef)
import Shibuya.Core.Ack (HaltReason)
import Shibuya.Core.Types (MessageId)
import Shibuya.Prelude
import UnliftIO (throwIO)
-- | Exception thrown when processing should halt.
-- The supervisor catches this to handle graceful shutdown.
data ProcessorHalt = ProcessorHalt
{ reason :: !HaltReason
}
deriving stock (Show, Generic)
instance Exception ProcessorHalt
-- | An infrastructure failure that must remain distinguishable from a handler's
-- deliberate 'AckHalt'. The optional message identity is retained when the
-- failure occurred while finalizing a delivery.
data ProcessorFailure = ProcessorFailure !Text !(Maybe MessageId)
deriving stock (Show, Generic)
instance Exception ProcessorFailure
-- | The first terminal request observed by a processor. Infrastructure failure
-- takes precedence over a graceful halt if concurrent work reports both.
data ProcessorExit
= ProcessorHalted !HaltReason
| ProcessorFailed !Text !(Maybe MessageId)
deriving stock (Eq, Show, Generic)
-- | A cheap hot-path stop observation. The corresponding publisher is kept
-- separate so message actions retain one opaque cold-path pointer rather than
-- capturing and field-splitting the signal and its STM wake cell.
newtype ProcessorSignal = ProcessorSignal
{ terminalExit :: IORef (Maybe ProcessorExit)
}
-- | Opaque cold-path terminal publisher. The function closure may retain both
-- the signal and an intake wake cell, but hot message closures retain only this
-- single pointer. The box prevents GHC from field-splitting those two captured
-- cells back into every nested per-message closure.
data ProcessorExitPublisher = ProcessorExitPublisher (ProcessorExit -> IO ())
newProcessorSignal :: IO ProcessorSignal
newProcessorSignal = ProcessorSignal <$> newIORef Nothing
newProcessorExitPublisher :: ProcessorSignal -> ProcessorExitPublisher
newProcessorExitPublisher signal =
ProcessorExitPublisher $ \requested ->
mask_ $ publishProcessorExit signal requested
{-# OPAQUE newProcessorExitPublisher #-}
newProcessorExitPublisherWithWake :: ProcessorSignal -> TVar Bool -> ProcessorExitPublisher
newProcessorExitPublisherWithWake signal intakeWake =
ProcessorExitPublisher $ \requested ->
-- Publish the terminal outcome before the STM wakeup. Masking prevents
-- cancellation from leaving only the outcome set; this path runs once per
-- terminal request, not once per message.
mask_ $ do
publishProcessorExit signal requested
atomically $ writeTVar intakeWake True
{-# OPAQUE newProcessorExitPublisherWithWake #-}
readProcessorExit :: ProcessorSignal -> IO (Maybe ProcessorExit)
readProcessorExit = readIORef . (.terminalExit)
{-# INLINE readProcessorExit #-}
requestProcessorExit :: ProcessorExitPublisher -> ProcessorExit -> IO ()
requestProcessorExit (ProcessorExitPublisher publish) = publish
{-# OPAQUE requestProcessorExit #-}
publishProcessorExit :: ProcessorSignal -> ProcessorExit -> IO ()
publishProcessorExit signal requested =
atomicModifyIORef' signal.terminalExit $ \current ->
( case (current, requested) of
(Just ProcessorFailed {}, _) -> current
(_, ProcessorFailed {}) -> Just requested
(Nothing, _) -> Just requested
(Just ProcessorHalted {}, ProcessorHalted {}) -> current,
()
)
throwProcessorExit :: ProcessorExit -> IO a
throwProcessorExit (ProcessorHalted reason) = throwIO (ProcessorHalt reason)
throwProcessorExit (ProcessorFailed failure messageId) =
throwIO (ProcessorFailure failure messageId)