示例#1
0
class NumbexDaemon(object):
    def __init__(self, cfg):
        self.cfg = cfg
        self.clients = []
        self.git = None
        self.db = None
        self.p2p_running = False
        self.log = logging.getLogger("daemon")
        self.updater_running = False
        self.updater_sched_stopped = True
        self.updater_worker_stopped = True
        self.updater_reqs = Queue(20)
        self.last_update = 0
        self.gitlock = threading.Lock()
        self.had_import_error = False
        self.had_export_error = False

    def reload_config(self, configname):
        newcfg = read_config(confname)
        # TODO do stuff that needs to be done after config changes
        self.cfg = newcfg

    def _p2p_get_peer_set(self):
        s = set()
        for c in self.clients:
            for p in c.peers:
                s.add(p)
        return s

    def p2p_get_peers(self):
        s = self._p2p_get_peer_set()
        return list(s)
 
    def p2p_get_updates(self, requested=None):
        try:
            self.updater_reqs.put_nowait(requested)
            return True
        except:
            return False

    def _p2p_get_updates(self, requested=None, git=None):
        if git is None:
            git = self.git
        # if no peers specificalyl requested, pick a random one
        if requested is None:
            self.log.info("get updates from random peer...")
            peers = self.p2p_get_peers()
            if peers:
                requested = [random.choice(peers)]
            else:
                return False, "no known peers"
        else:
            self.log.info("get updates")
            peers = self._p2p_get_peer_set()
            for p in requested:
                if p not in peers:
                    return False, "%s is an unknown peer"%p
        try:
            self.log.debug("acquiring lock")
            self.gitlock.acquire()
            self.log.debug("lock acquired")
            self.git.reload()
            for p in requested:
                self.log.info("fetching from %s...", p)
                start = time.time()
                remote = re.sub(r'[^a-zA-Z0-9_-]', '', 'remote_%s' % p)
                git.add_remote(remote, p, force=True)
                git.fetch_from_remote(remote)
                git.merge('%s/%s'%(remote, 'numbex'))
                git.reload()
                git.fix_overlaps()
                git.sync()
                end = time.time()
                self.log.info("fetch and merge complete in %.3fs", end-start)
            return True, ""
        finally:
            self.log.debug("lock released")
            self.gitlock.release()
            

    def _updater_thread(self):
        self.log.info("starting update processor")
        # need our own database connection here
        # and our own git, too, since it needs public keys
        db = Database(os.path.expanduser(self.cfg.get('DATABASE', 'path')),
                fill_example=False)
        git = NumbexRepo(os.path.expanduser(self.cfg.get('GIT', 'path')),
                db.get_public_keys)
        self.updater_worker_stopped = False
        while self.updater_running:
            requested = self.updater_reqs.get()
            if not self.updater_running:
                break
            if [None] == requested:
                continue
            try:
                r, msg = self._p2p_get_updates(requested, git=git)
                if not r:
                    self.log.warn("p2p_get_updates: %s", msg)
                if not self.updater_running:
                    break
                try:
                    self.log.debug("acquiring lock")
                    self.gitlock.acquire()       
                    self.log.debug("lock acquired")
                    if not self._import_from_p2p(db=db):
                        self.had_import_error = True
                        self.log.warn("stopping p2p and updater threads, please resolve the problems with e.g. forced p2p-export")
                        self.updater_stop()
                        self.p2p_stop()
                        break
                finally:
                    self.log.debug("lock released")
                    self.gitlock.release()
                self.last_update = time.time()
            except:
                self.log.exception("error in p2p_get_updates")
        self.updater_worker_stopped = True
        self.log.info("update processor stopped")

    def _updater_scheduler_thread(self):
        ival = self.cfg.getint('PEER', 'fetch_interval') 
        self.log.info("starting update scheduler, interval %s", ival)
        self.updater_sched_stopped = False
        while self.updater_running:
            ival = self.cfg.getint('PEER', 'fetch_interval') 
            time.sleep(ival)
            if not self.updater_running:
                break
            self.updater_reqs.put(None)
        self.updater_sched_stopped = True
        self.updater_reqs.put([None])
        self.log.info("update scheduler stopped")


    def updater_start(self):
        if self.updater_running:
            return False
        if not self.updater_sched_stopped or not self.updater_worker_stopped:
            self.log.info("updater threads not stopped yet")
            return False
        if self.cfg.getint('PEER', 'fetch_interval') <= 0:
            self.log.warn("updater disabled due to intveral=%s",
                    self.cfg.getint('PEER', 'fetch_interval'))
            return False
        self.updater_running = True
        t = threading.Thread(target=self._updater_scheduler_thread)
        t.daemon = True
        t.start()
        t = threading.Thread(target=self._updater_thread)
        t.daemon = True
        t.start()
        return True

    def updater_stop(self):
        self.log.info("stopping updater threads")
        self.updater_running = False
        return True

    def p2p_stop(self):
        if self.git is not None:
            self.git.stop_daemon()
        if self.p2p_running:
            self.log.info("stopping p2p")
        # even if flag not set, cleaning up can't hurt
        for p in self.clients:
            p.stop()
        self.clients = []
        self.p2p_running = False
        return True

    def p2p_start(self):
        if self.p2p_running:
            self.log.info("p2p already running")
            return False
        self.git.start_daemon(self.cfg.getint('GIT', 'daemon_port'))
        self.log.info("starting p2p peers")
        user = self.cfg.get('PEER', 'user')
        auth = self.cfg.get('PEER', 'auth')
        trackers = self.cfg.get('PEER', 'trackers').split()
        for track in trackers:
            peer = NumbexPeer(user=user, auth=auth, address=track,
                git=self.git, gitaddress=self.cfg.get('GIT', 'repo_url'))
            self.clients.append(peer)
            peer.register()
            t = threading.Thread(target=peer.mainloop)
            t.daemon = True
            t.start()
        self.p2p_running = True
        return True

    def _import_from_p2p(self, db, force_all=False):
        if force_all or db.ranges_empty():
            if force_all:
                self.log.info("forced import of everything")
            else:
                self.log.info("database empty, importing all...")
            start = time.time()   
            r = db.update_data(self.git.export_data_all())
            db.clear_changed_data()
            end = time.time()
            if r:
                self.log.info("database import completed in %.3f", end-start)
                return True, ""
            else:
                self.log.warn("database import failed, time %.3f", end-start)
                return False, "update_data returned False"

        if db.has_changed_data():
            return False, "database has changed data"

        hours = self.cfg.getint('GIT', 'export_timeout')
        since = datetime.datetime.now() - datetime.timedelta(hours)
        self.log.info("importing records modified since %s into the database",
                since)
        start = time.time()
        r = db.update_data(self.git.export_data_since(since))
        db.clear_changed_data()
        end = time.time()
        if r:
            self.log.info("database import completed in %.3f", end-start)
            return True, ""
        else:
            self.log.info("database import failed, time %.3f", end-start)
            return False, "update_data returned False"

    def import_from_p2p(self, force_all=False):
        return self._import_from_p2p(self.db, force_all)

    def export_to_p2p(self, force_all=False):
        if not force_all and not self.db.has_changed_data():
            self.log.info("nothing to export")
            return True
        try:
            self.log.debug("acquiring lock")
            self.gitlock.acquire()
            self.log.debug("lock acquired")
            self.git.reload()
            if self.git:
                hours = self.cfg.getint('DATABASE', 'export_timeout')
                since = datetime.datetime.now() - datetime.timedelta(hours)
                self.log.info("importing records since %s into git", since)
                start = time.time()
                delete = [x[0] for x in self.db.get_deleted_data()]
                r = self.git.import_data(self.db.get_data_since(since),
                        delete=delete)
                end = time.time()
            else:
                self.log.info("git repo empty, importing all records...")
                start = time.time()
                r = self.git.import_data(self.db.get_data_all())
                end = time.time()
        except:
            self.log.exception("export_to_p2p")
            raise
        finally:
            self.log.debug("lock released")
            self.gitlock.release()
        if r:
            self.log.info("import complete, time %.3f", end-start)
            self.db.clear_changed_data()
        else:
            self.log.warn("import failed, time %.3f", end-start)
        return r

    def status(self):
        return {
            'flags': {
                'p2p_running': self.p2p_running,
                'updater_running': self.updater_running,
                'had_import_error': self.had_import_error,
            },
            'p2p': {
                'peers': self.p2p_get_peers(),
                'trackers': [x.address for x in self.clients],
            },
            'updater': {
                'last_update': self.last_update,
            },
            'database': {
                'has_changed_data': self.db.has_changed_data(),
            }
        }

    def clear_errors(self):
        self.had_import_error = False

    def shutdown(self):
        self._exit()

    def _soap_start(self):
        port = self.cfg.getint('SOAP', 'port')
        self.log.info("starting SOAP controller on port %s...", port)
        dbpath = os.path.expanduser(self.cfg.get('DATABASE', 'path'))
        t = threading.Thread(target=AsServer,
                kwargs=dict(port=port, services=[MyNumbexService(dbpath),]))
        t.daemon = True
        t.start()

    def _startup(self):
        gitpath = os.path.expanduser(self.cfg.get('GIT', 'path'))
        self.db = Database(os.path.expanduser(self.cfg.get('DATABASE', 'path')),
                fill_example=False)
        self.git = NumbexRepo(os.path.expanduser(self.cfg.get('GIT', 'path')),
                self.db.get_public_keys)
        if self.db.ranges_empty():
            self.log.info("initial database empty")
            self.import_from_p2p()
        elif not self.git:
            self.log.info("initial git repository empty")
            r = self.export_to_p2p(force_all=True)
            if not r:
                self.log.error("import failed", msg)
        self.p2p_start()
        self.updater_start()
        self._soap_start()
        signal.signal(signal.SIGTERM, self._exit)

    def _main(self):
        try:
            self._startup()
        except:
            self._stop()
            raise
        try:
            host = self.cfg.get('GLOBAL', 'control_host')
            port = self.cfg.getint('GLOBAL', 'control_port')
            self.log.info("starting control daemon on %s port %s", host, port)
            xmlrpc = SimpleXMLRPCServer((host, port))
            xmlrpc.register_instance(self)
            xmlrpc.serve_forever()
        except (KeyboardInterrupt, SystemExit):
            pass
        except:
            self.log.exception("exception:")
            self._stop()
            sys.exit(2)
        finally:
            self._exit()

    def _exit(self, sig=None, frame=None):
        if sig is not None:
            self.log.info("received signal %s", sig)
        try:
            self._stop()
        except:
            self.log.exception('exception during exit ignored:')
        sys.exit(0)

    def _stop(self):
        self.updater_stop()
        self.p2p_stop()
        if self.git is not None:
            self.git.dispose()
            self.git = None
        self.db = None