packages feed

Quelea-1.0.0: Quelea/ShimLayer/GC.hs

{-# LANGUAGE ScopedTypeVariables, TemplateHaskell, DoAndIfThenElse, BangPatterns  #-}

module Quelea.ShimLayer.GC (
  maybeGCCache,
  gcWorker
) where

import Quelea.Consts
import Quelea.Types
import Quelea.ShimLayer.Types
import Quelea.DBDriver
import Quelea.ShimLayer.UpdateFetcher

import Control.Monad.State
import Control.Lens
import qualified Data.Set as S
import qualified Data.Map as M
import Control.Concurrent (threadDelay, myThreadId)
import Control.Concurrent.MVar
import System.Random (randomIO)
import Database.Cassandra.CQL
import Control.Applicative ((<$>))
import Data.Maybe (fromJust)
import Data.Time

makeLenses ''CacheManager
makeLenses ''DatatypeLibrary

-- #define DEBUG

debugPrint :: String -> IO ()
#ifdef DEBUG
debugPrint s = do
  tid <- myThreadId
  putStrLn $ "[" ++ (show tid) ++ "] " ++ s
#else
debugPrint _ = return ()
#endif


data VisitedState = Visited Bool  -- Boolean indicates whether the effect is resolved
                  | NotVisited (S.Set Addr)

data ResolutionState = ResolutionState {
  _keyCursor    :: M.Map SessID SeqNo,
  _visitedState :: M.Map Addr VisitedState
}

makeLenses ''ResolutionState

{- How to GC a transactional effect?
 -----------------------------------------
 - When GCing a set of effects {a@t1,b@t2} belonging to transactions t1 and t2,
 - one needs to ensure that the transaction markers are preserved after the GC.
 - Assume c is the new effect that summarizes a and b, and GCM is the gc
 - marker. Now the final state after GC will be:

    {c} --introduced by--> {GCM@{t1,t2}} --GCs--> {a,b}

 - where the set of transactions of the effects that were GCed are included in
 - the GC marker. Now, if c is included in any shim layer node, the
 - transactions t1 and t2 also need to be processed.
 -}


gcDB :: CacheManager -> ObjType -> Key -> GenSumFun -> IO ()
gcDB cm ot k gc = gcDBCore cm ot k gc 1

gcDBCore :: CacheManager -> ObjType -> Key -> GenSumFun -> Int -> IO ()
gcDBCore cm ot k gc 0 = return ()
gcDBCore cm ot k gc repeat = do
  debugPrint $ "gcDB: start"
  -- Allocate new session id
  gcSid <- SessID <$> randomIO
  -- Get GC lock
  getGCLock ot k gcSid $ cm^.pool
  -- Get time at the start of GC
  currentTime <- getCurrentTime
  let gcTime = currentTime
  lgctMap <- readMVar $ cm^.lastGCTimeMVar
  rows <- case M.lookup (ot,k) lgctMap of
            Nothing -> runCas (cm^.pool) $ cqlReadWithTime ot ALL k
            Just lastGCTime -> runCas (cm^.pool) $ cqlReadAfterTimeWithTime ot ALL k lastGCTime
  -- Split the rows into effects and gc markers
  let (effRows, gcMarker) = foldl (\(effAcc,gcAcc) (sid,sqn,time,deps,val,txnid) ->
        case txnid of
          Just _ -> (effAcc, gcAcc) -- XXX KC; Handle transactions
          Nothing ->
            case val of
              EffectVal bs -> ((sid,sqn,time,deps,bs):effAcc, gcAcc)
              GCMarker _ -> case gcAcc of
                            Nothing -> (effAcc, Just (sid,sqn,time,deps))
                            Just _ -> error "Multiple GC Markers") ([], Nothing) rows
  -- Build the GC cursor
  let gcCursor = case gcMarker of
                   Nothing -> M.empty
                   Just (sid, sqn, time, deps) ->
                     S.foldl (\m (Addr sid sqn) -> M.insert sid sqn m)
                             (M.singleton sid sqn) deps
  -- Build datastructure for filtering out unresolved effects
  let (effSet, depsMap) =
        foldl (\(setAcc, mapAcc) (sid, sqn, time, deps, eff) ->
                  (S.insert (Addr sid sqn, eff, time) setAcc,
                   M.insert (Addr sid sqn) (NotVisited deps) mapAcc))
          (S.empty, M.empty) effRows
  -- filter unresolved effects i,e) effects whose causal cut is also not visible.
  let filteredSet = filterUnresolved gcCursor depsMap effSet
  let (addrList, effList, timeList) = unzip3 (S.toList filteredSet)
  let gcedEffList = gc effList
  -- Allocate new gc address
  let gcAddr = Addr gcSid 1
  let (outRows, count) =
        foldl (\(acc, idx) eff ->
                  ((gcSid, idx, S.singleton gcAddr, EffectVal eff, Nothing):acc, idx+1))
              ([],2) gcedEffList
  runCas (cm^.pool) $ do
    -- Insert new effects
    mapM_ (\r -> cqlInsert ot ALL k r) outRows
    -- Delete previous marker if it exists
    case gcMarker of
      Nothing -> return ()
      Just (sid, sqn, time, _) -> cqlDelete ot k time sid sqn
    -- Insert marker
    let am = foldl (\m (Addr sid sqn) ->
               case M.lookup sid m of
                 Nothing -> M.insert sid sqn m
                 Just oldSqn -> M.insert sid (max oldSqn sqn) m) M.empty addrList
    let newCursor = M.unionWith max am gcCursor
    let newDeps = S.fromList $ map (\(sid,sqn) -> Addr sid sqn) $ M.toList newCursor
    cqlInsert ot ALL k (gcSid, 1, newDeps, GCMarker gcTime, Nothing)
    -- Update lastGCTime before deleting
    liftIO $ do
      lgctMap <- takeMVar $ cm^.lastGCTimeMVar
      putMVar (cm^.lastGCTimeMVar) $ M.insert (ot,k) gcTime lgctMap
    -- Delete old rows
    mapM_ (\(Addr sid sqn, time) -> cqlDelete ot k time sid sqn) $ zip addrList timeList
  {- info -}
  debugPrint $ "gcDB: inserted Rows=" ++ show (length outRows)
  debugPrint $ "gcDB: deleted Rows=" ++ show (length addrList)
  releaseGCLock ot k gcSid $ cm^.pool
  {- Remove the current object from diskRowCount map -}
  drc <- takeMVar $ cm^.diskRowCntMVar
  putMVar (cm^.diskRowCntMVar) $ M.delete (ot,k) drc
  debugPrint $ "gcDB: end"
  gcDBCore cm ot k gc $ repeat - 1

maybeGCCache :: CacheManager -> ObjType -> Key -> Int -> GenSumFun -> IO ()
maybeGCCache cm ot k curSize gc | curSize < cCACHE_LWM = return ()
                                | otherwise = do
  hwmMap <- readMVar $ cm^.hwmMVar
  let hwm = case M.lookup (ot,k) hwmMap of
              Nothing -> cCACHE_LWM
              Just x -> x
  when (curSize > hwm) $ do
    cache <- takeMVar $ cm^.cacheMVar
    let ctxt = case M.lookup (ot, k) cache of
                Nothing -> []
                Just s -> map (\(a,e) -> e) $ S.toList s
    let !newCtxt = gc ctxt
    newUUID <- SessID <$> randomIO
    let (newCache,_) = foldl (\(s,i) e -> (S.insert (Addr newUUID i, e) s, i+1)) (S.empty, 1) newCtxt
    hwm <- takeMVar $ cm^.hwmMVar
    putMVar (cm^.hwmMVar) $ M.insert (ot, k) (length newCtxt * 2) hwm
    putMVar (cm^.cacheMVar) $ M.insert (ot, k) newCache cache

filterUnresolved :: CursorAtKey                   -- Key Cursor
                 -> M.Map Addr VisitedState       -- Input visited state
                 -> S.Set (Addr, Effect, UTCTime) -- Unfiltered input
                 -> S.Set (Addr, Effect, UTCTime) -- Filtered result
filterUnresolved kc m1 s2 =
  let addrList = M.keys m1
      ResolutionState _ vs =
        foldl (\vs addr -> execState (isResolved addr) vs) (ResolutionState kc m1) addrList
  in S.filter (\(addr,_,_) ->
        case M.lookup addr vs of
          Nothing -> error "filterUnresolved: unexpected state(1)"
          Just (Visited True) -> True
          Just (Visited False) -> False
          Just (NotVisited _) -> error "filterUnresolved: unexpected state(2)") s2

isResolved :: Addr -> State ResolutionState Bool
isResolved addr = do
  vs <- use visitedState
  case M.lookup addr vs of
    Nothing -> do -- Might be an effect already in the cache
      let Addr sid sqn = addr
      if sqn == 0 -- Special case to please Cassandra
      then do
        visitedState .= M.insert addr (Visited True) vs
        return True
      else do
        kc <- use keyCursor
        case M.lookup sid kc of
          -- Session of effect not found in cache => effect unseen
          Nothing -> do
            visitedState .= M.insert addr (Visited False) vs
            return False
          -- Session found in cache, cache is sufficiently recent?
          Just maxSqn -> do
            let res = sqn <= maxSqn
            visitedState .= M.insert addr (Visited res) vs
            return res
    Just (Visited res) -> return res
    Just (NotVisited deps) -> do
      res <- foldM (\acc addr -> isResolved addr >>= \r -> return (r && acc)) True $ S.toList deps
      newVs <- use visitedState
      visitedState .= M.insert addr (Visited res) newVs
      return res

gcWorker :: OperationClass a => DatatypeLibrary a -> CacheManager -> IO ()
gcWorker dtLib cm = forever $ do
  threadDelay cGC_WORKER_THREAD_DELAY
  drc <- readMVar $ cm^.diskRowCntMVar
  let todoObjs = M.foldlWithKey (\todoObjs (ot,k) rowCount ->
                                    if rowCount > cDISK_LWM
                                    then (ot,k):todoObjs
                                    else todoObjs) [] drc
  mapM_ (\(ot,k) ->
    let gcFun = case dtLib ^. sumMap ^.at ot of {Nothing -> error "gcWorker"; Just x -> x}
    in gcDB cm ot k gcFun) todoObjs