import asyncio
import json
import logging
import time
log = logging.getLogger(__name__)
[docs]class Services:
FILE = '/hipchat/config/sidecar-discovery.json'
def __init__(self, path=FILE):
self.services = None
self.json = None
self.last_updated = None
self.last_changed = None
self.path = path
self.started = False
self._notifier = None
def __del__(self):
"""Destructor to ensure kernel data is freed when object goes out of scope."""
if self._notifier:
self._notifier.stop()
[docs] def get(self, name):
"""Returns a list of netlocs (host:port pairs) for the service 'name'.
Returns None if the service is not known.
Returns an empty list if there are now hosts for the service.
Raises RuntimeError if data hasn't been loaded yet. (See reload() or start().)
"""
if self.services is None:
raise RuntimeError("Can't call get() before data is loaded.")
return self.services.get(name)
[docs] def keys(self):
if self.services is None:
return []
return self.services.keys()
@asyncio.coroutine
[docs] def start(self, loop=None):
"""Starts the file watcher looking for updates in the file."""
self.reload()
try:
import pyinotify
except ImportError:
log.exception("WARNING: failed ot load pyinotify! Will not observe updates to %s", self.path)
else:
if not self.started:
def file_modified(notifier):
self.reload()
wm = pyinotify.WatchManager()
loop = loop or asyncio.get_event_loop()
self._notifier = pyinotify.AsyncioNotifier(wm, loop, callback=file_modified)
wm.add_watch(self.path, pyinotify.IN_CLOSE_WRITE)
[docs] def reload(self):
"""Reloads (or first time loads) the json data from self.path.
Keeps the stale data if there's a problem reloading.
Raises ValueError if there's a problem with initial load.
"""
if self.services is None:
self._first_load()
else:
self._later_load()
def _first_load(self):
if self.services is not None:
raise RuntimeError("Can't call first_load more than once.")
log.debug("Loading from %s", self.path)
try:
raw_json, parsed_json = self._get_data()
except (ValueError, OSError) as e:
raise ValueError("Couldn't load services from %s", self.path) from e
else:
now = time.time()
log.debug("Initial service list:\n%s", raw_json)
self.services, self.json = parsed_json, raw_json
self.last_updated = self.last_changed = now
def _later_load(self):
if self.services is None:
raise RuntimeError("Can't call _later_load before _first_load succeeded.")
log.debug("Reloading from %s", self.path)
try:
raw_json, parsed_json = self._get_data()
except (ValueError, OSError):
log.exception("Could not load updated %s; using stale data last updated %s (%s sec ago)",
self.path, time.ctime(self.last_updated), int(time.time() - self.last_updated))
else:
now = time.time()
if parsed_json == self.services:
log.debug("Nothing changed")
self.last_updated = now
else:
log.info("Reloaded from %s. Updated services:\n%s", self.path, _DiffMaker(self.json, raw_json))
self.services, self.json = parsed_json, raw_json
self.last_updated = self.last_changed = now
def _get_data(self):
"""Raises OSError on failure to read json file; ValueError on failure to parse json file."""
with open(self.path, 'r') as f:
raw_json = f.read()
parsed_json = json.loads(raw_json)
if parsed_json is None:
parsed_json = {}
return raw_json, parsed_json
class _DiffMaker:
"""Formats diffs, but only on calls to __str__.
This way if logging is disabled the diff won't be calculated."""
def __init__(self, original_str, updated_str):
self.original_str = original_str
self.updated_str = updated_str
def __str__(self):
# TODO: consider using json-delta instead of difflib
import difflib
d = difflib.Differ()
compared = d.compare(
(self.original_str or '').splitlines(),
(self.updated_str or '').splitlines())
return "\n".join(x for x in compared if x[0] != '?')