packages feed

effectful-poolboy-0.1.0.0: src/Data/Poolboy/Effectful.hs

--- |
-- Module      :  Data.Poolboy.Effectful
-- Copyright   :  Gautier DI FOLCO 2025
-- License     :  ISC
--
-- Maintainer  :  foos@difolco.dev
-- Stability   :  experimental
-- Portability :  GHC
--
-- Thin Effectful wrapper around the existing poolboy library (Data.Poolboy).
--
-- @
-- withPoolboy defaultPoolboySettings waitingStopFinishWorkers $ \workQueue ->
--    mapM_ (enqueue workQueue . execQuery insertBookQuery) books
-- @
--

module Data.Poolboy.Effectful
  ( -- * Configuration
    PB.PoolboySettings (..),
    PB.WorkersCountSettings (..),
    PB.PoolboyCommand (..),
    PB.defaultPoolboySettings,
    PB.poolboySettingsWith,
    PB.poolboySettingsName,
    PB.poolboySettingsLog,

    -- * Running
    WorkQueue,
    withPoolboy,
    newPoolboy,
    PB.hoistWorkQueue,

    -- * Driving
    changeDesiredWorkersCount,
    waitReadyQueue,

    -- * Stopping
    stopWorkQueue,
    isStoppedWorkQueue,
    PB.WaitingStopStrategy,
    waitingStopTimeout,
    waitingStopFinishWorkers,

    -- * Enqueueing
    enqueue,
    enqueueTracking,
    enqueueAfter,
    enqueueAfterTracking,
    PB.WorkQueueStoppedException,
  )
where

import Control.Concurrent.Async (Async)
import qualified Data.Poolboy as PB
import Effectful

-- | Local wrapper type: hides the underlying PB.WorkQueue IO inside Eff.
newtype WorkQueue (es :: [Effect]) = WorkQueue (PB.WorkQueue IO)

--------------------------------------------------------------------------------
-- Bracket / creation
--------------------------------------------------------------------------------

-- | Bracket-like helper that runs poolboy's `withPoolboy` in IO while presenting
--   a `WorkQueue es` to the provided Eff callbacks.
withPoolboy ::
  forall es a.
  (IOE :> es) =>
  PB.PoolboySettings (Eff es) ->
  PB.WaitingStopStrategy (Eff es) ->
  (WorkQueue es -> Eff es a) ->
  Eff es a
withPoolboy settings waitStrat inner = do
  -- acquire a conversion Eff -> IO so we can run library code in IO threads
  withEffToIO (ConcUnlift Ephemeral Unlimited) $ \(toIO :: forall b. Eff es b -> IO b) -> do
    -- convert PoolboySettings (Eff es) -> PoolboySettings IO
    let settingsIO :: PB.PoolboySettings IO
        settingsIO = PB.hoistPoolboySettings toIO settings

    -- convert WaitingStopStrategy: PB.WorkQueue IO -> IO ()
    let waitStratIO :: PB.WorkQueue IO -> IO ()
        waitStratIO wqIO = toIO (waitStrat $ PB.hoistWorkQueue liftIO wqIO)

    -- run poolboy's withPoolboy in IO and convert the provided inner callback
    PB.withPoolboy settingsIO waitStratIO $ \wqIO -> do
      let wqEff = WorkQueue wqIO
      toIO (inner wqEff)

-- | Standalone/manual usage
newPoolboy ::
  forall es.
  (IOE :> es) =>
  PB.PoolboySettings (Eff es) ->
  Eff es (WorkQueue es)
newPoolboy settings = do
  withEffToIO (ConcUnlift Ephemeral Unlimited) $ \(toIO :: forall b. Eff es b -> IO b) -> do
    let settingsIO = PB.hoistPoolboySettings toIO settings
    wqIO <- liftIO $ PB.newPoolboy settingsIO
    pure (WorkQueue wqIO)

-- | Request a worker number adjustment
--
-- Warning: non-concurrent operation
changeDesiredWorkersCount :: (IOE :> es) => WorkQueue es -> Int -> Eff es ()
changeDesiredWorkersCount (WorkQueue wq) n = liftIO $ PB.changeDesiredWorkersCount wq n

-- | Request stopping wokers
stopWorkQueue :: (IOE :> es) => WorkQueue es -> Eff es ()
stopWorkQueue (WorkQueue wq) =
  -- underlying API expects m ~ IO, so just call in IO
  liftIO $ PB.stopWorkQueue wq

-- | Non-blocking check of the work queue's running status
isStoppedWorkQueue :: (IOE :> es) => WorkQueue es -> Eff es Bool
isStoppedWorkQueue (WorkQueue wq) = liftIO $ PB.isStoppedWorkQueue wq

-- | Block until the queue is totally stopped (no more running worker)
waitingStopFinishWorkers :: (IOE :> es) => PB.WaitingStopStrategy (Eff es)
waitingStopFinishWorkers wq =
  withEffToIO (ConcUnlift Ephemeral Unlimited) $ \(toIO :: forall b. Eff es b -> IO b) -> do
    PB.waitingStopFinishWorkers $ PB.hoistWorkQueue toIO wq

-- | Block until the queue is totally stopped or deadline (in micro seconds) is reached
waitingStopTimeout :: (IOE :> es) => Int -> PB.WaitingStopStrategy (Eff es)
waitingStopTimeout delay wq =
  withEffToIO (ConcUnlift Ephemeral Unlimited) $ \(toIO :: forall b. Eff es b -> IO b) -> do
    PB.waitingStopTimeout delay $ PB.hoistWorkQueue toIO wq

-- | Enqueue one action in the work queue (non-blocking)
--
-- Throws 'WorkQueueStoppedException' if the work queue is stopped
enqueue :: forall es. (IOE :> es) => WorkQueue es -> Eff es () -> Eff es ()
enqueue (WorkQueue wq) eff = do
  withEffToIO (ConcUnlift Ephemeral Unlimited) $ \(toIO :: forall b. Eff es b -> IO b) -> do
    -- PB.enqueue :: WorkQueue m -> m () -> m (); here m ~ IO
    liftIO $ PB.enqueue wq (toIO eff)

-- | Enqueue one action in the work queue (non-blocking)
--
-- Throws 'WorkQueueStoppedException' if the work queue is stopped
enqueueTracking :: forall es a. (IOE :> es) => WorkQueue es -> Eff es a -> Eff es (Async a)
enqueueTracking (WorkQueue wq) eff = do
  withEffToIO (ConcUnlift Ephemeral Unlimited) $ \(toIO :: forall b. Eff es b -> IO b) -> do
    -- PB.enqueueTracking :: WorkQueue m -> m a -> m (Async a)
    liftIO $ PB.enqueueTracking wq (toIO eff)

-- | Block until one worker is available
waitReadyQueue :: (IOE :> es) => WorkQueue es -> Eff es ()
waitReadyQueue (WorkQueue wq) = liftIO $ PB.waitReadyQueue wq

-- | Enqueue action and some actions to be run after it
--
-- Throws 'WorkQueueStoppedException' if the work queue is stopped
enqueueAfter ::
  forall t es.
  (Traversable t, IOE :> es) =>
  WorkQueue es ->
  Eff es () ->
  t (Eff es ()) ->
  Eff es ()
enqueueAfter (WorkQueue wq) before afters = do
  withEffToIO (ConcUnlift Ephemeral Unlimited) $ \(toIO :: forall b. Eff es b -> IO b) -> do
    liftIO $ PB.enqueueAfter wq (toIO before) (fmap toIO afters)

-- | Enqueue action and some actions to be run after it
--
-- Throws 'WorkQueueStoppedException' if the work queue is stopped
enqueueAfterTracking ::
  forall t a b es.
  (Traversable t, IOE :> es) =>
  WorkQueue es ->
  Eff es a ->
  t (Eff es b) ->
  Eff es (Async a, t (Async b))
enqueueAfterTracking (WorkQueue wq) before afters = do
  withEffToIO (ConcUnlift Ephemeral Unlimited) $ \(toIO :: forall c. Eff es c -> IO c) -> do
    liftIO $ PB.enqueueAfterTracking wq (toIO before) (fmap toIO afters)