jenkins-bot has submitted this change and it was merged.

Change subject: Drop inotify; improve documentation and handling of 
configuration formats
......................................................................


Drop inotify; improve documentation and handling of configuration formats

* Although the way we use inotify works well in common cases, it is not
  exhaustive in covering all possible scenarios, and making it exhaustive would
  be impractical. So poll `os.stat` every second instead to watch the file for
  modifications. This approach is more crude, but it correctly handles a
  broader range of possible cases.

* Do not require the server name to be specified both as top-level key and a
  key within the server-specific configuration dict.

* Document the configuration file formats we handle.

Change-Id: Id031310287db7ccd6c31e77183a913b6db4f4004
---
M pybal/config.py
M pybal/pybal.py
2 files changed, 71 insertions(+), 30 deletions(-)

Approvals:
  Ori.livneh: Looks good to me, approved
  BBlack: Looks good to me, but someone else must approve
  jenkins-bot: Verified



diff --git a/pybal/config.py b/pybal/config.py
index cbe6cbf..56c44f7 100644
--- a/pybal/config.py
+++ b/pybal/config.py
@@ -11,10 +11,11 @@
 import ast
 import json
 import logging
+import os
+import re
 
-from twisted.internet import inotify, defer, reactor, task
-from twisted.names import client, dns
-from twisted.python import failure, filepath
+from twisted.internet import task
+from twisted.python import log
 
 
 def get_subclasses(cls):
@@ -42,28 +43,57 @@
 
 
 class FileConfigurationObserver(ConfigurationObserver):
-    """ConfigurationObserver for local configuration files."""
+    """ConfigurationObserver for local configuration files.
+
+    Handles the 'file://' scheme.
+    For example: 'file:///etc/pybal/pools/apache'.
+
+    If the file name ends in '.json', treat it as a new-style configuration
+    file, and expect the following format:
+
+        {
+          "pybal-test2002.codfw.wmnet": {
+            "enabled": false,
+            "weight": 10
+          },
+          "pybal-test2003.codfw.wmnet": {
+            "enabled": true,
+            "weight": 5
+          }
+        }
+
+    If the file name does NOT end in '.json', treat it as an old-style (eval)
+    configuration file, and expect the following format:
+
+        { 'host': 'pybal-test2002.codfw.wmnet', 'weight':10, 'enabled': True }
+        { 'host': 'pybal-test2003.codfw.wmnet', 'weight':10, 'enabled': True }
+
+    """
 
     urlScheme = 'file://'
 
-    def __init__(self, coordinator, configUrl):
+    def __init__(self, coordinator, configUrl, reloadIntervalSeconds=1):
         self.coordinator = coordinator
-        self.filePath = filepath.FilePath(configUrl[len(self.urlScheme):])
-        self.notifier = inotify.INotify()
-        self.notifier.startReading()
-        self.notifier.watch(filepath.FilePath(self.filePath.dirname()),
-                            mask=(inotify.IN_MODIFY| inotify.IN_ATTRIB),
-                            callbacks=[self.onNotify])
-        self.reloadConfig()
+        self.configUrl = configUrl
+        self.filePath = configUrl[len(self.urlScheme):]
+        self.reloadIntervalSeconds = reloadIntervalSeconds
+        self.lastFileStat = None
+        self.lastConfig = None
+        self.reloadTask = task.LoopingCall(self.reloadConfig)
+        self.startObserving()
 
-    def onNotify(self, ignored, filePath, mask):
-        if filePath == self.filePath:
-            try:
-                self.reloadConfig()
-            except Exception:
-                logging.exception('Unable to reload config!')
-                task.deferLater(reactor, 1, self.onNotify, ignored,
-                                filePath, mask)
+    def startObserving(self):
+        """Start (or re-start) watching the configuration file for changes."""
+        self.reloadTask \
+            .start(self.reloadIntervalSeconds) \
+            .addErrback(self.logError)
+
+    def logError(self, failure):
+        """Log an error and re-schedule the configuration file monitor."""
+        failure.trap(Exception)
+        log.err(failure)
+        self.fileStat = None
+        self.startObserving()
 
     def parseLegacyConfig(self, fp):
         """Parse a legacy (eval) configuration file."""
@@ -74,10 +104,13 @@
                 if not line or line.startswith('#'):
                     continue
                 server = ast.literal_eval(line)
-                host = server['host']
+                host = server.pop('host')
                 config[host] = server
-            except (KeyError, SyntaxError, TypeError, ValueError):
-                logging.exception('Bad configuration line: %s', line)
+            except (KeyError, SyntaxError, TypeError, ValueError) as ex:
+                # We catch exceptions here (rather than simply allow them to
+                # bubble up to FileConfigurationObserver.logError) because we
+                # want to try and parse as much of the file as we can.
+                log.err(ex, 'Bad configuration line: %s' % line)
                 continue
         return config
 
@@ -86,9 +119,17 @@
         return json.load(fp)
 
     def reloadConfig(self):
-        with self.filePath.open() as f:
-            if self.filePath.path.endswith('.json'):
+        """If the configuration file has changed, re-read it. If the parsed
+        configuration object has changed, notify the coordinator."""
+        fileStat = os.stat(self.filePath)
+        if fileStat == self.lastFileStat:
+            return
+        self.lastFileStat = fileStat
+        with open(self.filePath, 'rt') as f:
+            if self.filePath.endswith('.json'):
                 config = self.parseJsonConfig(f)
             else:
                 config = self.parseLegacyConfig(f)
-        self.coordinator.onConfigUpdate(config)
+        if config != self.lastConfig:
+            self.coordinator.onConfigUpdate(config)
+            self.lastConfig = config
diff --git a/pybal/pybal.py b/pybal/pybal.py
index 3f37ad7..93e9373 100644
--- a/pybal/pybal.py
+++ b/pybal/pybal.py
@@ -246,14 +246,14 @@
         self.modified = True    # Indicate that this instance previously 
existed
 
     @classmethod
-    def buildServer(cls, configuration, lvsservice):
+    def buildServer(cls, hostName, configuration, lvsservice):
         """
         Factory method which builds a Server instance from a
         dictionary of (allowed) configuration attributes
         """
 
-        server = cls(configuration['host'], lvsservice) # create a new 
instance...
-        server.merge(configuration)                     # ...and override 
attributes
+        server = cls(hostName, lvsservice) # create a new instance...
+        server.merge(configuration)        # ...and override attributes
         server.modified = False
 
         return server
@@ -386,7 +386,7 @@
                 print self, "Merged %s server %s, weight %d" % (server.enabled 
and "enabled" or "disabled", hostName, server.weight)
             else:
                 # New server
-                server = Server.buildServer(hostConfig, self.lvsservice)
+                server = Server.buildServer(hostName, hostConfig, 
self.lvsservice)
                 # Initialize with LVS service specific configuration
                 self.lvsservice.initServer(server)
                 self.servers[hostName] = server

-- 
To view, visit https://gerrit.wikimedia.org/r/233034
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: Id031310287db7ccd6c31e77183a913b6db4f4004
Gerrit-PatchSet: 1
Gerrit-Project: operations/debs/pybal
Gerrit-Branch: master
Gerrit-Owner: Ori.livneh <[email protected]>
Gerrit-Reviewer: BBlack <[email protected]>
Gerrit-Reviewer: Giuseppe Lavagetto <[email protected]>
Gerrit-Reviewer: Mark Bergsma <[email protected]>
Gerrit-Reviewer: Ori.livneh <[email protected]>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to