cql-io-1.1.0: src/test/Test/Database/CQL/IO/Jobs.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
module Test.Database.CQL.IO.Jobs (tests) where
import Control.Concurrent
import Control.Concurrent.Async
import Control.Exception
import Data.IORef
import Data.Either
import Data.Maybe
import Data.Numbers.Primes
import Test.Tasty
import Test.Tasty.HUnit
import Database.CQL.IO.Jobs
tests :: TestTree
tests = testGroup "Database.CQL.IO.Jobs"
[ testCase "run-sequential" testRunJobSequential
, testCase "run-concurrent" testRunJobConcurrent
, testCase "try-run-sequential" testTryRunJobSequential
, testCase "try-run-concurrent" testTryRunJobConcurrent
, testCase "try-run-replace" testTryRunJobReplace
]
-----------------------------------------------------------------------------
-- Tests
-- 1. Sequentially run jobs with the same key, each waiting on a latch.
-- 2. Release all latches.
-- 3. Expect only the last job to complete and all others to be replaced.
testRunJobSequential :: IO ()
testRunJobSequential = do
jobs <- newJobs
iref <- newIORef (1 :: Int)
let prepare = prepareRun jobs . mulJob iref
(fails, succs) <- execute mapM prepare (take 100 primes)
val <- readIORef iref
map fromException fails @?= replicate 99 (Just JobReplaced)
succs @?= [()]
val @?= 541 -- 100th prime => 100th job
-- 1. Concurrently run jobs with the same key, each waiting on a latch.
-- 2. Release all latches.
-- 3. Expect only one (non-deterministic) job to complete and all others to
-- be replaced.
testRunJobConcurrent :: IO ()
testRunJobConcurrent = do
jobs <- newJobs
iref <- newIORef (0 :: Int)
let prepare = prepareRun jobs . incJob iref
(fails, succs) <- execute mapConcurrently prepare [1..100 :: Int]
val <- readIORef iref
map fromException fails @?= replicate 99 (Just JobReplaced)
succs @?= [()]
val @?= 1
-- 1. Try to run jobs for the same key, each waiting on a latch.
-- 2. Release all latches.
-- 3. Expect only the first job to run.
testTryRunJobSequential :: IO ()
testTryRunJobSequential = do
jobs <- newJobs
iref <- newIORef (1 :: Int)
let prepare = prepareTryRun jobs . mulJob iref
(fails, succs) <- execute mapM prepare (take 100 primes)
val <- readIORef iref
length fails @?= 0
succs @?= [()]
val @?= 2
-- 1. Concurrently try to run jobs for the same key, each waiting on a latch.
-- 2. Release all latches.
-- 3. Expect only one job to run.
testTryRunJobConcurrent :: IO ()
testTryRunJobConcurrent = do
jobs <- newJobs
iref <- newIORef (0 :: Int)
let prepare = prepareTryRun jobs . incJob iref
(fails, succs) <- execute mapConcurrently prepare [1..100]
val <- readIORef iref
length fails @?= 0
succs @?= [()]
val @?= 1
-- 1. Concurrently try to run jobs for the same key, each waiting on
-- a latch. One of the jobs is run unconditionally and should
-- thus always take precedence.
-- 2. Release all latches.
-- 3. Expect only the unconditionally run job to complete,
-- replacing one job. All others are never run.
testTryRunJobReplace :: IO ()
testTryRunJobReplace = do
jobs <- newJobs
iref <- newIORef (1 :: Int)
let prepare = choose jobs (mulJob iref)
(Just a1, l1) <- prepare 2 -- there should always be a job to replace
(fails, succs) <- execute mapConcurrently prepare (take 99 (drop 1 primes))
putMVar l1 ()
r1 <- either Just (const Nothing) <$> waitCatch a1
val <- readIORef iref
(fromException =<< r1) @?= Just JobReplaced
length fails @?= 0
succs @?= [()]
val @?= 229 -- 50th's prime is unconditionally run
where
choose jobs run i
| i == 229 = prepareRun jobs (run i)
| otherwise = prepareTryRun jobs (run i)
-----------------------------------------------------------------------------
-- Test Jobs
type PrepareJob a = a -> IO (Maybe (Async ()), MVar ())
type ExecuteJob a = IORef a -> a -> IO ()
jobKey :: Int
jobKey = 1
prepareRun :: Jobs Int -> IO () -> IO (Maybe (Async ()), MVar ())
prepareRun jobs run = do
l <- newEmptyMVar
a <- runJob jobs jobKey $ takeMVar l >> run
return (Just a, l)
prepareTryRun :: Jobs Int -> IO () -> IO (Maybe (Async ()), MVar ())
prepareTryRun jobs run = do
l <- newEmptyMVar
a <- tryRunJob jobs jobKey $ takeMVar l >> run
return (a, l)
execute
:: (forall a b. (a -> IO b) -> [a] -> IO [b])
-> PrepareJob i
-> [i]
-> IO ([SomeException], [()])
execute runIO prepJob input = do
js <- runIO prepJob input
as <- runIO (\(a, l) -> putMVar l () *> pure a) js
partitionEithers <$> mapM waitCatch (catMaybes as)
incJob :: ExecuteJob Int
incJob iref _ = atomicModifyIORef' iref (\x -> (x + 1, ()))
mulJob :: ExecuteJob Int
mulJob iref i = atomicModifyIORef' iref (\x -> (x * i, ()))