def remove_candidate(self, candidate): ''' Removes candidate from zookeeper and updates leader queues to reflect deleted candidate. :param path: string. path to deleted node :rtype: True if successful, False otherwise ''' #create node in ZK. Node will be delete when backing framework is closed with self.lock: del_id = self.counter_by_candidate.get(candidate, None) get_logger().info("LeaderQueue.remove_candidate %s" % (del_id)) if del_id == None: return False try: # delete node self.connection.delete(id_to_item_path(self.path, del_id)) # update the leader queues self._handle_remove(del_id) #cleanup hashes with self.lock: del self.counter_by_candidate[candidate] return True except zookeeper.NoNodeException: return False
def add_listeners(self, add_callback=None, remove_callback=None): """ Allows caller to add callbacks for item addition and item removal. :param add_callback: (Optional) callback that will be called when an \ id is created. :param remove_callback: (Optional) callback that will be called when \ an id is created. Callbacks are expected to have the following interface: Parameters: * dbag - This object * affected_id - The id that is being added or deleted, as appropriate \ to the call """ with self.lock: if add_callback: get_logger().debug( "DistributedBag.add_listeners added add listener") self.add_callbacks.append(add_callback) if remove_callback: get_logger().debug( "DistributedBag.add_listeners added remove listener") self.delete_callbacks.append(remove_callback) return self.get_items()
def add_candidate(self, candidate, meta_data=None): ''' Adds new candidate to zookeeper and updates. Leader queues to reflect new candidate. :param candidate: object that represents the candidate in the queue :param meta_data: binary data to store on node :rtype: True if operation successfully completed. False otherwise ''' #ensure candidate is not already in queue if self.has_candidate(candidate): get_logger().warning( "LeaderQueue.remove_candidate Candidate already in queue.") return False else: #create node in ZK. Node will be deleted when connection is closed flags = zookeeper.EPHEMERAL | zookeeper.SEQUENCE newpath = self.connection.create(self.path + PREFIX + PREFIX, meta_data, zc.zk.OPEN_ACL_UNSAFE, flags) counter = pettingzoo.utils.counter_value(newpath) get_logger().info("LeaderQueue.add_candidate %s" % (newpath)) try: #create delete watch self._create_deletion_handlers(counter) except: exc_class, exc, tback = sys.exc_info() sys.stderr.write(str(exc_class) + "\n") traceback.print_tb(tback) raise exc_class, exc, tback with self.lock: self.counter_by_candidate[candidate] = counter self._handle_add(counter, candidate) return True
def _process_deleted(self, node): """ Callback used for Exists object. Not intended for external use. """ del_id = pettingzoo.utils.counter_value(node.path) get_logger().debug("LeaderQueue._process_deleted %s" % (del_id)) self._handle_remove(del_id) with self.lock: del self.deletion_handlers[del_id]
def _process_deleted(self, node): """ Callback used for Deleted object. **Not intended for external use.** """ del_id = pettingzoo.utils.counter_value(node.path) get_logger().debug( "DistributedBag._process_deleted %s" % (del_id)) self._on_delete_id(del_id) with self.lock: del self.deletion_handlers[del_id]
def get(self, item_id): """ Returns the data payload of a specific item_id's znode :param item_id: to retrieve :rtype: data stored at id (or absent if invalid id) """ try: get_logger().debug("DistributedBag.get %s" % item_id) return self.connection.get(id_to_item_path(self.path, item_id))[0] except zookeeper.NoNodeException: return None
def _child_callback(self, children): path = children.path get_logger().info("DistributedMultiConfig._child_callback: %s" % (path)) config = self._load_znodes(path, add_callback=False) callbacks = self.callbacks.get(path, []) for callback in callbacks: config_list = [] if config: config_list = [conf[1] for conf in config] else: get_logger().warning( "DistributedConfig._child_callback: NO CONFIGS AVAILABLE") callback(path, config_list)
def remove(self, item_id): """ Remove an item from the bag. :param item_id: of node to deleteid_to_item_path(path, item_id) \ (returned by addElement) :rtype: *boolean* true if node was deleted, false if node did not exist """ try: get_logger().debug("DistributedBag.remove %s" % item_id) self.connection.delete(id_to_item_path(self.path, item_id)) return True except zookeeper.NoNodeException: return False
def _load_znodes(self, path, add_callback=True): get_logger().info("DistributedConfig._load_znodes: %s. Callback: %s" % (path, add_callback)) if self.connection.exists(path): children = self.connection.children(path) if add_callback: children(self._child_callback) self.children[path] = children if len(children) > 0: selectee = random.choice([c for c in children]) znode = path + "/" + selectee config = (selectee, yaml.load(self.connection.get(znode)[0])) self._store_config_in_cache(path, config) return config
def _child_callback(self, children): path = children.path service_class, service_name = _znode_to_class_and_name(path) config = self._load_znodes(path, add_callback=False) callbacks = self.callbacks.get(path, []) get_logger().info("DistributedConfig._child_callback: %s" % (path)) for callback in callbacks: conf = None if config: conf = config[1] else: get_logger().warning( "DistributedConfig._child_callback: NO CONFIGS AVAILABLE") callback(path, conf)
def _handle_add(self, counter, candidate): ''' Called on node creation. :param counter: (int) counter of newly created candidate :param candidate: newly created candidate to add to predecessor \ dictionary :rtype: None ''' if counter == None: get_logger().warning( "LeaderQueue._handle_add Unknown candidate %s" % (counter)) else: self._update_predecessor_dict(counter, candidate) return None
def _on_delete_id(self, removed_id): """ Called internally when an item is deleted. **Not intended for external use.** """ path = id_to_item_path(self.path, removed_id) get_logger().info("DistributedBag._on_delete_id %s" % (path)) with self.lock: try: self.ids.remove(removed_id) for callback in self.delete_callbacks: callback(self, removed_id) except: exc_class, exc, tback = sys.exc_info() sys.stderr.write(str(exc_class) + "\n") traceback.print_tb(tback) raise exc_class, exc, tback
def write_distributed_config(connection, service_class, service_name, config, key=None, interface='eth0', ephemeral=True): """ Writes a discovery config file out to zookeeper. :param connection: a zc.zk.ZooKeeper connection :param service_class: the classification of the service \ (e.g. mysql, memcached, etc) :param service_name: the cluster of the service \ (production, staging, etc) :param config: A dict containing discovery config values following the \ pettingzoo discovery file rules. :param key: (Optional) A unique string used to differentiate providers of \ a config inside pettingzoo. If a key is not provided, pettingzoo will \ automatically use the ip address of the computer this function is being \ called from. The Key is needed if you wish to explicitly remove a config. :param interface: (Default eth0) If falling back to pettingzoo to generate \ the key, you can set which networkign device to use to generate the \ ip address :param ephermeral: (Default True) Determines if this discovery config will \ be written to zookeeper as an ephemeral node or not. Practically what \ this means is that if your zookeeper connection closes, zookeeper will \ automatically remove the config. You generally want this to be True. :rtype: *str* the key """ if not key: key = _get_local_ip(interface) path = _znode_path(service_class, service_name) connection.create_recursive(path, "", acl=zc.zk.OPEN_ACL_UNSAFE) config = _set_metadata( validate_config(config, service_class), service_name, key) payload = yaml.dump(config) flags = 0 if ephemeral: flags = zookeeper.EPHEMERAL znode = _znode_path(service_class, service_name, key) if connection.exists(znode): connection.delete(znode) connection.create(znode, payload, zc.zk.OPEN_ACL_UNSAFE, flags) get_logger().info("write_distributed_config: %s/%s/%s, Ephemeral: %s" % (service_class, service_name, key, ephemeral)) get_logger().debug("%s" % (config)) return key
def _process_children_changed(self, children): """ Callback used for Children object. **Not intended for external use.** """ try: new_max = pettingzoo.utils.max_counter(children) get_logger().debug( "DistributedBag._process_children_changed %s" % (new_max)) with self.lock: while self.max_token < new_max: self.max_token += 1 self._on_new_id(self.max_token) except: exc_class, exc, tback = sys.exc_info() sys.stderr.write(str(exc) + "\n") traceback.print_tb(tback) raise exc_class, exc, tback
def remove_stale_config(connection, service_class, service_name, key): """ This function manually removes a discovery config from zookeeeper. This can be used either to remove non ephemeral discovery configs, when they are no longer valid, or to remove ephemeral discovery configs when you don't wish to close the zookeeper connection. For example when your configs are being written by a seperate monitoring process. :param connection: a zc.zk.ZooKeeper connection :param service_class: the classification of the service \ (e.g. mysql, memcached, etc) :param service_name: the cluster of the service \ (production, staging, etc) :param key: the key the specific discovery config was written out as. """ get_logger().info("remove_stale_config: %s/%s/%s" % (service_class, service_name, key)) connection.delete(_znode_path(service_class, service_name, key))
def _load_znodes(self, path, add_callback=True): get_logger().info( "DistributedMultiConfig._load_znodes: %s. Callback: %s" % (path, add_callback)) if self.connection.exists(path): children = self.connection.children(path) if add_callback: children(self._child_callback) self.children[path] = children if len(children) > 0: config = [] for child in children: znodep = path + "/" + child znode = self.connection.get(znodep) single = yaml.load(znode[0]) config.append((child, single)) self._store_config_in_cache(path, config) return config
def _cleanup_tokens(self, children, max_token): """ Tokens are used to track the current max token only. This substantially lowers the impact on zookeeper when items are added as opposed to tracking children in the /items path, especially when /items becomes large. This method removes any errant tokens if it sees any tokens still in the system smaller then max_token. """ for child in children: token_id = pettingzoo.utils.counter_value(child) if token_id < max_token: get_logger().warning( "DistributedBag._cleanup_tokens %s" % (token_id)) try: self.connection.adelete( id_to_token_path(self.path, token_id)) except zookeeper.NoNodeException: pass # If it doesn't exist, that's ok
def _on_new_id(self, new_id): """ Called internally when an item is added. **Not intended for external use.** """ try: path = id_to_item_path(self.path, new_id) get_logger().info("DistributedBag._on_new_id %s" % (path)) with self.lock: self.ids.add(new_id) deleted = Deleted( self.connection, path, [self._process_deleted]) self.deletion_handlers[new_id] = deleted for callback in self.add_callbacks: callback(self, new_id) except: exc_class, exc, tback = sys.exc_info() sys.stderr.write(str(exc_class) + "\n") traceback.print_tb(tback) raise exc_class, exc, tback
def _populate_ids(self): """Fills out the bag when initial connection to it is made""" ichildren = self.connection.children(self.path + ITEM_PATH) with self.lock: for child in ichildren: try: new_id = pettingzoo.utils.counter_value(child) path = id_to_item_path(self.path, new_id) get_logger().info("DistributedBag._on_new_id %s" % (path)) self.ids.add(new_id) deleted = Deleted( self.connection, path, [self._process_deleted]) self.deletion_handlers[new_id] = deleted for callback in self.add_callbacks: callback(self, new_id) except: exc_class, exc, tback = sys.exc_info() sys.stderr.write(str(exc_class) + "\n") traceback.print_tb(tback) raise exc_class, exc, tback
def _handle_remove(self, del_id): ''' Called on delete. Updates the candidate by predecessor dict. :param del_id: (int) id of deleted candidate :rtype: None ''' with self.lock: candidate = self.candidate_by_predecessor.get(del_id, None) if del_id == None: get_logger().warning( "LeaderQueue._handle_remove Unknown candidate %s" % (del_id)) elif candidate == None: get_logger().debug( "LeaderQueue._handle_remove Removed candidate is not" + " a predecessor %s" % (del_id)) else: self._update_predecessor_dict(del_id, candidate) del self.candidate_by_predecessor[del_id] return None
def add(self, data, ephemeral=True): """ Inserts an element into the bag. :param data: value stored at element :param ephemeral: if true, element is automatically deleted when \ backing framework is closed :rtype: *int* element id (required for deletion) """ flags = zookeeper.SEQUENCE if ephemeral: flags = zookeeper.EPHEMERAL | zookeeper.SEQUENCE newpath = self.connection.create( self.path + ITEM_PATH + ITEM_PATH, data, zc.zk.OPEN_ACL_UNSAFE, flags) item_id = pettingzoo.utils.counter_value(newpath) get_logger().debug("DistributedBag.add %s: %s" % (item_id, data)) self.connection.acreate( id_to_token_path(self.path, item_id), "", zc.zk.OPEN_ACL_UNSAFE) if item_id > 0: children = self.connection.children(self.path + TOKEN_PATH) self._cleanup_tokens(children, item_id) return item_id
def add_candidate(self, candidate, meta_data=None): ''' Adds new candidate to zookeeper and updates. Leader queues to reflect new candidate. :param candidate: object that represents the candidate in the queue :param meta_data: binary data to store on node :rtype: True if operation successfully completed. False otherwise ''' #ensure candidate is not already in queue if self.has_candidate(candidate): get_logger().warning( "LeaderQueue.remove_candidate Candidate already in queue.") return False else: #create node in ZK. Node will be deleted when connection is closed flags = zookeeper.EPHEMERAL | zookeeper.SEQUENCE newpath = self.connection.create( self.path + PREFIX + PREFIX, meta_data, zc.zk.OPEN_ACL_UNSAFE, flags) counter = pettingzoo.utils.counter_value(newpath) get_logger().info("LeaderQueue.add_candidate %s" % (newpath)) try: #create delete watch self._create_deletion_handlers(counter) except: exc_class, exc, tback = sys.exc_info() sys.stderr.write(str(exc_class) + "\n") traceback.print_tb(tback) raise exc_class, exc, tback with self.lock: self.counter_by_candidate[candidate] = counter self._handle_add(counter, candidate) return True
def load_config(self, service_class, service_name, callback=None): """ Returns a config using the fallback scheme for DistributedDiscovery to select a config at random from the available configs for a particular service. :param service_class: the classification of the service \ (e.g. mysql, memcached, etc) :param service_name: the cluster of the service \ (production, staging, etc) :param callback: callback function to call if the config for this \ service changes. (Optional) :rtype: *list* The list contains config dicts in the pettingzoo \ config format. """ path = _znode_path(service_class, service_name) cached = self._get_config_from_cache(path, callback) if cached: get_logger().info( "DistributedMultiConfig.load_config: %s/%s (cached)" % (service_class, service_name)) rconfig = [ _set_metadata( validate_config(conf[1], service_class), service_name, conf[0]) for conf in cached] get_logger().debug("%s" % (rconfig)) return rconfig config = self._load_znodes(path) if config: get_logger().info( "DistributedMultiConfig.load_config: %s/%s (zookeeper)" % (service_class, service_name)) rconfig = [ _set_metadata( validate_config(conf[1], service_class), service_name, conf[0]) for conf in config] get_logger().debug("%s" % (rconfig)) return rconfig config = self._load_file_config(service_class, service_name) get_logger().info("DistributedMultiConfig.load_config: %s/%s (file)" % (service_class, service_name)) rconfig = [ _set_metadata( validate_config(c, service_class), service_name) for c in config] get_logger().debug("%s" % (rconfig)) return rconfig