packages feed

doc-review-0.7: src/State/Logger.hs

{-|
  Module that logs all state modifying actions to a file so that they
  can be reprocessed.
 -}
module State.Logger
    ( wrap
    , replay
    , foldLogFile
    , foldLogFile_
    , foldComments
    , foldMapComments
    )
where

import Control.Monad ( forever )
import System.IO.Error ( try )
import System.IO ( stderr, openBinaryFile, hPutStrLn, hClose, hFlush
                 , IOMode(AppendMode), Handle )
import Data.ByteString.Lazy as B
import Data.Binary.Get ( runGetState )
import Data.Binary ( Binary(..), getWord8, putWord8, encode )
import Control.Applicative ( (<$>), (<*>), pure )
import Control.Concurrent.MVar ( MVar, newMVar, modifyMVar )
import Control.Concurrent ( forkIO, threadDelay )
import Data.Monoid ( mempty, mappend, Monoid )
import Network.URI ( URI, parseRelativeReference )
import qualified Data.Text as T
import qualified Data.Text.Encoding as T

import State.Types

data Action = AddComment CommentId (Maybe ChapterId) Comment
            | AddChapter ChapterId [CommentId] (Maybe URI)
              deriving (Eq, Show)

instance Binary Action where
    get = do
      ty <- getWord8
      case ty of
        0xfe -> AddComment <$> get <*> get <*> get
        0xff -> AddChapter <$> get <*> get <*> pure Nothing
        0xfd -> AddChapter <$> get <*> get <*> (parseRelativeReference <$> get)
        _    -> error $ "Bad type code: " ++ show ty

    put (AddComment cId mChId c) =
        do putWord8 0xfe
           put cId
           put mChId
           put c
    put (AddChapter chId cs Nothing) =
        do putWord8 0xff
           put chId
           put cs
    put (AddChapter chId cs (Just uri)) =
        do putWord8 0xfd
           put chId
           put cs
           put $ T.encodeUtf8 $ T.pack $ show uri

-- Attempt to replay a log file, skipping corrupted sections of the file
foldLogFile :: Binary act => (act -> a -> IO a) -> a -> B.ByteString -> IO a
foldLogFile processAction initial = go initial
    where
      go x bs | B.null bs = return x
      go x bs = uncurry go =<< (step x bs `catch` \_ -> return (x, B.drop 1 bs))

      step x bs = do
        let (act, bs', _) = runGetState get bs 0
        x' <- processAction act x
        return (x', bs')

foldLogFile_ :: Binary act => (act -> IO ()) -> B.ByteString -> IO ()
foldLogFile_ f = foldLogFile (const . f) ()

replay :: State -> B.ByteString -> IO ()
replay st =
    foldLogFile_ $ \act ->
    case act of
      AddComment cId mChId c  -> addComment st cId mChId c
      AddChapter chId cs mURI -> addChapter st chId cs mURI

-- | Strict fold over the comments in the log file
foldComments :: (CommentId -> Maybe ChapterId -> Comment -> b -> b) -> b
             -> B.ByteString -> IO b
foldComments f =
    foldLogFile $ \act ->
    case act of
      AddChapter _ _ _       -> return
      AddComment cId mChId c -> \x -> let x' = f cId mChId c x
                                      in  x' `seq` return x'

foldMapComments :: Monoid b => (CommentId -> Maybe ChapterId -> Comment -> b)
                -> B.ByteString -> IO b
foldMapComments f =
    foldComments (\cId mChId c -> (f cId mChId c `mappend`)) mempty

-- |Wrap a store so that all of its modifying operations are logged to
-- a file. The file can later be replayed to restore the state.
wrap :: FilePath -> State -> IO State
wrap logFileName st = do
  ref <- newMVar Nothing
  forkIO $ rotateLog ref
  return st { addComment = \cId mChId c -> do
                             writeLog logFileName ref $ AddComment cId mChId c
                             addComment st cId mChId c

            , addChapter = \chId cs mURI -> do
                             writeLog logFileName ref $ AddChapter chId cs mURI
                             addChapter st chId cs mURI
            }

-- |Attempt to close the log file every five minutes so that we can
-- open new files and make sure that the entries are being written to
-- a mapped disk file
rotateLog :: MVar (Maybe Handle) -> IO ()
rotateLog v = forever $ do
                modifyMVar v $ \mh -> do
                          hPutStrLn stderr "Rotating"
                          maybe (return ()) closeLog mh
                          return (Nothing, ())

                -- reopen the log file every five minutes
                threadDelay $ 1000000 * 600

    where
      closeLog h = hClose h `catch` putErr "Failed to close log handle"

-- Write any errors to stderr so that we have a record of them
-- somewhere
putErr :: String -> IOError -> IO ()
putErr msg e = hPutStrLn stderr $ msg ++ ": " ++ show e

-- |Write a log entry to a file, opening the file for append if
-- necessary
writeLog :: FilePath -> MVar (Maybe Handle) -> Action -> IO ()
writeLog fn v act =
    modifyMVar v $ \mh -> do
      mh' <- getHandle mh >>= writeEntry
      return (mh', ())

    where
      -- If we don't have a handle, try to open one
      getHandle (Just h) = return $ Just h
      getHandle Nothing  = do
        res <- try $ openBinaryFile fn AppendMode
        case res of
          Right h -> return $ Just h
          Left e  -> do putErr "Failed to open log file" e
                        return Nothing

      -- Try to write the log entry, but don't raise an exception if
      -- we can't, so that we have the best chance of actually saving
      -- the data
      writeEntry Nothing  = return Nothing
      writeEntry (Just h) = do
        res <- try $ do
                 B.hPut h (encode act)
                 hFlush h
        case res of
          Right () -> return $ Just h
          Left e   -> do putErr "Failed to write log entry" e
                         hClose h `catch` putErr "Failed to close log file"
                         return Nothing