packages feed

shibuya-core-0.8.0.0: src/Shibuya/Internal/Runner/KeyedScheduler.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.
--
-- Bounded FIFO scheduler for work keyed by an optional partition key.
module Shibuya.Internal.Runner.KeyedScheduler
  ( runKeyedScheduler,
  )
where

import Control.Applicative ((<|>))
import Control.Concurrent.MVar (newEmptyMVar, putMVar, takeMVar)
import Control.Concurrent.STM
  ( STM,
    TVar,
    atomically,
    modifyTVar',
    newTVarIO,
    readTVar,
    readTVarIO,
    retry,
    writeTVar,
  )
import Data.Foldable (traverse_)
import Data.IORef (newIORef, readIORef, writeIORef)
import Data.Map.Strict qualified as Map
import Data.Sequence (Seq)
import Data.Sequence qualified as Seq
import Data.Set qualified as Set
import Data.Unique (Unique, newUnique)
import Streamly.Data.Fold qualified as Fold
import Streamly.Data.Stream qualified as Stream
import UnliftIO (Async, SomeException, async, cancel, catchAny, finally, throwIO, withAsync)

-- | Run every item in a stream through the worker action, at most
-- @maxConcurrency@ at a time. Items with the same @Just key@ run strictly in
-- stream order; @Nothing@ items have no mutual ordering constraint. The pending
-- buffer is bounded and blocks the reader when full, preserving upstream
-- backpressure.
runKeyedScheduler ::
  (Ord key) =>
  Int ->
  Int ->
  (item -> Maybe key) ->
  (item -> IO ()) ->
  Stream.Stream IO item ->
  IO ()
runKeyedScheduler requestedConcurrency requestedPendingLimit itemKey itemAction itemStream = do
  scheduler <- newTVarIO emptyKeyedSchedulerState
  workers <- newTVarIO (Map.empty :: Map.Map Unique (Async ()))
  let maxConcurrency = max 1 requestedConcurrency
      pendingLimit = max 1 requestedPendingLimit
      cancelWorkers = do
        liveWorkers <- readTVarIO workers
        traverse_ cancel (Map.elems liveWorkers)

      reader =
        ( do
            Stream.fold Fold.drain $
              Stream.mapM (enqueueItem pendingLimit scheduler) itemStream
            atomically $ markInputDone scheduler Nothing
        )
          `catchAny` \ex ->
            atomically $ markInputDone scheduler (Just ex)

      loop = do
        step <- atomically $ nextSchedulerStep maxConcurrency itemKey scheduler
        case step of
          SchedulerDone Nothing -> pure ()
          SchedulerDone (Just ex) -> throwIO ex
          StartItem item -> do
            workerId <- newUnique
            startGate <- newEmptyMVar
            worker <- async $ do
              takeMVar startGate
              runWorker scheduler workers workerId itemKey itemAction item
            atomically $ modifyTVar' workers (Map.insert workerId worker)
            putMVar startGate ()
            loop

  withAsync reader $ \_reader ->
    loop `finally` cancelWorkers

data KeyedSchedulerState key item = KeyedSchedulerState
  { inputDone :: !Bool,
    activeKeys :: !(Set.Set key),
    running :: !Int,
    pending :: !(Seq item),
    firstFailure :: !(Maybe SomeException)
  }

data SchedulerStep item
  = StartItem !item
  | SchedulerDone !(Maybe SomeException)

emptyKeyedSchedulerState :: KeyedSchedulerState key item
emptyKeyedSchedulerState =
  KeyedSchedulerState
    { inputDone = False,
      activeKeys = Set.empty,
      running = 0,
      pending = Seq.empty,
      firstFailure = Nothing
    }

runWorker ::
  (Ord key) =>
  TVar (KeyedSchedulerState key item) ->
  TVar (Map.Map Unique (Async ())) ->
  Unique ->
  (item -> Maybe key) ->
  (item -> IO ()) ->
  item ->
  IO ()
runWorker scheduler workers workerId itemKey itemAction item = do
  resultRef <- newIORef Nothing
  let runItem =
        (itemAction item >> pure Nothing)
          `catchAny` (pure . Just)
          >>= writeIORef resultRef
      cleanup = do
        result <- readIORef resultRef
        atomically $ finishItem scheduler itemKey item result
        atomically $ modifyTVar' workers (Map.delete workerId)
  runItem `finally` cleanup

enqueueItem ::
  Int ->
  TVar (KeyedSchedulerState key item) ->
  item ->
  IO ()
enqueueItem pendingLimit scheduler item =
  atomically $ do
    s <- readTVar scheduler
    if Seq.length s.pending >= pendingLimit
      then retry
      else writeTVar scheduler s {pending = s.pending Seq.|> item}

markInputDone ::
  TVar (KeyedSchedulerState key item) ->
  Maybe SomeException ->
  STM ()
markInputDone scheduler failure =
  modifyTVar' scheduler $ \s ->
    s
      { inputDone = True,
        firstFailure = s.firstFailure <|> failure
      }

nextSchedulerStep ::
  (Ord key) =>
  Int ->
  (item -> Maybe key) ->
  TVar (KeyedSchedulerState key item) ->
  STM (SchedulerStep item)
nextSchedulerStep maxConcurrency itemKey scheduler = do
  s <- readTVar scheduler
  case (s.running < maxConcurrency, popStartable itemKey s.activeKeys s.pending) of
    (True, Just (item, rest)) -> do
      writeTVar
        scheduler
        s
          { activeKeys = maybe s.activeKeys (`Set.insert` s.activeKeys) (itemKey item),
            running = s.running + 1,
            pending = rest
          }
      pure (StartItem item)
    _
      | s.inputDone && Seq.null s.pending && s.running == 0 ->
          pure (SchedulerDone s.firstFailure)
      | otherwise ->
          retry

finishItem ::
  (Ord key) =>
  TVar (KeyedSchedulerState key item) ->
  (item -> Maybe key) ->
  item ->
  Maybe SomeException ->
  STM ()
finishItem scheduler itemKey item failure =
  modifyTVar' scheduler $ \s ->
    s
      { activeKeys = maybe s.activeKeys (`Set.delete` s.activeKeys) (itemKey item),
        running = s.running - 1,
        firstFailure = s.firstFailure <|> failure
      }

popStartable ::
  (Ord key) =>
  (item -> Maybe key) ->
  Set.Set key ->
  Seq item ->
  Maybe (item, Seq item)
popStartable itemKey active = go Seq.empty
  where
    go skipped items =
      case Seq.viewl items of
        Seq.EmptyL ->
          Nothing
        item Seq.:< rest ->
          case itemKey item of
            Just key
              | key `Set.member` active ->
                  go (skipped Seq.|> item) rest
            _ ->
              Just (item, skipped <> rest)