packages feed

heni-0.1.0.0: src/Test/Heni/Internal/Baseline.hs

-- | Campaign-local single-flight baseline sharing. Keys may contain secrets and
-- are never serialized or hashed for portable reports.
module Test.Heni.Internal.Baseline (BaselineCache, newBaselineCache, clearBaselineCache, shareBaseline) where

import Control.Concurrent.MVar
import Control.Exception

newtype BaselineCache key value = BaselineCache (MVar [(key, MVar (Maybe value))])

newBaselineCache :: IO (BaselineCache key value)
newBaselineCache = BaselineCache <$> newMVar []

clearBaselineCache :: BaselineCache key value -> IO ()
clearBaselineCache (BaselineCache cache) = modifyMVar_ cache (const (pure []))

-- | The producer returns Nothing for an unshareable observation. Waiters then
-- execute independently. Cancellation publishes Nothing before rethrowing so
-- another worker can never remain blocked on abandoned ownership.
shareBaseline :: (Eq key) => BaselineCache key value -> key -> IO (Maybe value) -> IO (Bool, Maybe value)
shareBaseline (BaselineCache cache) key compute = mask $ \restore -> do
  (owner, slot) <- modifyMVar cache $ \entries -> case lookup key entries of
    Just existing -> pure (entries, (False, existing))
    Nothing -> do fresh <- newEmptyMVar; pure ((key, fresh) : entries, (True, fresh))
  if owner
    then do
      result <- try (restore compute)
      case result of
        Left (e :: SomeException) -> putMVar slot Nothing >> throwIO e
        Right value -> putMVar slot value >> pure (False, value)
    else do
      cached <- restore (readMVar slot)
      case cached of
        Just value -> pure (True, Just value)
        Nothing -> (False,) <$> restore compute