packages feed

glue-core-0.8.0.0: test/Glue/PeriodicSpec.hs

{-# LANGUAGE ScopedTypeVariables #-}

module Glue.PeriodicSpec (spec) where

import           Control.Concurrent       (yield)
import           Control.Concurrent.Async (async, wait)
import           Data.IORef
import           Glue.Clock
import           Glue.Clock.Virtual
import           Glue.Periodic
import           Test.Hspec
import           UnliftIO.Exception       (Exception, SomeException,
                                           fromException, throwIO)
import           UnliftIO.STM             (atomically, newEmptyTMVarIO,
                                           putTMVar, readTMVar)

data PassFailed = PassFailed
  deriving (Eq, Show)

instance Exception PassFailed

waitUntil :: IO Bool -> IO ()
waitUntil condition = condition >>= \done -> if done then pure () else yield >> waitUntil condition

options :: VirtualClock -> PeriodicOptions IO
options vc = defaultPeriodicOptions { periodMs = 100, periodicGracePeriodMs = 50, periodicClock = virtualClock vc }

spec :: Spec
spec = do
  describe "periodically" $ do
    it "Runs a pass every period, measured from the end of the previous pass" $ do
      vc     <- newVirtualClock
      starts <- newIORef []
      let pass = do
            now <- virtualNow vc
            atomicModifyIORef' starts (\s -> (s ++ [now], ()))
            clockSleep (virtualClock vc) 30000
      handle <- periodically (options vc) pass
      waitForSleepers vc 1
      advanceClock vc 100000
      waitForSleepers vc 1
      advanceClock vc 30000
      waitUntil ((== 1) <$> passesCompleted handle)
      waitForSleepers vc 1
      advanceClock vc 100000
      waitForSleepers vc 1
      advanceClock vc 30000
      waitUntil ((== 2) <$> passesCompleted handle)
      readIORef starts `shouldReturn` [100000, 230000]
      stopPeriodic handle `shouldReturn` True

    it "Runs the first pass immediately when asked" $ do
      vc <- newVirtualClock
      handle <- periodically (options vc) { runImmediately = True } (pure ())
      waitUntil ((== 1) <$> passesCompleted handle)
      virtualNow vc `shouldReturn` 0
      stopPeriodic handle `shouldReturn` True

    it "Adds jitter from the random source to the period" $ do
      vc <- newVirtualClock
      handle <- periodically (options vc) { periodJitterMs = 40, periodicRandom = \n -> pure (n `div` 2) } (pure ())
      waitForSleepers vc 1
      advanceClock vc 119000
      passesCompleted handle `shouldReturn` 0
      advanceClock vc 1000
      waitUntil ((== 1) <$> passesCompleted handle)
      stopPeriodic handle `shouldReturn` True

    it "Reports a failed pass and keeps running" $ do
      vc       <- newVirtualClock
      failures <- newIORef (0 :: Int)
      calls    <- newIORef (0 :: Int)
      let pass = do
            n <- atomicModifyIORef' calls (\c -> (c + 1, c + 1))
            if n == 1 then throwIO PassFailed else pure ()
          onFailed (e :: SomeException) = case fromException e of
            Just PassFailed -> atomicModifyIORef' failures (\f -> (f + 1, ()))
            Nothing         -> pure ()
      handle <- periodically (options vc) { onPassFailed = onFailed } pass
      runNow handle `shouldReturn` True
      runNow handle `shouldReturn` True
      readIORef failures `shouldReturn` 1
      readIORef calls `shouldReturn` 2
      passesCompleted handle `shouldReturn` 2
      stopPeriodic handle `shouldReturn` True

    it "runNow queues behind a running pass" $ do
      vc    <- newVirtualClock
      gate  <- newEmptyTMVarIO
      calls <- newIORef (0 :: Int)
      let pass = do
            n <- atomicModifyIORef' calls (\c -> (c + 1, c + 1))
            if n == 1 then atomically (readTMVar gate) else pure ()
      handle <- periodically (options vc) { runImmediately = True } pass
      waitUntil ((== 1) <$> readIORef calls)
      queued <- async (runNow handle)
      atomically $ putTMVar gate ()
      wait queued `shouldReturn` True
      readIORef calls `shouldReturn` 2
      stopPeriodic handle `shouldReturn` True

    it "Stopping lets a running pass finish within the grace period" $ do
      vc       <- newVirtualClock
      gate     <- newEmptyTMVarIO
      started  <- newEmptyTMVarIO
      finished <- newIORef False
      let pass = do
            atomically $ putTMVar started ()
            atomically $ readTMVar gate
            writeIORef finished True
      handle <- periodically (options vc) { runImmediately = True } pass
      atomically $ readTMVar started
      stopping <- async (stopPeriodic handle)
      waitForSleepers vc 1
      atomically $ putTMVar gate ()
      wait stopping `shouldReturn` True
      readIORef finished `shouldReturn` True

    it "Stopping cancels a pass that outlasts the grace period" $ do
      vc        <- newVirtualClock
      started   <- newEmptyTMVarIO
      finished  <- newIORef False
      let pass = do
            atomically $ putTMVar started ()
            clockSleep (virtualClock vc) 1000000
            writeIORef finished True
      handle <- periodically (options vc) { runImmediately = True } pass
      atomically $ readTMVar started
      stopping <- async (stopPeriodic handle)
      waitForSleepers vc 2
      advanceClock vc 50000
      wait stopping `shouldReturn` False
      readIORef finished `shouldReturn` False
      runNow handle `shouldReturn` False