packages feed

Spock-worker-0.1.0.2: src/Web/Spock/Worker/Queue.hs

{-# OPTIONS_GHC -F -pgmF htfpp #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Web.Spock.Worker.Queue
    ( WorkerQueue, newQueue, size, enqueue, dequeue, isFull
    , htf_thisModulesTests
    )
where

import Test.Framework

import Control.Monad
import Control.Concurrent
import Control.Concurrent.STM
import Control.Applicative
import Data.List (sortBy)
import Data.Ord (comparing)
import Data.Maybe

import qualified Data.Map.Strict as M
import qualified Data.Vector as V

data PureQueue p v
   = PureQueue
   { pq_container :: M.Map p (V.Vector v)
   , pq_maxSize :: Int
   }

emptyPQ :: Int -> PureQueue p v
emptyPQ maxSize =
    PureQueue M.empty maxSize

sizePQ :: PureQueue p v -> Int
sizePQ (PureQueue m _) =
    M.size m

isFullPQ :: PureQueue p v -> Bool
isFullPQ pq@(PureQueue _ maxSize) =
    sizePQ pq >= maxSize

toListPQ :: Ord p => PureQueue p v -> [(p, [v])]
toListPQ pq@(PureQueue m _) =
    map (\(k, v) -> (k, V.toList v)) (M.toList m)

fromListPQ :: Ord p => Int -> [(p, [v])] -> Maybe (PureQueue p v)
fromListPQ limit kv
    | length kv > limit = Nothing
    | otherwise =
        Just $
        foldl (\(PureQueue content _) (k, v) ->
                   (PureQueue (M.insert k (V.fromList v) content) limit)
              ) (emptyPQ limit) kv

maxPrioPQ :: Ord p => PureQueue p v -> p
maxPrioPQ (PureQueue m _) =
    fst (M.findMax m)

enqueuePQ :: Ord p => p -> v -> PureQueue p v -> (Bool, PureQueue p v)
enqueuePQ prio value pq@(PureQueue m _) =
    if isFullPQ pq
    then ( False, pq )
    else ( True
         , pq
           { pq_container =
                 M.insertWith (V.++) prio (V.singleton value) m
           }
         )

dequeuePQ :: Ord p => p -> PureQueue p v -> (Maybe v, PureQueue p v)
dequeuePQ bound pq =
    removePrio $ dequeuePQ' bound pq
    where
      removePrio (Nothing, q) = (Nothing, q)
      removePrio (Just (p, v), q) = (Just v, q)

dequeuePQ' :: Ord p => p -> PureQueue p v -> (Maybe (p, v), PureQueue p v)
dequeuePQ' prioBound pq@(PureQueue m _)
    | M.null m = (Nothing, pq)
    | minK > prioBound = (Nothing, pq)
    | otherwise =
        case V.toList vec of
          [workEl] ->
              (Just (minK, workEl), updatePQ (M.delete minK))
          (workEl:xs) ->
              (Just (minK, workEl), updatePQ (M.adjust (const (V.fromList xs)) minK))
          [] ->
              error "Library-Error: This should never happen."
    where
      (minK, vec) =
          M.findMin m
      updatePQ fun =
          pq { pq_container = fun (pq_container pq) }

newtype WorkerQueue p v =
    WorkerQueue (TVar (PureQueue p v))

newQueue :: Int -> IO (WorkerQueue p v)
newQueue limit =
    WorkerQueue <$> newTVarIO (emptyPQ limit)

size :: WorkerQueue p v -> STM Int
size (WorkerQueue qVar) =
    readTVar qVar >>= (return . sizePQ)

isFull :: WorkerQueue p v -> STM Bool
isFull (WorkerQueue qVar) =
    readTVar qVar >>= (return . isFullPQ)

enqueue :: Ord p => p -> v -> WorkerQueue p v -> STM ()
enqueue prio value (WorkerQueue qVar) =
    do q <- readTVar qVar
       let (ok, newQ) = enqueuePQ prio value q
       if ok
       then writeTVar qVar newQ
       else retry

dequeue :: Ord p => p -> WorkerQueue p v -> STM (Maybe v)
dequeue minP (WorkerQueue qVar) =
    do q <- readTVar qVar
       let (mVal, newQ) = dequeuePQ minP q
       when (isJust mVal) $ writeTVar qVar newQ
       return mVal

-- -------------
-- TESTS
-- -------------

tAddToMap k val m =
    M.insertWith (++) k [val] m
tDeq maxP q
    | sizePQ q == 0 =
        M.empty
    | otherwise =
        let (mVal, newQ) = dequeuePQ' maxP q
        in case mVal of
             Nothing ->
                 M.empty
             Just (k, val) ->
                 tAddToMap k val (tDeq maxP newQ)
tMappifyInput xs =
    foldl (\m (k, v) ->
               tAddToMap k v m
          ) M.empty xs

prop_enqueueDequeuePQ xs =
    let pq = foldl (\q (prio :: Int, el :: Int) ->
                        let (ok, newPQ) = enqueuePQ prio el q
                        in if ok then newPQ else (error "Failed to enqueue!")
                   ) (emptyPQ (length xs)) xs
        maxP = maxPrioPQ pq
    in (tMappifyInput xs == tDeq maxP pq)

prop_onlyDequeueBelowPrio prio xs =
    let xs' = M.toList $ tMappifyInput xs
        Just pq = fromListPQ (length xs) (xs' :: [(Int, [Int])])
        filtered = filter (\(p, _) -> p <= prio) xs
    in (tMappifyInput filtered == tDeq prio pq)

prop_isFull limit xs =
    limit > 0 ==>
    let xs' = M.toList $ tMappifyInput xs
        mPq = fromListPQ limit (xs' :: [(Int, [Int])])
    in case mPq of
         Just pq ->
             if limit == (length xs')
             then isFullPQ pq
             else limit > (length xs')
         Nothing ->
             limit < (length xs')

test_dontEnqueueIfFull =
    let pq = emptyPQ 0
        (ok, newPQ) = enqueuePQ (0 :: Int) False pq
    in assertBool (not ok)