module Ganeti.ConfigReader
( ConfigReader
, initConfigReader
) where
import Control.Concurrent
import Control.Exception
import Control.Monad (unless)
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)
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)
)
onWatcherTimer :: FilePath -> (Result ConfigData -> IO ())
-> MVar ServerState -> IO ()
onWatcherTimer path save_fn state = do
threadDelay watchInterval
logDebug "Config-reader watcher timer fired"
modifyMVar_ state (onWatcherInner path save_fn)
onWatcherTimer 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 -> IO ()) -> IO ()
initConfigReader save_fn = do
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 conf_file save_fn statemvar
unless has_inotify $ do
_ <- forkIO $ onPollTimer inotiaction conf_file save_fn statemvar
return ()