pgmq-inspect-0.7.0.0: test/RefreshSpec.hs
module RefreshSpec (tests) where
import Control.Concurrent (threadDelay)
import Control.Concurrent.Async
import Control.Concurrent.MVar
import Control.Concurrent.STM
import Control.Exception (finally)
import Control.Monad (forM_, replicateM, replicateM_, void)
import Data.Aeson (encode)
import Data.Map.Strict qualified as Map
import Data.Text (Text)
import Data.Time (UTCTime (..), fromGregorian)
import Data.Word (Word64)
import Effectful (liftIO)
import Effectful.Dispatch.Dynamic (interpret)
import ErrorSpec (serverError)
import Pgmq.Effectful.Effect qualified as E
import Pgmq.Hasql.Statements.Types qualified as H
import Pgmq.Inspect.Budget
import Pgmq.Inspect.Config
import Pgmq.Inspect.Env
import Pgmq.Inspect.Frames
import Pgmq.Inspect.Outbox
import Pgmq.Inspect.Refresh
import Pgmq.Types (UnvalidatedQueue (..))
import System.Timeout (timeout)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit
cfg :: InspectConfig
cfg = defaultInspectConfig {pollIntervalUs = 200000, metricsMinRefreshUs = 100000, notifyDebounceUs = 20000}
epoch :: UTCTime
epoch = UTCTime (fromGregorian 2026 1 1) 0
metrics :: Text -> Int -> H.QueueMetrics
metrics name count = H.QueueMetrics name (fromIntegral count) Nothing Nothing (fromIntegral count) epoch (fromIntegral count) Nothing
data Probe = Probe
{calls :: TVar (Map.Map Text Int), live :: TVar (Map.Map Text Int), peak :: TVar Int}
newProbe :: IO Probe
newProbe = Probe <$> newTVarIO Map.empty <*> newTVarIO Map.empty <*> newTVarIO 0
probeEnv :: Probe -> (Text -> Int -> IO H.QueueMetrics) -> InspectEnv
probeEnv probe readOne =
InspectEnv
( runnerFromInterpreter
( interpret
( \_ -> \case
E.ListQueuesUnvalidated -> pure [UnvalidatedQueue name epoch False False | name <- ["orders", "other"]]
E.QueueMetricsUnvalidated name -> liftIO $ do
index <- atomically $ do
counts <- readTVar (calls probe)
let index = Map.findWithDefault 0 name counts + 1
writeTVar (calls probe) (Map.insert name index counts)
modifyTVar' (live probe) (Map.insertWith (+) name 1)
active <- sum . Map.elems <$> readTVar (live probe)
modifyTVar' (peak probe) (max active)
pure index
readOne name index `finally` atomically (modifyTVar' (live probe) (Map.adjust (subtract 1) name))
_ -> error "RefreshSpec unexpected effect"
)
)
)
Nothing
await :: STM a -> IO a
await action = timeout 2000000 (atomically action) >>= maybe (assertFailure "coordinator barrier timed out") pure
next :: Subscriber -> IO ServerFrame
next subscriber = snd <$> await (takeOutgoing (subscriberOutbox subscriber))
callCount :: Probe -> Text -> STM Int
callCount probe name = Map.findWithDefault 0 name <$> readTVar (calls probe)
settled :: Probe -> MetricsCoordinator -> Text -> Int -> IO ()
settled probe coordinator name expected = await $ do
n <- callCount probe name
check (n == expected)
coordinatorInFlight coordinator >>= check . (== 0)
withClock :: InspectConfig -> InspectEnv -> Maybe PushHooks -> (TVar Word64 -> MetricsCoordinator -> IO a) -> IO a
withClock config env hooks k = do
clock <- newTVarIO 1000000
withInspectionBudget config $ \budget -> withCoordinatorUsing (readTVarIO clock) config env budget hooks (k clock)
tests :: TestTree
tests =
testGroup
"WebSocket shared coordinator"
[ testCase "100 subscriptions share one in-flight snapshot and one metrics read per poll" $ do
probe <- newProbe
started <- newEmptyMVar
release <- newEmptyMVar
let env =
probeEnv
probe
( \name n -> do
if n == 1 then putMVar started () >> takeMVar release else pure ()
pure (metrics name 0)
)
withClock cfg env Nothing $ \clock coordinator -> do
subscribers <- replicateM 100 (newSubscriber 4)
subscribeQueue coordinator (subscribers !! 0) "orders"
takeMVar started
forM_ (drop 1 subscribers) $ \subscriber -> subscribeQueue coordinator subscriber "orders"
atomically (coordinatorSubscriberCount coordinator "orders") >>= (@?= 100)
putMVar release ()
forM_ subscribers $ \subscriber -> next subscriber >>= (\case Snapshot "orders" _ False -> pure (); frame -> assertFailure (show frame))
settled probe coordinator "orders" 1
-- Re-subscription reuses shared current metrics and cannot bypass the floor.
replicateM_ 100 (subscribeQueue coordinator (subscribers !! 0) "orders")
atomically (callCount probe "orders") >>= (@?= 1)
atomically (writeTVar clock 1200000)
settled probe coordinator "orders" 2
forM_ (drop 1 subscribers) $ \subscriber -> atomically (queuedFrames (subscriberOutbox subscriber)) >>= (@?= 0),
testCase "hints coalesce globally and obey the minimum interval even when a poll is due" $ do
probe <- newProbe
hint <- newEmptyMVar
let hooks = PushHooks (\_ callback -> putMVar hint callback >> pure (pure ()))
withClock cfg (probeEnv probe (\name _ -> pure (metrics name 0))) (Just hooks) $ \clock coordinator -> do
a <- newSubscriber 4
b <- newSubscriber 4
subscribeQueue coordinator a "orders"
subscribeQueue coordinator b "orders"
next a >>= (\case Snapshot _ _ True -> pure (); frame -> assertFailure (show frame))
void (next b)
notify <- takeMVar hint
atomically (replicateM_ 100 notify >> writeTVar clock 1050000)
threadDelay 30000
atomically (callCount probe "orders") >>= (@?= 1)
atomically (writeTVar clock 1200000)
forM_ [a, b] $ \subscriber -> next subscriber >>= (\case Update _ _ NotifySource -> pure (); frame -> assertFailure (show frame))
settled probe coordinator "orders" 2,
testCase "a slow queue cannot overlap itself or starve another queue" $ do
probe <- newProbe
started <- newEmptyMVar
release <- newEmptyMVar
let env =
probeEnv
probe
( \name n -> do
if name == "orders" && n == 1 then putMVar started () >> takeMVar release else pure ()
pure (metrics name n)
)
withClock cfg env Nothing $ \clock coordinator -> do
a <- newSubscriber 8
b <- newSubscriber 8
subscribeQueue coordinator a "orders"
takeMVar started
subscribeQueue coordinator b "other"
void (next b)
atomically (writeTVar clock 1300000)
void (next b)
atomically (callCount probe "orders") >>= (@?= 1)
atomically (readTVar (peak probe)) >>= (\n -> assertBool "global bound" (n <= maxConcurrentReads cfg))
active <- atomically (Map.findWithDefault 0 "orders" <$> readTVar (live probe))
active @?= 1
putMVar release (),
testCase "unsubscribe/re-subscribe discards stale completion and preserves refresh floor" $ do
probe <- newProbe
started <- newEmptyMVar
release <- newEmptyMVar
let env =
probeEnv
probe
( \name n -> do
if n == 1 then putMVar started () >> takeMVar release else pure ()
pure (metrics name n)
)
withClock cfg env Nothing $ \clock coordinator -> do
subscriber <- newSubscriber 4
subscribeQueue coordinator subscriber "orders"
takeMVar started
unsubscribeQueue coordinator subscriber "orders"
subscribeQueue coordinator subscriber "orders"
putMVar release ()
settled probe coordinator "orders" 1
atomically (queuedFrames (subscriberOutbox subscriber)) >>= (@?= 0)
atomically (writeTVar clock 1200000)
frame <- next subscriber
case frame of
Snapshot "orders" actual False -> encode actual @?= encode (metrics "orders" 2)
_ -> assertFailure (show frame),
testCase "HTTP-held admission reports busy and keeps a single bounded retry" $ do
probe <- newProbe
clock <- newTVarIO 1000000
started <- newEmptyMVar
release <- newEmptyMVar
let config = cfg {maxConcurrentReads = 1}
withInspectionBudget config $ \budget ->
withAsync (withInspectionPermit budget (putMVar started () >> takeMVar release)) $ \held -> do
takeMVar started
withCoordinatorUsing (readTVarIO clock) config (probeEnv probe (\name n -> pure (metrics name n))) budget Nothing $ \coordinator -> do
subscriber <- newSubscriber 4
subscribeQueue coordinator subscriber "orders"
next subscriber >>= (\case ErrorFrame "inspection_busy" _ _ -> pure (); frame -> assertFailure (show frame))
atomically (callCount probe "orders") >>= (@?= 0)
putMVar release ()
void (wait held)
atomically (writeTVar clock 1200000)
next subscriber >>= (\case Snapshot _ _ _ -> pure (); frame -> assertFailure (show frame))
settled probe coordinator "orders" 1,
testCase "statement deadline is in-band and a later poll recovers" $ do
probe <- newProbe
attempts <- newTVarIO (0 :: Int)
let base = probeEnv probe (\name n -> pure (metrics name n))
env =
base
{ runInspection = \action -> do
n <- atomically $ do
current <- readTVar attempts
writeTVar attempts (current + 1)
pure current
if n == 0 then pure (Left (serverError "57014")) else runInspection base action
}
withClock cfg env Nothing $ \clock coordinator -> do
subscriber <- newSubscriber 4
subscribeQueue coordinator subscriber "orders"
next subscriber >>= (\case ErrorFrame "inspection_timeout" _ _ -> pure (); frame -> assertFailure (show frame))
atomically (writeTVar clock 1200000)
next subscriber >>= (\case Snapshot _ _ False -> pure (); frame -> assertFailure (show frame)),
testCase "snapshot publication precedes an update even when a hint races initial metrics" $ do
probe <- newProbe
beforePublish <- newEmptyMVar
release <- newEmptyMVar
let hooks = PushHooks (\_ hint -> putMVar beforePublish hint >> takeMVar release >> pure (pure ()))
withClock cfg (probeEnv probe (\name n -> pure (metrics name n))) (Just hooks) $ \clock coordinator -> do
subscriber <- newSubscriber 4
subscribeQueue coordinator subscriber "orders"
hint <- takeMVar beforePublish
atomically (hint >> writeTVar clock 1300000)
atomically (queuedFrames (subscriberOutbox subscriber)) >>= (@?= 0)
putMVar release ()
next subscriber >>= (\case Snapshot _ actual True -> encode actual @?= encode (metrics "orders" 1); frame -> assertFailure (show frame))
next subscriber >>= (\case Update _ actual _ -> encode actual @?= encode (metrics "orders" 2); frame -> assertFailure (show frame)),
testCase "partitioned metadata is poll-only even when a push hook exists" $ do
registered <- newTVarIO False
let env =
InspectEnv
( runnerFromInterpreter
( interpret
( \_ -> \case
E.ListQueuesUnvalidated -> pure [UnvalidatedQueue "orders" epoch True False]
E.QueueMetricsUnvalidated name -> pure (metrics name 0)
_ -> error "unexpected partition test effect"
)
)
)
Nothing
hooks = PushHooks (\_ _ -> atomically (writeTVar registered True) >> pure (pure ()))
withClock cfg env (Just hooks) $ \_ coordinator -> do
subscriber <- newSubscriber 4
subscribeQueue coordinator subscriber "orders"
next subscriber >>= (\case Snapshot _ _ False -> pure (); frame -> assertFailure (show frame))
readTVarIO registered >>= (@?= False)
]