module Ganeti.ConfigReader
( ConfigReader
, initConfigReader
) where
import Control.Concurrent
import Control.Exception
import Control.Monad (liftM, unless)
import Data.IORef
import System.Posix.Files
import System.Posix.Types
import System.INotify
import Ganeti.BasicTypes
import Ganeti.Objects
import Ganeti.Confd.Utils
import Ganeti.Config
import Ganeti.Logging
import qualified Ganeti.Constants as C
import qualified Ganeti.Path as Path
import Ganeti.Utils
type ConfigReader = IO (Result ConfigData)
type FStat = (EpochTime, FileID, FileOffset)
nullFStat :: FStat
nullFStat = (1, 1, 1)
data ReloadModel = ReloadNotify
| ReloadPoll Int
deriving (Eq, Show)
data ServerState = ServerState
{ reloadModel :: ReloadModel
, reloadTime :: Integer
, reloadFStat :: FStat
}
maxIdlePollRounds :: Int
maxIdlePollRounds = 3
watchInterval :: Int
watchInterval = C.confdConfigReloadTimeout * 1000000
pollInterval :: Int
pollInterval = C.confdConfigReloadRatelimit
reloadRatelimit :: Integer
reloadRatelimit = fromIntegral C.confdConfigReloadRatelimit
initialPoll :: ReloadModel
initialPoll = ReloadPoll 0
data ConfigReload = ConfigToDate
| ConfigReloaded
| ConfigIOError
deriving (Eq)
moveToPolling :: String -> INotify -> FilePath -> (Result ConfigData -> IO ())
-> MVar ServerState -> IO ReloadModel
moveToPolling msg inotify path save_fn mstate = do
logInfo $ "Moving to polling mode: " ++ msg
let inotiaction = addNotifier inotify path save_fn mstate
_ <- forkIO $ onPollTimer inotiaction path save_fn mstate
return initialPoll
moveToNotify :: IO ReloadModel
moveToNotify = do
logInfo "Moving to inotify mode"
return ReloadNotify
updateConfig :: FilePath -> (Result ConfigData -> IO ()) -> IO ()
updateConfig path save_fn = do
newcfg <- loadConfig path
let !newdata = case newcfg of
Ok !cfg -> Ok cfg
Bad msg -> Bad $ "Cannot load configuration from " ++ path
++ ": " ++ msg
save_fn newdata
case newcfg of
Ok cfg -> logInfo ("Loaded new config, serial " ++
show (configSerial cfg))
Bad msg -> logError $ "Failed to load config: " ++ msg
return ()
safeUpdateConfig :: FilePath -> FStat -> (Result ConfigData -> IO ())
-> IO (FStat, ConfigReload)
safeUpdateConfig path oldfstat save_fn =
Control.Exception.catch
(do
nt <- needsReload oldfstat path
case nt of
Nothing -> return (oldfstat, ConfigToDate)
Just nt' -> do
updateConfig path save_fn
return (nt', ConfigReloaded)
) (\e -> do
let msg = "Failure during configuration update: " ++
show (e::IOError)
save_fn $ Bad msg
return (nullFStat, ConfigIOError)
)
buildFileStatus :: FileStatus -> FStat
buildFileStatus ofs =
let modt = modificationTime ofs
inum = fileID ofs
fsize = fileSize ofs
in (modt, inum, fsize)
getFStat :: FilePath -> IO FStat
getFStat p = liftM buildFileStatus (getFileStatus p)
needsReload :: FStat -> FilePath -> IO (Maybe FStat)
needsReload oldstat path = do
newstat <- getFStat path
return $ if newstat /= oldstat
then Just newstat
else Nothing
onWatcherTimer :: IO Bool -> FilePath -> (Result ConfigData -> IO ())
-> MVar ServerState -> IO ()
onWatcherTimer inotiaction path save_fn state = do
threadDelay watchInterval
logDebug "Watcher timer fired"
modifyMVar_ state (onWatcherInner path save_fn)
_ <- inotiaction
onWatcherTimer inotiaction path save_fn state
onWatcherInner :: FilePath -> (Result ConfigData -> IO ()) -> ServerState
-> IO ServerState
onWatcherInner path save_fn state = do
(newfstat, _) <- safeUpdateConfig path (reloadFStat state) save_fn
return state { reloadFStat = newfstat }
onPollTimer :: IO Bool -> FilePath -> (Result ConfigData -> IO ())
-> MVar ServerState -> IO ()
onPollTimer inotiaction path save_fn state = do
threadDelay pollInterval
logDebug "Poll timer fired"
continue <- modifyMVar state (onPollInner inotiaction path save_fn)
if continue
then onPollTimer inotiaction path save_fn state
else logDebug "Inotify watch active, polling thread exiting"
onPollInner :: IO Bool -> FilePath -> (Result ConfigData -> IO ())
-> ServerState -> IO (ServerState, Bool)
onPollInner _ _ _ state@(ServerState { reloadModel = ReloadNotify } ) =
return (state, False)
onPollInner inotiaction path save_fn
state@(ServerState { reloadModel = ReloadPoll pround } ) = do
(newfstat, reload) <- safeUpdateConfig path (reloadFStat state) save_fn
let state' = state { reloadFStat = newfstat }
newmode <- case reload of
ConfigToDate ->
if pround >= maxIdlePollRounds
then do
result <- inotiaction
if result
then moveToNotify
else return initialPoll
else return (ReloadPoll (pround + 1))
_ -> return initialPoll
let continue = case newmode of
ReloadNotify -> False
_ -> True
return (state' { reloadModel = newmode }, continue)
addNotifier :: INotify -> FilePath -> (Result ConfigData -> IO ())
-> MVar ServerState -> IO Bool
addNotifier inotify path save_fn mstate =
Control.Exception.catch
(addWatch inotify [CloseWrite] path
(onInotify inotify path save_fn mstate) >> return True)
(\e -> const (return False) (e::IOError))
onInotify :: INotify -> String -> (Result ConfigData -> IO ())
-> MVar ServerState -> Event -> IO ()
onInotify inotify path save_fn mstate Ignored = do
logDebug "File lost, trying to re-establish notifier"
modifyMVar_ mstate $ \state -> do
result <- addNotifier inotify path save_fn mstate
(newfstat, _) <- safeUpdateConfig path (reloadFStat state) save_fn
let state' = state { reloadFStat = newfstat }
if result
then return state'
else do
mode <- moveToPolling "cannot re-establish inotify watch" inotify
path save_fn mstate
return state' { reloadModel = mode }
onInotify inotify path save_fn mstate _ =
modifyMVar_ mstate $ \state ->
if reloadModel state == ReloadNotify
then do
ctime <- getCurrentTimeUSec
(newfstat, _) <- safeUpdateConfig path (reloadFStat state) save_fn
let state' = state { reloadFStat = newfstat, reloadTime = ctime }
if abs (reloadTime state ctime) < reloadRatelimit
then do
mode <- moveToPolling "too many reloads" inotify path save_fn
mstate
return state' { reloadModel = mode }
else return state'
else return state
initConfigReader :: (Result ConfigData -> a) -> IORef a -> IO ()
initConfigReader cfg_transform ioref = do
let save_fn = writeIORef ioref . cfg_transform
inotify <- initINotify
conf_file <- Path.clusterConfFile
(fstat, reloaded) <- safeUpdateConfig conf_file nullFStat save_fn
ctime <- getCurrentTime
statemvar <- newMVar $ ServerState ReloadNotify ctime fstat
let inotiaction = addNotifier inotify conf_file save_fn statemvar
has_inotify <- if reloaded == ConfigReloaded
then inotiaction
else return False
if has_inotify
then logInfo "Starting up in inotify mode"
else do
logInfo "Starting up in polling mode"
modifyMVar_ statemvar
(\state -> return state { reloadModel = initialPoll })
_ <- forkIO $ onWatcherTimer inotiaction conf_file save_fn statemvar
unless has_inotify $ do
_ <- forkIO $ onPollTimer inotiaction conf_file save_fn statemvar
return ()