module Snap.Snaplet.TypedSession.SessionMap (
SessionMap,
new,
close,
insert,
update,
delete,
lookup,
touch
) where
import Prelude hiding (lookup, catch)
import Control.Concurrent
import Control.Exception
import Control.Monad
import Data.ByteString (ByteString)
import Data.Time
import Data.Typeable
import Snap.Snaplet.Session (RNG, mkRNG, randomToken)
import qualified Data.HashTable.IO as HT
import qualified Data.PSQueue as PQ
data SessionMap v = SessionMap
!(MVar (Maybe (PQ.PSQ ByteString UTCTime)))
!(HT.BasicHashTable ByteString v)
!ThreadId
!RNG
!Int
data PokeWatcher = PokeWatcher deriving (Typeable, Show)
instance Exception PokeWatcher
new :: Int -> IO (SessionMap v)
new to = do
q <- newMVar (Just PQ.empty)
ht <- HT.new
w <- forkIO (doWatcher q ht)
gen <- mkRNG
return (SessionMap q ht w gen to)
doWatcher :: MVar (Maybe (PQ.PSQ ByteString UTCTime))
-> HT.BasicHashTable ByteString v
-> IO ()
doWatcher q ht = mask $ \_ -> do
nxt <- modifyMVar q (cleanExpired ht)
case nxt of
Nothing -> return ()
Just del -> do threadDelay del
`catch` \PokeWatcher -> return ()
doWatcher q ht
cleanExpired :: HT.BasicHashTable ByteString v
-> Maybe (PQ.PSQ ByteString UTCTime)
-> IO (Maybe (PQ.PSQ ByteString UTCTime), Maybe Int)
cleanExpired _ Nothing = return (Nothing, Nothing)
cleanExpired ht (Just q) = case PQ.minView q of
Nothing -> return (Just q, Just maxBound)
Just (k PQ.:-> e, q') -> do
t <- getCurrentTime
if e <= t then do
HT.delete ht k
cleanExpired ht (Just q')
else return (Just q, Just (round (1000000 * diffUTCTime e t)))
withOpenMap :: MVar (Maybe (PQ.PSQ ByteString UTCTime))
-> (PQ.PSQ ByteString UTCTime -> IO (Maybe (PQ.PSQ ByteString UTCTime), a))
-> IO a
withOpenMap qq f = modifyMVar qq go
where go Nothing = error "Session map is already closed"
go (Just q) = f q
close :: SessionMap v -> IO ()
close (SessionMap qq ht w _ _) = withOpenMap qq $ \_ -> do
throwTo w PokeWatcher
vals <- HT.toList ht
forM_ vals $ \(k,_) -> HT.delete ht k
return (Nothing, ())
insert :: SessionMap v -> v -> IO ByteString
insert m@(SessionMap qq ht _ gen _) v = withOpenMap qq $ \q -> do
k <- uniqueKey gen ht
(q', _) <- update' m k v q
return (q', k)
uniqueKey :: RNG -> HT.BasicHashTable ByteString v -> IO ByteString
uniqueKey gen ht = do
k <- randomToken 40 gen
maybe (return k) (const (uniqueKey gen ht)) =<< HT.lookup ht k
update :: SessionMap v -> ByteString -> v -> IO ()
update m@(SessionMap q _ _ _ _) k v =
withOpenMap q (update' m k v)
update' :: SessionMap v -> ByteString -> v
-> PQ.PSQ ByteString UTCTime
-> IO (Maybe (PQ.PSQ ByteString UTCTime), ())
update' m@(SessionMap _ ht _ _ _) k v q = do
HT.insert ht k v
touch' m k q
delete :: SessionMap v -> ByteString -> IO ()
delete (SessionMap qq ht _ _ _) k = withOpenMap qq $ \q -> do
HT.delete ht k
return (Just (PQ.delete k q), ())
lookup :: SessionMap v -> ByteString -> IO (Maybe v)
lookup (SessionMap qq ht _ _ _) k = withOpenMap qq $ \q -> do
v <- HT.lookup ht k
return (Just q, v)
touch :: SessionMap v -> ByteString -> IO ()
touch m@(SessionMap q _ _ _ _) k =
withOpenMap q (touch' m k)
touch' :: SessionMap v -> ByteString
-> PQ.PSQ ByteString UTCTime
-> IO (Maybe (PQ.PSQ ByteString UTCTime), ())
touch' (SessionMap _ ht w _ to) k q = do
throwTo w PokeWatcher
t <- getCurrentTime
let p = addUTCTime (fromIntegral to) t
ans <- HT.lookup ht k
case ans of
Nothing -> return (Just q, ())
Just _ -> return (Just (PQ.insert k p q), ())