Пример #1
0
    def _create_connection(self):
        """Create our 'zk' connection object.

        If the object does not exist, create it. If it does exist, move on.
        """

        if self._zk:
            return

        # Define our zookeeper client here so that it never gets overwritten
        self._zk = ZookeeperClient(hosts=self._server,
                                   timeout=self._timeout,
                                   read_only=self._readonly,
                                   handler=EventHandler(),
                                   retry_delay=0.1,
                                   retry_backoff=2,
                                   retry_max_delay=10)

        # Set up our rate limiting
        self._zk.set_rate_limiter(delay=self._rate_limit_time,
                                  calls=self._rate_limit_calls)

        # Get a lock handler
        self._run_lock = self._zk.handler.lock_object()

        # Watch for any connection state changes
        self._zk.add_listener(self._state_listener)
Пример #2
0
class KazooServiceRegistry(nd_service_registry):

    _instance = None
    _initialized = False

    def __new__(self, **kwargs):
        """Only creates a new object if one does not already exist."""
        if self._instance is not None:
            return self._instance

        self._instance = object.__new__(self)
        return self._instance

    def __init__(self, server=SERVER, readonly=False, timeout=TIMEOUT,
                 cachefile=None, username=None, password=None,
                 acl=None, lazy=False):
        """Initialize the KazooServiceRegistry object.

        Generally speaking, you can initialize the object with no explicit
        settings and it will work fine as long as your Zookeeper server is
        available at 'localhost:2181'.

        A few notes though about some of the options...

        'lazy' mode allows the object to initialize and lazily connect to
               the Zookeeper services. If Zookeeper is down, it will continue
               to try to connect in the background, while allowing the object
               to respond to certain queries out of the 'cachefile'. This
               option is not very useful without the 'cachefile' setting.

       'cachefile' is a location to save a pickle-dump of our various
                   data objects. In the event that we start up in 'lazy' mode
                   and are unable to reach the backend Zookeeper service,
                   objects are re-created with this cache file instead and
                   we are able to respond to get() requests with the latest
                   saved data.

                   Of important note here, if you have multiple processes
                   using the same file location, they will not overwrite, but
                   rather they will append to the file as they save data. Each
                   object saves to this file immediately upon receiving a
                   data-change from the objects... so its generally very up-
                   to-date.

        'username' and 'password's can be supplied if you wish to authenticate
                   with Zookeeper by creating a 'Digest Auth'.

        'readonly' mode just safely sets our connection status to readonly to
                   prevent any accidental data changes. Good idea for your
                   data consumers.

        Args:
            server: String in the format of 'localhost:2181'
            readonly: Boolean that sets whether our connection to Zookeeper
                      is a read_only connection or not.
            timeout: Value in seconds for connection timeout
            lazy: Boolean whether or not to allow lazy connection mode
            cachefile: Location of desired cachefile
            username: Username to create Digest Auth with
            password: Password to create Digest Auth with
            acl: A Kazoo ACL-object if special ACLs are desired
        """

        # See if we're already initialized. If we are, just break out quickly.
        if self._initialized:
            return

        # Create our logger
        self.log = logging.getLogger('%s.KazooServiceRegistry' % __name__)
        self.log.info('Initializing ServiceRegistry object')

        # Quiet down the Kazoo connection ping messages
        logging.getLogger('kazoo.protocol.connection').addFilter(shims.KazooFilter())

        # Record the supplied settings
        self._timeout = timeout
        self._username = username
        self._password = password
        self._readonly = readonly
        self._acl = acl
        self._server = server
        self._lazy = lazy
        self._pid = os.getpid()
        self._zk = None

        # Store all of our Registration objects here
        self._registrations = {}

        # Store all of our Watcher objects here
        self._watchers = {}

        # Create a local 'dict' that we'll use to store the results of our
        # get_nodes/get_node_data calls.
        self._cachefile = cachefile

        # up things like authentication)
        self._connect(lazy)

        # On python interpreter shutdown, cleanly disconnect our connections
        atexit.register(self._shutdown)

        # Mark us as initialized
        self._initialized = True
        self.log.info('Initialization Done!')

    def _shutdown(self):
        """Cleanly shut down our connections."""

        self.log.debug('Shutting down...')

        # Quiet down the loggers to only show major errors during the shutdown
        # process.
        self.log.setLevel(logging.ERROR)
        logging.getLogger('kazoo').setLevel(logging.ERROR)
        logging.getLogger('kazoo.protocol.connection').setLevel(logging.ERROR)

        # Now disconnect our connection
        self.stop()

    def _health_check(func):
        """Decorator used to heathcheck the Zookeeper connection.

        If this healthcheck fails, we raise a ServiceUnavailable exception.
        If we detect that we've been forked, then we re-create our connection
        to the Zookeeper backend and move on with our health check."""

        @wraps(func)
        def _health_check_decorator(self, *args, **kwargs):
            self.log.debug('Running healthcheck...')
            pid = os.getpid()
            if pid != self._pid:
                self.log.warning('Fork detected!')
                self._pid = pid
                self.rebuild()

            # check if our connection is up or not
            if not self._zk.connected:
                e = 'Service is down. Try again later.'
                raise exceptions.NoConnection(e)

            return func(self, *args, **kwargs)
        return _health_check_decorator

    def stop(self):
        """Cleanly stops our Zookeeper connection.

        All Registration/Watch objects stick around. If the connection
        is re-established, they will automatically re-create their
        connections."""

        try:
            self._zk.stop()
        except:
            pass

    def start(self):
        """Starts up our ZK connection again.

        All existing watches/registrations/etc will be re-established."""
        self._connect(self._lazy)

    @_health_check
    def set(self, node, data, state, type):
        """Registers a supplied node (full path and nodename).

        Registers the supplied node-name with Zookeeper and converts the
        supplied data into JSON-text.

        Args:
            node: A string representing the node name and service port
                  (/services/foo/host:port)
            data: A dict with any additional data you wish to register.
            state: True/False whether or not the node is actively listed
                   in Zookeeper
            type: A Registration class object representing the type of node
                  we're registering.

        Returns:
            True: registration was successful"""

        # Check if we're in read-only mode
        if self._readonly:
            raise exceptions.ReadOnly('In read-only mode, no writes allowed')

        # Check if the node is already there or not. If it is, we have to
        # figure out if we were the ones who registered it or not. If we are,
        # we leave it alone. If not, we attempt to delete it and register our
        # own. If we cannot do that, we throw an error.
        self.log.debug('Looking for Registration object for [%s] with [%s].'
                       % (node, data))
        if node in self._registrations:
            # The Registration objects can stop themselves in the event of
            # a failure. If they do, lets throw a message, toss the object,
            # and then let a new one be created.
            self.log.debug('[%s] already has Registration object.' % node)
            self._registrations[node].update(data=data, state=state)
            return True

        # Create a new registration object
        self.log.debug('Creating Registration object for [%s]' % node)
        self._registrations[node] = type(zk=self._zk, path=node,
                                         data=data, state=state)
        return True

    def unset(self, node):
        """Destroys a particular Registration object.

        Args:
            node: (String) representing the node path to the object"""

        try:
            self._registrations[node].stop()
        except:
            self.log.warning('Node object for %s not found.' % node)
            return

        self.log.info('Registration for %s stopped.' % node)

    def _create_connection(self):
        """Create our 'zk' connection object.

        If the object does not exist, create it. If it does exist, just move on.
        """

        if self._zk:
            return

        # Define our zookeeper client here so that it never gets overwritten
        self._zk = ZookeeperClient(hosts=self._server,
                                   timeout=self._timeout,
                                   read_only=self._readonly,
                                   handler=EventHandler(),
                                   retry_delay=0.1,
                                   retry_backoff=2,
                                   retry_max_delay=10)

        # Get a lock handler
        self._run_lock = self._zk.handler.lock_object()

        # Watch for any connection state changes
        self._zk.add_listener(self._state_listener)

    def _connect(self, lazy):
        """Connect to Zookeeper.

        This function starts the connection to Zookeeper using the Kazoo
        start_async() function. By using start_async(), we can continue to
        re-try the connection if it fails either on the initial __init__ of the
        module, or if it fails after the object has been running for a while.

        Args:
            lazy: True/False - determines whether or not we continue to try
                  to connect in the background if the initial connection fails.
        """

        self.log.info('Connecting to Zookeeper Service (%s)' % self._server)

        # Create our zookeeper connection object
        self._create_connection()

        # Start our connection asynchronously
        self.event = self._zk.start_async()
        self.event.wait(timeout=self._timeout)

        # After the timeout above, check if we're connected.
        if not self._zk.connected:
            # If we allow lazy-connection mode, we'll continue to try to
            # connect in the background. In the foreground, we'll check if
            # theres a locally cached dict() file that we can grab some data
            # from so that we can function partially.
            if lazy:
                self.log.warning(
                    'Could not reach Zookeeper server. '
                    'Starting up in crippled mode. '
                    'Will continue to try to connect in the background.')
            else:
                # If lazy mode is False, then we stop trying to connect to
                # Zookeeper and raise an exception. The client can deal with
                # what-to-do at this point.
                self._zk.stop()
                raise exceptions.NoConnection(
                    'Could not connect to Zookeeper')

    @_health_check
    def _setup_auth(self):
        """Set up our credentials with the Zookeeper service.

        If credentials were passwed to us, authenticate with Zookeeper. These
        credentials do not have to exist in the system, they're compared
        against other credentials to validate whether two users are the same,
        or whether a particular set of credentials has access to a particular
        node.
        """

        with self._run_lock:
            if self._username and self._password:
                # If an ACL was provided, we'll use it. If not though, we'll
                # create our own ACL described below:
                #
                # This ACL essentially allows our USERNAME+PASSWORD combo to
                # completely own any nodes that were also created with the same
                # USERNAME+PASSWORD combo. This means that if all of your
                # production machines share a particular username/password,
                # they can each mess with the other machines node
                # registrations.
                #
                # Its highly recommended that you break up your server farms
                # into different permission groups.
                ACL = kazoo.security.make_digest_acl(self._username,
                                                     self._password, all=True)

                # This allows *all* users to read child nodes, but disallows
                # them from reading, updating permissions, deleting child
                # nodes, or writing to child nodes that they do not own.
                READONLY_ACL = kazoo.security.make_acl('world', 'anyone',
                                                       create=False,
                                                       delete=False,
                                                       write=False,
                                                       read=True,
                                                       admin=False)

                self.log.debug('Credentials were supplied, adding auth.')
                self._zk.retry(self._zk.add_auth, 'digest', "%s:%s" %
                              (self._username, self._password))

                if not self._acl:
                    self._acl = (ACL, READONLY_ACL)

            # If an ACL was providfed, or we dynamically generated one with the
            # username/password, then set it.
            if self._acl:
                self._zk.default_acl = (ACL, READONLY_ACL)

    def _state_listener(self, state):
        """Listens for state changes about our connection.

        If our client becomes disconnected, we set a local variable that lets
        the rest of the code know to not try to run any Zookeeper commands
        until the service is back up."""

        message='Zookeeper connection state changed'
        if state == KazooState.SUSPENDED:
            # In this state, just mark that we can't handle any 'writes' right
            # now but that we might come back to life soon.
            self.log.warning('%s: %s' % (message,state))
            return
        elif state == KazooState.LOST:
            # If we enter the LOST state, we've started a whole new session
            # with the Zookeeper server. Watches are re-established auto-
            # magically. Registered paths are re-established by their own
            # Registration control objects.
            self.log.warning('%s: %s' % (message,state))
            return
        else:
            self.log.info('%s: %s' % (message,state))
            # We've re-connected, so re-configure our auth digest settings
            self._setup_auth()

            # We are not allowed to call any blocking calls in this callback
            # because it is actually triggered by the thread that holds onto
            # the Zookeeper connection -- and that causes a deadlock.
            #
            # In order to re-register our Watchers, we use the Kazoo spawn()
            # function. This runs the function in a separate thread and allows
            # the state_listener function to return quickly.
            self._zk.handler.spawn(self._convert_dummywatchers_to_watchers)

    def add_callback(self, path, callback):
        """Adds a callback in the event of a path change.

        Adds a callback to a given watcher. If the watcher doesnt exist,
        we create it with that callback. Either way, your callback is
        immediately executed with the service data.

        Args:
            path: A string reprsenting the path to watch for changes.
            callback: Reference to the function to callback to.
        """

        if path in self._watchers:
            self.log.debug('Found [%s] in watchers. Adding callback.' % path)
            self._watchers[path].add_callback(callback)
        else:
            self.log.debug('No existing watcher for [%s] exists. Creating.' %
                           path)
            self.get(path, callback=callback)

    def get(self, path, callback=None):
        """Retrieves a list of nodes (or a single node) in dict() form.

        Creates a Watcher object for the supplied path and returns the data
        for the requested path. Triggers callback immediately.

        Args:
            path: A string representing the path to the servers.
            callback: (optional) reference to function to call if the path
                      changes.

        Returns:
            nd_service_registry.Watcher.get() dict object
        """

        # Return the object from our cache, if it's there
        self.log.debug('[%s] Checking for existing object...' % path)
        if path in self._watchers:
            self.log.debug('Found [%s] in cache: %s' %
                          (path, str(self._watchers[path].get())))
            # If a callback was suplied, but we already have a Watcher object,
            # add that callback to the existing object.
            if callback:
                self._watchers[path].add_callback(callback)

            # Return the Watcher object get() data.
            return self._watchers[path].get()

        try:
            # Go get a Watcher object since one doesnt already exist
            self._watchers[path] = self._get_watcher(path, callback)
            return self._watchers[path].get()
        except exceptions.NoConnection, e:
            # Get a DummyWatcher cached object instead
            self.log.warning('Health Check failed: %s' % e)

        try:
            self.log.info('[%s] Loading from cache instead' % path)
            self._watchers[path] = self._get_dummywatcher(path, callback)
            return self._watchers[path].get()
        except exceptions.ServiceRegistryException, e:
            # Ugh. Total failure. Return false
            self.log.error('Unable to retrieve [%s] from Zookeeper or cache - '
                           'try again later: %s' % (path, e))
Пример #3
0
class KazooServiceRegistry(nd_service_registry):

    _instance = None
    _initialized = False

    def __new__(self, **kwargs):
        """Only creates a new object if one does not already exist."""
        if self._instance is not None:
            return self._instance

        self._instance = object.__new__(self)
        return self._instance

    def __init__(self, server=SERVER, readonly=False, timeout=TIMEOUT,
                 cachefile=None, username=None, password=None,
                 acl=None, lazy=False, rate_limit_time=2,
                 rate_limit_calls=50):
        """Initialize the KazooServiceRegistry object.

        Generally speaking, you can initialize the object with no explicit
        settings and it will work fine as long as your Zookeeper server is
        available at 'localhost:2181'.

        A few notes though about some of the options...

        'lazy' mode allows the object to initialize and lazily connect to
               the Zookeeper services. If Zookeeper is down, it will continue
               to try to connect in the background, while allowing the object
               to respond to certain queries out of the 'cachefile'. This
               option is not very useful without the 'cachefile' setting.

       'cachefile' is a location to save a pickle-dump of our various
                   data objects. In the event that we start up in 'lazy' mode
                   and are unable to reach the backend Zookeeper service,
                   objects are re-created with this cache file instead and
                   we are able to respond to get() requests with the latest
                   saved data.

                   Of important note here, if you have multiple processes
                   using the same file location, they will not overwrite, but
                   rather they will append to the file as they save data. Each
                   object saves to this file immediately upon receiving a
                   data-change from the objects... so its generally very up-
                   to-date.

        'username' and 'password's can be supplied if you wish to authenticate
                   with Zookeeper by creating a 'Digest Auth'.

        'readonly' mode just safely sets our connection status to readonly to
                   prevent any accidental data changes. Good idea for your
                   data consumers.

        'rate_limit_calls' indicates the number of calls to the Zookeeper API
                           that we will allow to pass through unimpeded
                           before we consider whether or not to rate limit
                           them for safety. If None, then this is disabled.

        'rate_limit_time' is the average time in seconds you would like to try
                          to maintain between Zookeeper calls. This is only
                          used if 'rate_limit_calls' above is set.

        Args:
            server: String in the format of 'localhost:2181'
            readonly: Boolean that sets whether our connection to Zookeeper
                      is a read_only connection or not.
            timeout: Value in seconds for connection timeout
            lazy: Boolean whether or not to allow lazy connection mode
            cachefile: Location of desired cachefile
            username: Username to create Digest Auth with
            password: Password to create Digest Auth with
            acl: A Kazoo ACL-object if special ACLs are desired
            rate_limit_time: Target 'sleep' time between Zookeeper API calls.
                             (integer)
            rate_limit_calls: Minimum number of calls before we begin
                              throttling API calls. (integer)
        """

        # See if we're already initialized. If we are, just break out quickly.
        if self._initialized:
            return

        # Create our logger
        log.info('Initializing ServiceRegistry object')

        # Kazoo is very noisy by default. We quiet it down and only pay
        # attention to the most important messages.
        logging.getLogger('kazoo').setLevel(logging.WARNING)
        logging.getLogger('kazoo.protocol.connection').addFilter(
            shims.KazooFilter())

        # Record the supplied settings
        self._timeout = timeout
        self._username = username
        self._password = password
        self._readonly = readonly
        self._acl = acl
        self._server = server
        self._lazy = lazy
        self._rate_limit_time = rate_limit_time
        self._rate_limit_calls = rate_limit_calls
        self._pid = os.getpid()
        self._zk = None

        # Store our connection state, and a list of methods to notify
        # if that state changes at all.
        self._conn_state = False
        self._conn_state_callbacks = []

        # Store all of our Registration objects here
        self._registrations = {}

        # Store all of our Watcher objects here
        self._watchers = {}

        # Store our Lock objects here
        self._locks = {}

        # Create a local 'dict' that we'll use to store the results of our
        # get_nodes/get_node_data calls.
        self._cachefile = cachefile

        # up things like authentication)
        self._connect(lazy)

        # On python interpreter shutdown, cleanly disconnect our connections
        atexit.register(self._shutdown)

        # Mark us as initialized
        self._initialized = True
        log.info('Initialization Done!')

    def _shutdown(self):
        """Cleanly shut down our connections."""

        log.debug('Shutting down...')

        # Quiet down the loggers to only show major errors during the shutdown
        # process.
        log.setLevel(logging.ERROR)
        logging.getLogger('kazoo').setLevel(logging.ERROR)
        logging.getLogger('kazoo.protocol.connection').setLevel(logging.ERROR)

        # Now disconnect our connection
        self.stop()

    def _health_check(func):
        """Decorator used to heathcheck the Zookeeper connection.

        If this healthcheck fails, we raise a ServiceUnavailable exception.
        If we detect that we've been forked, then we re-create our connection
        to the Zookeeper backend and move on with our health check."""

        @wraps(func)
        def _health_check_decorator(self, *args, **kwargs):
            log.debug('Running healthcheck...')
            pid = os.getpid()
            if pid != self._pid:
                log.info('Fork detected!')
                self._pid = pid
                self.rebuild()

            # check if our connection is up or not
            if not self._zk.connected:
                e = 'Service is down. Try again later.'
                raise exceptions.NoConnection(e)

            return func(self, *args, **kwargs)
        return _health_check_decorator

    def stop(self):
        """Cleanly stops our Zookeeper connection.

        All Registration/Watch objects stick around. If the connection
        is re-established, they will automatically re-create their
        connections."""

        try:
            self._zk.stop()
            log.debug('Stopping Zookeeper connection...')
        except:
            pass

    def start(self):
        """Starts up our ZK connection again.

        All existing watches/registrations/etc will be re-established."""
        log.debug('Starting Zookeeper conncetion...')
        self._connect(self._lazy)

    @_health_check
    def set(self, node, data, state, type):
        """Registers a supplied node (full path and nodename).

        Registers the supplied node-name with Zookeeper and converts the
        supplied data into JSON-text.

        Args:
            node: A string representing the node name and service port
                  (/services/foo/host:port)
            data: A dict with any additional data you wish to register.
            state: True/False whether or not the node is actively listed
                   in Zookeeper
            type: A Registration class object representing the type of node
                  we're registering.

        Returns:
            True: registration was successful"""

        # Check if we're in read-only mode
        if self._readonly:
            raise exceptions.ReadOnly('In read-only mode, no writes allowed')

        # Check if the node is already there or not. If it is, we have to
        # figure out if we were the ones who registered it or not. If we are,
        # we leave it alone. If not, we attempt to delete it and register our
        # own. If we cannot do that, we throw an error.
        log.debug('Looking for Registration object for [%s] with [%s].'
                  % (node, data))
        if node in self._registrations:
            # The Registration objects can stop themselves in the event of
            # a failure. If they do, lets throw a message, toss the object,
            # and then let a new one be created.
            log.debug('[%s] already has Registration object.' % node)
            self._registrations[node].update(data=data, state=state)
            return True

        # Create a new registration object
        log.debug('Creating Registration object for [%s]' % node)
        self._registrations[node] = type(zk=self._zk, path=node,
                                         data=data, state=state)
        return True

    def _create_connection(self):
        """Create our 'zk' connection object.

        If the object does not exist, create it. If it does exist, move on.
        """

        if self._zk:
            return

        # Define our zookeeper client here so that it never gets overwritten
        self._zk = ZookeeperClient(hosts=self._server,
                                   timeout=self._timeout,
                                   read_only=self._readonly,
                                   handler=EventHandler(),
                                   retry_delay=0.1,
                                   retry_backoff=2,
                                   retry_max_delay=10)

        # Set up our rate limiting
        self._zk.set_rate_limiter(delay=self._rate_limit_time,
                                  calls=self._rate_limit_calls)

        # Get a lock handler
        self._run_lock = self._zk.handler.lock_object()

        # Watch for any connection state changes
        self._zk.add_listener(self._state_listener)

    def _connect(self, lazy):
        """Connect to Zookeeper.

        This function starts the connection to Zookeeper using the Kazoo
        start_async() function. By using start_async(), we can continue to
        re-try the connection if it fails either on the initial __init__ of the
        module, or if it fails after the object has been running for a while.

        Args:
            lazy: True/False - determines whether or not we continue to try
                  to connect in the background if the initial connection fails.
        """

        log.info('Connecting to Zookeeper Service (%s)' % self._server)

        # Create our zookeeper connection object
        self._create_connection()

        # Start our connection asynchronously
        self.event = self._zk.start_async()
        self.event.wait(timeout=self._timeout)

        # After the timeout above, check if we're connected.
        if not self._zk.connected:
            # If we allow lazy-connection mode, we'll continue to try to
            # connect in the background. In the foreground, we'll check if
            # theres a locally cached dict() file that we can grab some data
            # from so that we can function partially.
            if lazy:
                log.warning(
                    'Could not reach Zookeeper server. '
                    'Starting up in crippled mode. '
                    'Will continue to try to connect in the background.')
            else:
                # If lazy mode is False, then we stop trying to connect to
                # Zookeeper and raise an exception. The client can deal with
                # what-to-do at this point.
                self._zk.stop()
                raise exceptions.NoConnection(
                    'Could not connect to Zookeeper')

    @_health_check
    def _setup_auth(self):
        """Set up our credentials with the Zookeeper service.

        If credentials were passwed to us, authenticate with Zookeeper. These
        credentials do not have to exist in the system, they're compared
        against other credentials to validate whether two users are the same,
        or whether a particular set of credentials has access to a particular
        node.
        """

        with self._run_lock:
            if self._username and self._password:
                # If an ACL was provided, we'll use it. If not though, we'll
                # create our own ACL described below:
                #
                # This ACL essentially allows our USERNAME+PASSWORD combo to
                # completely own any nodes that were also created with the same
                # USERNAME+PASSWORD combo. This means that if all of your
                # production machines share a particular username/password,
                # they can each mess with the other machines node
                # registrations.
                #
                # Its highly recommended that you break up your server farms
                # into different permission groups.
                ACL = kazoo.security.make_digest_acl(u'%s' % self._username,
                                                     u'%s' % self._password,
                                                     all=True)

                # This allows *all* users to read child nodes, but disallows
                # them from reading, updating permissions, deleting child
                # nodes, or writing to child nodes that they do not own.
                READONLY_ACL = kazoo.security.make_acl(u'world', u'anyone',
                                                       create=False,
                                                       delete=False,
                                                       write=False,
                                                       read=True,
                                                       admin=False)

                log.debug('Credentials were supplied, adding auth.')
                self._zk.retry(self._zk.add_auth_async, 'digest', "%s:%s" %
                               (self._username, self._password))

                if not self._acl:
                    self._acl = (ACL, READONLY_ACL)

            # If an ACL was providfed, or we dynamically generated one with the
            # username/password, then set it.
            if self._acl:
                self._zk.default_acl = self._acl

    def _state_listener(self, state):
        """Listens for state changes about our connection.

        If our client becomes disconnected, we set a local variable that lets
        the rest of the code know to not try to run any Zookeeper commands
        until the service is back up."""

        message = 'Zookeeper connection state changed'
        if state == KazooState.SUSPENDED:
            # In this state, just mark that we can't handle any 'writes' right
            # now but that we might come back to life soon.
            log.warning('%s: %s' % (message, state))
            self._conn_state = False
        elif state == KazooState.LOST:
            # If we enter the LOST state, we've started a whole new session
            # with the Zookeeper server. Watches are re-established auto-
            # magically. Registered paths are re-established by their own
            # Registration control objects.
            log.warning('%s: %s' % (message, state))
            self._conn_state = False
        else:
            log.info('%s: %s' % (message, state))
            self._conn_state = True
            # We've re-connected, so re-configure our auth digest settings
            self._setup_auth()

            # We are not allowed to call any blocking calls in this callback
            # because it is actually triggered by the thread that holds onto
            # the Zookeeper connection -- and that causes a deadlock.
            #
            # In order to re-register our Watchers, we use the Kazoo spawn()
            # function. This runs the function in a separate thread and allows
            # the state_listener function to return quickly.
            self._zk.handler.spawn(self._convert_dummywatchers_to_watchers)

        # Execute any of our state-listener callbacks now that we've finished
        # updating the internal state of our connection.
        def execute_state_callbacks(state):
            """Sequentially executes the callback functions.

            args:
                state: Boolean of our connection state
            """
            log.debug('Configured connection state callbacks: %s' %
                      self._conn_state_callbacks)

            for callback in self._conn_state_callbacks:
                log.debug('Calling [%s] with new connection state: %s' %
                          (callback, state))
                callback(state)

        self._zk.handler.spawn(execute_state_callbacks(self._conn_state))

    def _get_state(self, callback=None):
        """Returns the current connection state to Zookeeper.

        Optionally, if a callback function is supplied we will add it to the
        callback list for the _state_listener() function. This will trigger the
        callback immediately as well.

        args:
            callback: A reference to a function to call when the state changes.

        returns:
            Boolean of the connection state.
        """
        if callback and callback in self._conn_state_callbacks:
            log.debug('[%s] already in state listener callbacks.' % callback)
            return self._conn_state

        if callback:
            self._conn_state_callbacks.append(callback)
            callback(self._conn_state)

        return self._conn_state

    def add_callback(self, path, callback):
        """Adds a callback in the event of a path change.

        Adds a callback to a given watcher. If the watcher doesnt exist,
        we create it with that callback. Either way, your callback is
        immediately executed with the service data.

        Args:
            path: A string reprsenting the path to watch for changes.
            callback: Reference to the function to callback to.
        """

        if path in self._watchers:
            log.debug('Found [%s] in watchers. Adding callback.' % path)
            self._watchers[path].add_callback(callback)
        else:
            log.debug('No existing watcher for [%s] exists. Creating.' % path)
            self.get(path, callback=callback)

    def get(self, path, callback=None):
        """Retrieves a list of nodes (or a single node) in dict() form.

        Creates a Watcher object for the supplied path and returns the data
        for the requested path. Triggers callback immediately.

        Args:
            path: A string representing the path to the servers.
            callback: (optional) reference to function to call if the path
                      changes.

        Returns:
            nd_service_registry.Watcher.get() dict object
        """

        # Return the object from our cache, if it's there
        log.debug('[%s] Checking for existing object...' % path)
        if path in self._watchers:
            log.debug('Found [%s] in cache: %s' %
                      (path, self._watchers[path].get()))
            # If a callback was suplied, but we already have a Watcher object,
            # add that callback to the existing object.
            if callback:
                self._watchers[path].add_callback(callback)

            # Return the Watcher object get() data.
            return self._watchers[path].get()

        try:
            # Go get a Watcher object since one doesnt already exist
            self._watchers[path] = self._get_watcher(path, callback)
            return self._watchers[path].get()
        except exceptions.NoConnection as e:
            # Get a DummyWatcher cached object instead
            log.warning('Health Check failed: %s' % e)

        try:
            log.info('[%s] Loading from cache instead' % path)
            watcher = self._get_dummywatcher_from_cache(path, callback)
            self._watchers[path] = watcher
            return self._watchers[path].get()
        except exceptions.ServiceRegistryException as e:
            # Failure getting the DummyWatcher from our cache, so now
            # create a blank DummyWatcher just to keep track of this
            # request. When the connection comes back, this DummyWatcher
            # will be converted into a real Watcher object and will notify
            # the callback appropriately.
            log.error('Unable to retrieve [%s] from Zookeeper or cache: '
                      '%s' % (path, e))
            watcher = self._get_dummywatcher(path, callback)
            self._watchers[path] = watcher
        return False

    @_health_check
    def _get_watcher(self, path, callback=None):
        """Creates a Watcher object for the supplid path.

        Creates a Watcher object for the supplied path and returns the
        object. Triggers callbacks immediately.

        This is broken into its own function so that it can leverage
        the @_health_check decorator. This function should never be
        called from outside of this class.

        Args:
            path: A string representing the path to the servers.
            callback: (optional) reference to function to call if the path
                      changes.

        Returns:
            nd_service_registry.Watcher object
        """

        # Ok, so the cache is missing the key. Lets look for it in Zookeeper
        log.debug('[%s] Creating Watcher object...' % path)
        watcher = Watcher(self._zk,
                          path,
                          watch_children=True,
                          callback=callback)

        # Always register our dictionary saver callback, in addition to
        # whatever our user has supplied
        watcher.add_callback(self._save_watcher_to_dict)
        return watcher

    @_health_check
    def _get_lock(self, path, name, simultaneous, wait):
        """Retrieves a lock semaphore-style object from the supplied path.

        This method creates our Lock object and returns it. It is not meant
        to be used directly though.

        Args:
            path: A string representing the path for the lock.
            name: Optional string representing the server identifier.
            simultaneous: Int representing how many concurrent locks can
                          occur on this path.
        Returns:
            nd_service_registry.Lock object
        """

        # Return the object from our cache, if it's there
        log.debug('[%s] Checking for existing object...' % path)
        if path in self._locks:
            log.debug('Found [%s] in cache: %s' %
                      (path, str(self._locks[path])))
            return self._locks[path]

        # Go create a Lock object and store it
        log.debug('[%s] Creating Lock object...' % path)
        self._locks[path] = Lock(self._zk,
                                 path,
                                 name,
                                 simultaneous,
                                 wait)

        return self._locks[path]