packages feed

polysemy-conc-0.15.0.0: test/Polysemy/Conc/Test/MonitorTest.hs

{-# options_ghc -fplugin=Polysemy.Plugin #-}

module Polysemy.Conc.Test.MonitorTest where

import Control.Concurrent (threadDelay)
import qualified Data.Text as Text
import Data.Time (UTCTime)
import Polysemy.Test (TestError (TestError), UnitTest, assertEq, assertJust, runTestAuto)
import qualified Polysemy.Time as Time
import Polysemy.Time (
  GhcTime,
  Hours (Hours),
  MilliSeconds (MilliSeconds),
  Minutes (Minutes),
  NanoSeconds (NanoSeconds),
  Seconds (Seconds),
  Time,
  TimeUnit,
  convert,
  interpretTimeGhc,
  interpretTimeGhcConstantNow,
  )
import Torsor (difference)

import Polysemy.Conc.AtomicState (interpretAtomic)
import qualified Polysemy.Conc.Effect.Monitor as Monitor
import Polysemy.Conc.Effect.Monitor (Monitor, MonitorCheck (MonitorCheck), Restart)
import qualified Polysemy.Conc.Effect.Sync as Sync
import Polysemy.Conc.Effect.Sync (Sync)
import Polysemy.Conc.Interpreter.Monitor (interpretMonitorRestart)
import Polysemy.Conc.Interpreter.Race (interpretRace)
import Polysemy.Conc.Interpreter.Sync (interpretSync)
import Polysemy.Conc.Monitor (clockSkewConfig, monitorClockSkew)

prog ::
  Members [Monitor Restart, Time t d, AtomicState Int, Sync ()] r =>
  Sem r Int
prog = do
  Monitor.monitor do
    atomicModify' (1 +)
    i <- atomicGet
    when (i <= 2) do
      Sync.putBlock ()
      Time.sleep (Hours 1)
    pure i

checker ::
  Members [Time t d, Sync (), Embed IO] r =>
  Sem r Bool
checker =
  True <$ Sync.takeBlock

test_monitorBasic :: UnitTest
test_monitorBasic =
  runTestAuto $
  asyncToIOFinal $
  interpretRace $
  interpretTimeGhc $
  interpretAtomic 0 $
  interpretSync $
  interpretMonitorRestart (MonitorCheck (NanoSeconds 0) checker) do
    assertEq 3 =<< Monitor.restart prog

locked ::
  Members [AtomicState [Text], Error TestError, Embed IO] r =>
  Sem r a
locked = do
  embed (threadDelay 1_000_000)
  msgs <- atomicGet
  throw (TestError (Text.unlines ("Locked after:" : reverse msgs)))

takeBlock ::
  ∀ label r .
  HasCallStack =>
  Members [AtomicState [Text], Sync (Proxy label), Error TestError, Embed IO] r =>
  Text ->
  Sem r ()
takeBlock desc =
  withFrozenCallStack do
    atomicModify' (("taking '" <> desc <> "'") :)
    whenM (isNothing <$> Sync.takeWait (Seconds 1)) locked

putBlock ::
  ∀ label r .
  HasCallStack =>
  Members [AtomicState [Text], Sync (Proxy label), Error TestError, Embed IO] r =>
  Text ->
  Sem r ()
putBlock desc =
  withFrozenCallStack do
    atomicModify' (("putting '" <> desc <> "'") :)
    unlessM (Sync.putWait (Seconds 1) Proxy) locked

progSkew ::
  Members [Sync (Proxy "start"), Sync (Proxy "1"), Sync (Proxy "2"), Sync (Proxy "3"), Embed IO] r =>
  Members [Monitor Restart, AtomicState [Text], AtomicState Int, Error TestError, Time t d] r =>
  Sem r Int
progSkew =
  Monitor.monitor do
    atomicModify' (1 +)
    putBlock @"start" "start"
    takeBlock @"1" "1"
    putBlock @"2" "2"
    takeBlock @"3" "3"
    atomicGet

sleepPoll ::
  Members [GhcTime, Embed IO] r =>
  TimeUnit u =>
  u ->
  UTCTime ->
  Sem r ()
sleepPoll duration start =
  spin
  where
    spin = do
      embed (threadDelay 10_000)
      unlessM (later <$> Time.now) spin
    later now =
      difference now start >= diff
    diff =
      convert duration

interceptTime ::
  ∀ r a .
  Members [AtomicState [Text], GhcTime, Sync (Proxy "sleep"), Error TestError, Embed IO] r =>
  Sem r a ->
  Sem r a
interceptTime =
  intercept \case
    Time.Now -> do
      atomicModify' ("now" :)
      Time.now
    Time.Today ->
      Time.today
    Time.Sleep t -> do
      atomicModify' ("sleep" :)
      now <- Time.now
      putBlock @"sleep" "sleep"
      sleepPoll t now
      atomicModify' ("slept" :)
    Time.SetTime now ->
      Time.setTime now
    Time.Adjust (diff :: u) -> do
      atomicModify' ("adjust" :)
      send (Time.Adjust diff)
    Time.SetDate startAt ->
      Time.setDate startAt

test_monitorClockSkew :: UnitTest
test_monitorClockSkew =
  runTestAuto $
  asyncToIOFinal $
  interpretRace $
  interpretAtomic (0 :: Int) $
  interpretAtomic ([] :: [Text]) $
  interpretAtomic @(Maybe UTCTime) Nothing $
  interpretSync @(Proxy "start") $
  interpretSync @(Proxy "sleep") $
  interpretSync @(Proxy "1") $
  interpretSync @(Proxy "2") $
  interpretSync @(Proxy "3") $
  interpretTimeGhcConstantNow $
  interceptTime $
  interpretMonitorRestart monitor do
    h <- async (Monitor.restart progSkew)
    takeBlock @"start" "start 1"
    takeBlock @"sleep" "sleep"
    Time.adjust (Hours 1)
    _ <- takeBlock @"start" "start 2"
    takeBlock @"sleep" "sleep"
    Time.adjust (Minutes 10)
    putBlock @"1" "1 1"
    takeBlock @"2" "2 1"
    takeBlock @"sleep" "sleep"
    Time.adjust (Hours 1)
    _ <- takeBlock @"start" "start 3"
    putBlock @"1" "1 2"
    takeBlock @"2" "2 2"
    putBlock @"3" "3"
    assertJust 3 =<< await h
  where
    monitor = monitorClockSkew (clockSkewConfig (MilliSeconds 1) (Minutes 30))