1 {-# LANGUAGE BangPatterns #-}
3 {-| Implementation of configuration reader with watching support.
9 Copyright (C) 2011, 2012, 2013 Google Inc.
11 This program is free software; you can redistribute it and/or modify
12 it under the terms of the GNU General Public License as published by
13 the Free Software Foundation; either version 2 of the License, or
14 (at your option) any later version.
16 This program is distributed in the hope that it will be useful, but
17 WITHOUT ANY WARRANTY; without even the implied warranty of
18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
19 General Public License for more details.
21 You should have received a copy of the GNU General Public License
22 along with this program; if not, write to the Free Software
23 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
28 module Ganeti.ConfigReader
33 import Control.Concurrent
34 import Control.Exception
35 import Control.Monad (liftM, unless)
37 import System.Posix.Files
38 import System.Posix.Types
41 import Ganeti.BasicTypes
43 import Ganeti.Confd.Utils
46 import qualified Ganeti.Constants as C
47 import qualified Ganeti.Path as Path
50 -- | A type for functions that can return the configuration when
52 type ConfigReader = IO (Result ConfigData)
54 -- | File stat identifier.
55 type FStat = (EpochTime, FileID, FileOffset)
57 -- | Null 'FStat' value.
59 nullFStat = (-1, -1, -1)
61 -- | Reload model data type.
62 data ReloadModel = ReloadNotify -- ^ We are using notifications
63 | ReloadPoll Int -- ^ We are using polling
66 -- | Server state data type.
67 data ServerState = ServerState
68 { reloadModel :: ReloadModel
69 , reloadTime :: Integer -- ^ Reload time (epoch) in microseconds
70 , reloadFStat :: FStat
73 -- | Maximum no-reload poll rounds before reverting to inotify.
74 maxIdlePollRounds :: Int
77 -- | Reload timeout in microseconds.
79 watchInterval = C.confdConfigReloadTimeout * 1000000
81 -- | Ratelimit timeout in microseconds.
83 pollInterval = C.confdConfigReloadRatelimit
85 -- | Ratelimit timeout in microseconds, as an 'Integer'.
86 reloadRatelimit :: Integer
87 reloadRatelimit = fromIntegral C.confdConfigReloadRatelimit
89 -- | Initial poll round.
90 initialPoll :: ReloadModel
91 initialPoll = ReloadPoll 0
93 -- | Reload status data type.
94 data ConfigReload = ConfigToDate -- ^ No need to reload
95 | ConfigReloaded -- ^ Configuration reloaded
96 | ConfigIOError -- ^ Error during configuration reload
99 -- * Configuration handling
101 -- ** Helper functions
103 -- | Helper function for logging transition into polling mode.
104 moveToPolling :: String -> INotify -> FilePath -> (Result ConfigData -> IO ())
105 -> MVar ServerState -> IO ReloadModel
106 moveToPolling msg inotify path save_fn mstate = do
107 logInfo $ "Moving to polling mode: " ++ msg
108 let inotiaction = addNotifier inotify path save_fn mstate
109 _ <- forkIO $ onPollTimer inotiaction path save_fn mstate
112 -- | Helper function for logging transition into inotify mode.
113 moveToNotify :: IO ReloadModel
115 logInfo "Moving to inotify mode"
118 -- ** Configuration loading
120 -- | (Re)loads the configuration.
121 updateConfig :: FilePath -> (Result ConfigData -> IO ()) -> IO ()
122 updateConfig path save_fn = do
123 newcfg <- loadConfig path
124 let !newdata = case newcfg of
126 Bad msg -> Bad $ "Cannot load configuration from " ++ path
130 Ok cfg -> logInfo ("Loaded new config, serial " ++
131 show (configSerial cfg))
132 Bad msg -> logError $ "Failed to load config: " ++ msg
135 -- | Wrapper over 'updateConfig' that handles IO errors.
136 safeUpdateConfig :: FilePath -> FStat -> (Result ConfigData -> IO ())
137 -> IO (FStat, ConfigReload)
138 safeUpdateConfig path oldfstat save_fn =
139 Control.Exception.catch
141 nt <- needsReload oldfstat path
143 Nothing -> return (oldfstat, ConfigToDate)
145 updateConfig path save_fn
146 return (nt', ConfigReloaded)
148 let msg = "Failure during configuration update: " ++
151 return (nullFStat, ConfigIOError)
154 -- | Computes the file cache data from a FileStatus structure.
155 buildFileStatus :: FileStatus -> FStat
156 buildFileStatus ofs =
157 let modt = modificationTime ofs
160 in (modt, inum, fsize)
162 -- | Wrapper over 'buildFileStatus'. This reads the data from the
163 -- filesystem and then builds our cache structure.
164 getFStat :: FilePath -> IO FStat
165 getFStat p = liftM buildFileStatus (getFileStatus p)
167 -- | Check if the file needs reloading
168 needsReload :: FStat -> FilePath -> IO (Maybe FStat)
169 needsReload oldstat path = do
170 newstat <- getFStat path
171 return $ if newstat /= oldstat
175 -- ** Watcher threads
178 -- We have three threads/functions that can mutate the server state:
180 -- 1. the long-interval watcher ('onWatcherTimer')
182 -- 2. the polling watcher ('onPollTimer')
184 -- 3. the inotify event handler ('onInotify')
186 -- All of these will mutate the server state under 'modifyMVar' or
187 -- 'modifyMVar_', so that server transitions are more or less
188 -- atomic. The inotify handler remains active during polling mode, but
189 -- checks for polling mode and doesn't do anything in this case (this
190 -- check is needed even if we would unregister the event handler due
191 -- to how events are serialised).
193 -- | Long-interval reload watcher.
195 -- This is on top of the inotify-based triggered reload.
196 onWatcherTimer :: IO Bool -> FilePath -> (Result ConfigData -> IO ())
197 -> MVar ServerState -> IO ()
198 onWatcherTimer inotiaction path save_fn state = do
199 threadDelay watchInterval
200 logDebug "Watcher timer fired"
201 modifyMVar_ state (onWatcherInner path save_fn)
203 onWatcherTimer inotiaction path save_fn state
205 -- | Inner onWatcher handler.
207 -- This mutates the server state under a modifyMVar_ call. It never
208 -- changes the reload model, just does a safety reload and tried to
209 -- re-establish the inotify watcher.
210 onWatcherInner :: FilePath -> (Result ConfigData -> IO ()) -> ServerState
212 onWatcherInner path save_fn state = do
213 (newfstat, _) <- safeUpdateConfig path (reloadFStat state) save_fn
214 return state { reloadFStat = newfstat }
216 -- | Short-interval (polling) reload watcher.
218 -- This is only active when we're in polling mode; it will
219 -- automatically exit when it detects that the state has changed to
221 onPollTimer :: IO Bool -> FilePath -> (Result ConfigData -> IO ())
222 -> MVar ServerState -> IO ()
223 onPollTimer inotiaction path save_fn state = do
224 threadDelay pollInterval
225 logDebug "Poll timer fired"
226 continue <- modifyMVar state (onPollInner inotiaction path save_fn)
228 then onPollTimer inotiaction path save_fn state
229 else logDebug "Inotify watch active, polling thread exiting"
231 -- | Inner onPoll handler.
233 -- This again mutates the state under a modifyMVar call, and also
234 -- returns whether the thread should continue or not.
235 onPollInner :: IO Bool -> FilePath -> (Result ConfigData -> IO ())
236 -> ServerState -> IO (ServerState, Bool)
237 onPollInner _ _ _ state@(ServerState { reloadModel = ReloadNotify } ) =
238 return (state, False)
239 onPollInner inotiaction path save_fn
240 state@(ServerState { reloadModel = ReloadPoll pround } ) = do
241 (newfstat, reload) <- safeUpdateConfig path (reloadFStat state) save_fn
242 let state' = state { reloadFStat = newfstat }
243 -- compute new poll model based on reload data; however, failure to
244 -- re-establish the inotifier means we stay on polling
245 newmode <- case reload of
247 if pround >= maxIdlePollRounds
248 then do -- try to switch to notify
249 result <- inotiaction
252 else return initialPoll
253 else return (ReloadPoll (pround + 1))
254 _ -> return initialPoll
255 let continue = case newmode of
256 ReloadNotify -> False
258 return (state' { reloadModel = newmode }, continue)
260 -- the following hint is because hlint doesn't understand our const
261 -- (return False) is so that we can give a signature to 'e'
262 {-# ANN addNotifier "HLint: ignore Evaluate" #-}
263 -- | Setup inotify watcher.
265 -- This tries to setup the watch descriptor; in case of any IO errors,
266 -- it will return False.
267 addNotifier :: INotify -> FilePath -> (Result ConfigData -> IO ())
268 -> MVar ServerState -> IO Bool
269 addNotifier inotify path save_fn mstate =
270 Control.Exception.catch
271 (addWatch inotify [CloseWrite] path
272 (onInotify inotify path save_fn mstate) >> return True)
273 (\e -> const (return False) (e::IOError))
275 -- | Inotify event handler.
276 onInotify :: INotify -> String -> (Result ConfigData -> IO ())
277 -> MVar ServerState -> Event -> IO ()
278 onInotify inotify path save_fn mstate Ignored = do
279 logDebug "File lost, trying to re-establish notifier"
280 modifyMVar_ mstate $ \state -> do
281 result <- addNotifier inotify path save_fn mstate
282 (newfstat, _) <- safeUpdateConfig path (reloadFStat state) save_fn
283 let state' = state { reloadFStat = newfstat }
285 then return state' -- keep notify
287 mode <- moveToPolling "cannot re-establish inotify watch" inotify
289 return state' { reloadModel = mode }
291 onInotify inotify path save_fn mstate _ =
292 modifyMVar_ mstate $ \state ->
293 if reloadModel state == ReloadNotify
295 ctime <- getCurrentTimeUSec
296 (newfstat, _) <- safeUpdateConfig path (reloadFStat state) save_fn
297 let state' = state { reloadFStat = newfstat, reloadTime = ctime }
298 if abs (reloadTime state - ctime) < reloadRatelimit
300 mode <- moveToPolling "too many reloads" inotify path save_fn
302 return state' { reloadModel = mode }
306 initConfigReader :: (Result ConfigData -> a) -> IORef a -> IO ()
307 initConfigReader cfg_transform ioref = do
308 let save_fn = writeIORef ioref . cfg_transform
311 inotify <- initINotify
312 -- try to load the configuration, if possible
313 conf_file <- Path.clusterConfFile
314 (fstat, reloaded) <- safeUpdateConfig conf_file nullFStat save_fn
315 ctime <- getCurrentTime
316 statemvar <- newMVar $ ServerState ReloadNotify ctime fstat
317 let inotiaction = addNotifier inotify conf_file save_fn statemvar
318 has_inotify <- if reloaded == ConfigReloaded
322 then logInfo "Starting up in inotify mode"
324 -- inotify was not enabled, we need to update the reload model
325 logInfo "Starting up in polling mode"
326 modifyMVar_ statemvar
327 (\state -> return state { reloadModel = initialPoll })
328 -- fork the timeout timer
329 _ <- forkIO $ onWatcherTimer inotiaction conf_file save_fn statemvar
330 -- fork the polling timer
331 unless has_inotify $ do
332 _ <- forkIO $ onPollTimer inotiaction conf_file save_fn statemvar