def request(self, args):
     client, sizehint = args[:2]
     self.update_files()  # TODO: fix the race condition behind this
     filelist = [ filename for filename in self.scanner.keys() \
                     if client not in self.files[filename] \
                         and not self.held(filename, client) ]
     filelist = sorted(filelist, key=lambda f: self.scanner[f]["size"])
     filelist = sorted(filelist, key=lambda f: len(self.files[f]))
     self.logger.debug(f"pulled a list for {client}:")
     self.dump_files(filelist)
     if not filelist:  # no files for this client
         return Communique("__none__", negatives=("__none__", ))
     target = len(self.files[filelist[0]]) + 1
     if target < self.copies:  # implies underserved; expand to
         target = self.copies  # consider any underserved file
     candidates = [ filename for filename in filelist \
                     if len(self.files[filename]) < target ]
     self.logger.debug(f"targeted list for {client}:")
     self.dump_files(candidates)
     # TODO: return multiple files
     filename = self.least_served(candidates, int(sizehint))
     self.logger.debug(f"least_served gives {filename}")
     if filename:
         self.hold(filename, client)
         return Communique(filename, str(self.scanner[filename]["size"]))
     else:
         return Communique("__none__", negatives=("__none__", ))
    def claim(self, args):
        client, filename, checksum = args[:3]
        if filename not in self.scanner:
            self.logger.warn("Client has a file, I don't; deleted?")
            return Communique("drop", truthiness=False)
            return Communique("NACK", truthiness=False)

        filestate = self.scanner[filename]
        self.logger.debug(f"{client} claims file {filename}")
        if filestate["checksum"] == "deferred":
            self.logger.debug(
                "I have a deferred checksum; let it go (for now)")
            # return Communique("keep", truthiness=True)
            # fallthrough
        elif checksum != filestate["checksum"]:
            self.logger.warn(f"{client} has a different checksum ...")
            self.scanner.update(filename)
            filestate = self.scanner[filename]
            if checksum != filestate["checksum"]:
                self.logger.warn(f"{client} has the wrong checksum!\n" * 10)
                return Communique("update", truthiness=False)
            else:
                self.logger.warn(f"{client} was right; I'm straight now")
                return Communique("keep", truthiness=True)

        if filename not in self.files:
            self.logger.warn(f"I'm learning about {filename}")
            self.files[filename] = [client]
        elif client not in self.files[filename]:
            self.files[filename].append(client)
        self.stats['claims'] += 1
        self.release(filename)
        # return Communique("keep", truthiness=True)
        return Communique("ack")
 def status(self, args):
     client = args[0]
     response = []
     if self.underserved(args):
         response.append("underserved")
     if self.request((client, 0)):
         response.append("available")
     if self.overserved(args):
         response.append("overserved")
     if response:
         return Communique(response)
     else:
         return Communique("just right")
 def unclaim(self, args):
     client, filename = args[:2]
     # self.logger.debug(f"files: {self.files}, client: {client}, filename: {filename}")
     if filename and len(self.files) > 0 and filename in self.files \
         and client in self.files[filename]:
         self.files[filename].remove(client)
         self.stats['drops'] += 1
     return Communique("ack")
 def overserved(self, args):
     client = args[0]
     files = {}
     for filename in self.files:
         # self.logger.debug(f"{filename}: {len(self.files[filename])}/{self.copies} {self.files[filename]}")
         if client in self.files[filename]:
             # self.logger.debug(f"client match for {filename}")
             if len(self.files[filename]) > self.copies:
                 # self.logger.debug(f"overserved file: {filename}")
                 files[filename] = len(self.files[filename])
     if len(files.keys()) > 0:
         filenames = sorted(files.keys(),
                            key=lambda x: files[x],
                            reverse=True)
         return Communique(self.random_subset(filenames, 20))
     else:
         self.logger.debug("no overserved files :/")
         return Communique(None)
    def handle(self, data):
        request = Communique.build(data)
        action, server_context = request[:2]
        args = request[2:]
        if server_context not in self.servlets:
            return "__none__"
        # self.logger.debug(f"acting: {server_context} => {action}({args})")
        response = self.servlets[server_context].handle(action, args)

        if not response:
            return "__none__"
        return str(response)
 def inventory(self, args):
     client = args[0]
     self.logger.debug(f"{client} wants inventory")
     files = []
     for filename in self.files:
         if client in self.files[filename]:
             files.append(filename)
         # else:
         #     self.logger.debug(f"{client} not in {filename}")
     # TODO: make this better
     c = Communique(files)
     self.logger.debug(f"inventory: {c}")
     return c
 def underserved(self, args):
     client = args[0]
     self.logger.debug(f"Underserved for {client}?")
     # self.audit()
     files = []
     for filename in self.files:
         if client not in self.files[filename] and \
             len(self.files[filename]) < self.copies:
             # weighted random... TODO make this ~O(1)
             files.append(filename)
             # return filename
     if len(files) > 0:
         # TODO: return a subset of the list
         return self.random_subset(files, 20)
         # file = random.choice(files)
         # self.logger.debug(f"returning {file} and moar")
         return files
     self.logger.debug("I got nothin'")
     return Communique(None)
 def heartbeep(self, args):
     client = args[0]
     self.logger.info(f"heartbeep from {client}: {args}")
     return Communique("ack")
 def drain(self, args):
     client, filename = args[:2]
     self.logger.debug(f"client: {client} requests drain {filename}")
     self.drains[f"{client}:{filename}"] = 1
     return Communique("ack")