How to implement watcher for descendant items?

43 Views Asked by At

Context

# Every mapping has a parent whose path is a prefix with one less element; the exception to this rule is root ("/") which has no parent.
# Unlike a regular file system, all these mappings can have both data (so it acts like a file) and children (so it acts like a directory)

# The data structure used is a dictionary called store. Values are arbitrary, by default there is an initial key of "/".  
# All keys start with the absolute path, for example "/dir1/file1".  
# Its not able to create a key if its parent key does not exist. 
# For example, it cannot create "/app1/p1" if dictionary only has the "/" key. It has to create an "/app1" key first, then call create on "/app1/p1"  

#                         (tree visualization example)
#                                    "/"  
#                      /                             \
#                  "/app1"                        "/app2" ...
#     /            /         \
# "/app1/p1"  "/app1/p2" "/app1/p3" ... 


class Zookeeper():
    store = {"/":None}
    def create(self, path, value):
        if self.validate(path):
            self.store[path] = value
        else:
            pass
            #raise KeyError("create error: {} invalid".format(path))
    
    def read(self, path):
        if path in self.store.keys():
            return self.store[path]
        else:
            return "read error: {} does not exist".format(path)
    
    def update(self, path, value):
        if path in self.store.keys():
            self.store[path] = value
        else:
            return "update error: {} does not exist".format(path)

    def validate(self, path):
        if path is None:
            return False
        if len(path.strip()) == 0:
            return False
        if path[0] != "/":
            return False
        if path.rindex("/") == path.index("/"):
            return True
        if path[0:path.rindex("/")] not in self.store:
            return False
        if path in self.store:
            return False
        return True


zk = Zookeeper()
zk.create("/app1", "/app1 value")
print(zk.read("/app1"))

zk.create("/app1/p1", "/app1/p1 value")
print(zk.read("/app1/p1"))

zk.create("/p1/p1", "/p1/p1 value")
print(zk.read("/p1/p1"))

zk.create("/p1", "/p1 value")
print(zk.read("/p1"))

zk.create("/p1/p1", "/p1/p1 value")
print(zk.read("/p1/p1"))

More context
The question is to implement a watch method

watch(path, watcher)  
  • Sets a watcher that will be called whenever the watched path or any of its descendants are updated by a call to update or create
  • watch accepts path and some sort of listener object (depending on the language this can be a function, pointer, object, or something else)
  • Whenever the value of path or that of any of its descendants are set, the callback registered for the path must be called
  • The watcher should accept two arguments - the path that was changed and the value that was set

How do you implement watch?

1

There are 1 best solutions below

0
jsbueno On

The fact your paths are all implemented as strings in a flat dictionary simplifies some things, and complicates others.

If upon installing a watcher you want to look just at that level of the path, and not on sub-levels, it is super-simple.

As all accesses are either through the "create" or "update" methods, all you have to do is to check in these methods if there is a "watcher" for the path that is being fiddled with. The watchers registry can be a simple dictionary, as is ".store" - and you can even have several listeners for each key by having the values of the store being sequences.

The "watch" functionality can even be implemented in a subclass of your original class with no hassles:


class WatchableZookeeper():
    def __init__(self):
        self.listeners = {}
        super().__init__()
        

    def watch(self, path, listener):
        # The "setdefault" method in dictionaries will create a new emty
        # list as the value, if it does not exist, and return it - 
        # otherwise it returns the existing list:
        self.listeners.setdefault(path, []).append(listener)
    
    def update(self, path, value):
        # let the superclass update run first: 
        # if it raises an error, we let it bubble up nd do not call the events!
        super().update(path, value)
        for listener in self.listeners.get(path):
            listerner(path, value)
            
    def create(self, path, value):
        super().update(path, value)
        for listener in self.listeners.get(path):
            listerner(path, value)