packages feed

shibuya-core-0.10.0.0: src/Shibuya/Internal/App.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.
module Shibuya.Internal.App
  ( QueueProcessor (..),
    mkProcessor,
    mkBatchProcessor,
    AppHandle (..),
    OwnershipFailure (..),
    acquireOwned,
  )
where

import Control.Concurrent.STM (TMVar, TVar)
import Data.Map.Strict (Map)
import Effectful (Eff, IOE, (:>))
import Effectful.Exception qualified as Exception
import Shibuya.Adapter (Adapter (..))
import Shibuya.Batch (BatchConfig, BatchHandler)
import Shibuya.Core.Metrics (ProcessorId (..))
import Shibuya.Handler (Handler)
import Shibuya.Internal.Runner.Master (Master)
import Shibuya.Internal.Runner.Supervised (SupervisedProcessor)
import Shibuya.Policy (Concurrency (..), OrderingPolicy (..))
import UnliftIO (SomeException)

-- | Failure while acquiring an owner or while transferring resources into an
-- acquired owner's custody. Kept in this internal module so tests can inject a
-- deterministic cancellation barrier into the exact primitive used by
-- 'Shibuya.App.runApp'.
data OwnershipFailure
  = OwnerAcquisitionFailed !SomeException
  | OwnedActionFailed !SomeException !(Maybe SomeException)
  deriving stock (Show)

-- | Acquire an owner under masking, restore interruptibility while acquiring
-- the resources it will own, and clean the owner up before reporting any
-- synchronous or asynchronous failure from that action.
acquireOwned ::
  (IOE :> es) =>
  Eff es owner ->
  (owner -> Eff es ()) ->
  (owner -> Eff es value) ->
  Eff es (Either OwnershipFailure value)
acquireOwned acquireOwner releaseOwner acquireResources =
  Exception.mask $ \restore -> do
    ownerResult <- Exception.try @SomeException acquireOwner
    case ownerResult of
      Left failure -> pure $ Left $ OwnerAcquisitionFailed failure
      Right owner -> do
        resourceResult <- Exception.try @SomeException $ restore $ acquireResources owner
        case resourceResult of
          Right value -> pure $ Right value
          Left failure -> do
            cleanupResult <- Exception.try @SomeException $ releaseOwner owner
            pure $
              Left $
                OwnedActionFailed
                  failure
                  (either Just (const Nothing) cleanupResult)

-- | A queue processor pairs an adapter with a handler. The message type is
-- existentially hidden, allowing heterogeneous queues in one @runApp@ call.
--
-- @QueueProcessor@ processes one message at a time; @BatchingProcessor@ groups
-- messages into batches (see "Shibuya.Batch") and runs a batch handler over each.
data QueueProcessor es where
  QueueProcessor ::
    { adapter :: Adapter es msg,
      handler :: Handler es msg,
      ordering :: OrderingPolicy,
      concurrency :: Concurrency
    } ->
    QueueProcessor es
  BatchingProcessor ::
    { adapter :: Adapter es msg,
      batchHandler :: BatchHandler es msg,
      batchConfig :: BatchConfig es msg,
      ordering :: OrderingPolicy,
      concurrency :: Concurrency
    } ->
    QueueProcessor es

-- | Convenience constructor with default policies (Unordered + Serial).
-- Provides backward compatibility with existing code.
mkProcessor :: Adapter es msg -> Handler es msg -> QueueProcessor es
mkProcessor adapter handler = QueueProcessor adapter handler Unordered Serial

-- | Convenience constructor for a batching processor with safe default policies
-- (Unordered ordering + Serial concurrency, i.e. one batch at a time).
mkBatchProcessor ::
  Adapter es msg -> BatchHandler es msg -> BatchConfig es msg -> QueueProcessor es
mkBatchProcessor adapter batchHandler batchConfig =
  BatchingProcessor adapter batchHandler batchConfig Unordered Serial

-- | Handle for a running multi-queue application.
-- Provides introspection and control over all processors.
data AppHandle es = AppHandle
  { -- | The master coordinator
    master :: !Master,
    -- | Map of processor IDs to their handles
    processors :: !(Map ProcessorId (SupervisedProcessor, QueueProcessor es)),
    -- | Coordinates repeated and concurrent graceful-stop calls. The first
    -- caller performs shutdown; every caller observes the same terminal result.
    shutdownStarted :: !(TVar Bool),
    shutdownResult :: !(TMVar (Either SomeException Bool))
  }