예제 #1
0
    def test_taskqueue(self):
        task1 = TaskInfo("Buy clock",    0, 1376712000, 2)
        task2 = TaskInfo("Basketball",   0, 1376701200, 1)
        task3 = TaskInfo("Wash clothes", 0, 1376701200, 2)
        task4 = TaskInfo("Send letter",  0, 1376704800, 4)

        queue = Queue(task1, task2, task3, task4, "Qiandao Lake")
        self.assertEqual(queue.size(), 4)
        self.assertEqual(queue.empty(), False)

        # it is Basketball
        task  = queue[0]
        self.assertEqual(task.name, "Basketball")
        self.assertEqual(task.time, 1376701200)
예제 #2
0
    def test_taskqueue(self):
        task1 = TaskInfo("Buy clock", 0, 1376712000, 2)
        task2 = TaskInfo("Basketball", 0, 1376701200, 1)
        task3 = TaskInfo("Wash clothes", 0, 1376701200, 2)
        task4 = TaskInfo("Send letter", 0, 1376704800, 4)

        queue = Queue(task1, task2, task3, task4, "Qiandao Lake")
        self.assertEqual(queue.size(), 4)
        self.assertEqual(queue.empty(), False)

        # it is Basketball
        task = queue[0]
        self.assertEqual(task.name, "Basketball")
        self.assertEqual(task.time, 1376701200)
예제 #3
0
파일: scheduler.py 프로젝트: bjtulug/mirror
    def reload_config(self):
        log.info("Stopping running tasks...")
        signal.signal(signal.SIGCHLD, signal.SIG_DFL)
        self.stop_all_tasks()
        signal.signal(signal.SIGCHLD, mirror.handler.sigchld_handler)

        log.info("Clearing old data...")
        self.tasks = odict(filter(lambda x:x[1].isinternal,self.tasks.items()))
        self.queue = Queue()

        log.info("Reloading new configs...")
        self.config = ConfigManager("mirror.ini", need_reload = True)
        self.init_general(self.config)
        self.init_tasks  (self.config)
예제 #4
0
파일: scheduler.py 프로젝트: znanl/mirror
    def __init__(self, options=None, args=None):
        # The name is "Scheduler"
        super(Scheduler, self).__init__(self.__class__.__name__)
        # self.tasks contains all tasks needed to run in theory,
        # including the tasks that are not enabled
        self.config = ConfigManager("mirror.ini")
        self.tasks = odict()
        self.queue = Queue()
        self.todo = self.SCHEDULE_TASK
        # the number of tasks that enabled
        self.active_tasks = -1
        self.roused_by_child = False

        self.init_general(self.config)
        self.init_tasks(self.config)
예제 #5
0
    def reload_config(self):
        log.info("Stopping running tasks...")
        signal.signal(signal.SIGCHLD, signal.SIG_DFL)
        self.stop_all_tasks()
        signal.signal(signal.SIGCHLD, mirror.handler.sigchld_handler)

        log.info("Clearing old data...")
        for taskname, task in self.tasks.iteritems():
            if not task.isinternal:
                del self.tasks[taskname]
        self.queue = Queue()

        log.info("Reloading new configs...")
        self.config = ConfigManager("mirror.ini", need_reload=True)
        self.init_general(self.config)
        self.init_tasks(self.config)
예제 #6
0
파일: scheduler.py 프로젝트: yyxfei/mirror
    def __init__(self, options = None, args = None):
        # The name is "Scheduler"
        super(Scheduler, self).__init__(self.__class__.__name__)
        # self.tasks contains all tasks needed to run in theory,
        # including the tasks that are not enabled
        self.config  = ConfigManager("mirror.ini")
        self.tasks   = odict()
        self.queue   = Queue()
        self.todo    = self.SCHEDULE_TASK
        # the number of tasks that enabled
        self.active_tasks    = -1
        self.roused_by_child = False

        self.init_general(self.config)
        self.init_tasks  (self.config)
예제 #7
0
파일: scheduler.py 프로젝트: yyxfei/mirror
    def reload_config(self):
        log.info("Stopping running tasks...")
        signal.signal(signal.SIGCHLD, signal.SIG_DFL)
        self.stop_all_tasks()
        signal.signal(signal.SIGCHLD, mirror.handler.sigchld_handler)

        log.info("Clearing old data...")
        for taskname, task in self.tasks.iteritems():
            if not task.isinternal:
                del self.tasks[taskname]
        self.queue = Queue()

        log.info("Reloading new configs...")
        self.config = ConfigManager("mirror.ini", need_reload = True)
        self.init_general(self.config)
        self.init_tasks  (self.config)
예제 #8
0
파일: scheduler.py 프로젝트: yyxfei/mirror
class Scheduler(Component):
    CHECK_TIMEOUT = 0x01
    SCHEDULE_TASK = 0x02

    def __init__(self, options = None, args = None):
        # The name is "Scheduler"
        super(Scheduler, self).__init__(self.__class__.__name__)
        # self.tasks contains all tasks needed to run in theory,
        # including the tasks that are not enabled
        self.config  = ConfigManager("mirror.ini")
        self.tasks   = odict()
        self.queue   = Queue()
        self.todo    = self.SCHEDULE_TASK
        # the number of tasks that enabled
        self.active_tasks    = -1
        self.roused_by_child = False

        self.init_general(self.config)
        self.init_tasks  (self.config)

    def start(self):
        event_manager = component.get("EventManager")
        event_manager.emit(mirror.event.MirrorStartEvent())
        while (True):
            self.sleep()
            if not self.roused_by_child:
                log.info("I am waking up...")
            self.schedule()

    TODO = { REGULAR_TASK : SCHEDULE_TASK,
             SYSTEM_TASK  : SCHEDULE_TASK,
             TIMEOUT_TASK : CHECK_TIMEOUT,
            }

    def sleep(self):
        self.append_tasks()
        self.write_mmap()

        nexttask  = self.queue[0]
        self.todo = 0
        if nexttask:
            for taskinfo in self.queue:
                if taskinfo.time > nexttask.time:
                    break
                self.todo |= self.TODO.get(taskinfo.tasktype, 0)
        # nexttask.time - time.time() is
        # the duration we can sleep...
        if nexttask:
            sleeptime = nexttask.time - time.time()
        else:
            sleeptime = 1800 # half an hour
        log.info("I am going to sleep, next waking up: %s",
                 time.ctime(time.time() + sleeptime))
        self.roused_by_child = False
        time.sleep(sleeptime)

    def schedule(self):
        if self.queue.empty():
            log.info("But no task needed to start...")
            return

        self.init_sysinfo()

        # we do not need microseconds
        curtime    = int(time.time())
        taskqueue  = [ taskinfo for taskinfo in self.queue ]
        if ( self.todo & self.SCHEDULE_TASK ):
            # to move to zero second
            timestamp  = curtime
            timestamp -= curtime   % 60
            # next miniute
            end        = timestamp + 60
            for taskinfo in taskqueue:
                if self.TODO.get(taskinfo.tasktype, 0) != self.SCHEDULE_TASK:
                    continue
                if taskinfo.time < timestamp:
                    log.info("Strange problem happened,"
                             "task: %s schedule time is in past,"
                             "maybe I sleeped too long...", taskinfo.name)
                    self.queue.remove(taskinfo)
                    self.append_task(taskinfo.name, self.tasks[taskinfo.name], since = end)
                if taskinfo.time >= end:
                    break
                if taskinfo.time >= timestamp and taskinfo.time < end:
                    self.schedule_task(taskinfo)

        if ( self.todo & self.CHECK_TIMEOUT ):
            for taskinfo in taskqueue:
                if self.TODO.get(taskinfo.tasktype, 0) != self.CHECK_TIMEOUT:
                    continue
                if taskinfo.time <= curtime:
                    log.info("Task: %s timeouts",
                             taskinfo.name)
                    self.stop_task(taskinfo)
                if taskinfo.time > curtime:
                    break

    def schedule_task(self, taskinfo):
        """
        Schedule a task, but it is not guaranteed that it will really be run, it is 
        decided by some conditions, e.g. system load, current http connections.

        NOTE:
        The priority that a task can run is a function of ( current value / limit ).
        See get_runnable_priority(). However an exception is `maxtasks`, if current
        running tasks is reaching `maxtasks`, only specific priority (lower than or
        equal to 2) tasks can still be running.

        """
        task = self.tasks[taskinfo.name]
        if task.isinternal:
            self.run_system_task(taskinfo)
            return

        if task.priority > self.get_runnable_priority(self.current_load, self.loadlimit):
            log.info("Task: %s not scheduled because system load %.2f is too high",
                     taskinfo.name, self.current_load)
            self.delay_task(taskinfo)
            return
        if task.priority > self.get_runnable_priority(self.current_conn,  self.httpconn):
            log.info("Task: %s not scheduled because http connections is too many",
                     taskinfo.name)
            self.delay_task(taskinfo)
            return
        if self.maxtasks > 0 and self.count_running_tasks() >= self.maxtasks and task.priority > 2:
            log.info("Task: %s not scheduled because running tasks is larger than %d",
                     taskinfo, self.maxtasks)
            self.delay_task(taskinfo)
            return
        log.info("Starting task: %s ...", taskinfo.name)
        self.run_task(taskinfo)

    def init_sysinfo(self):
        """
        Get system info for this turn of schedule().

        """
        self.current_load = loadavg()
        self.current_conn = tcpconn()

    def delay_task(self, taskinfo, delay_seconds = 1800):
        """
        If a task is not scheduled due to some reason, it will be
        delayed for `delay_seconds` seconds (wich is default half
        an hour), when task's next schedule time is later than that,
        else it's set to task's next schedule time.

        """
        if taskinfo not in self.queue:
            return
        task      = self.tasks[taskinfo.name]
        next_time = task.get_schedule_time(since = time.time())
        if taskinfo.time + delay_seconds > next_time:
            taskinfo.time  = next_time
        else:
            # In python objects are passed by reference
            taskinfo.time += delay_seconds
        self.reappend_task(task, taskinfo)

    def count_running_tasks(self):
        """
        Calculate the number of current running tasks.

        """
        running = 0
        for taskname, task in self.tasks.iteritems():
            if task.isinternal:
                continue
            running += task.running
        return running

    def append_tasks(self):
        """
        Append the tasks that are needed to run into self.queue.

        NOTE:
        If a task is currently running or it is not enabled, it will
        not be added to the queue.

        """
        now = time.time()
        for taskname in self.tasks:
            task = self.tasks[taskname]
            self.append_task(taskname, task, since = now)

    def append_task(self, taskname, task, since):
        """
        In some cases a task with same name may be ignored if there
        is a running one, but this is a feature, not a bug...

        """
        if task.running and task.timeout > 0:
            return
        if not task.enabled:
            return
        taskinfo = TaskInfo(taskname, (SYSTEM_TASK if task.isinternal else REGULAR_TASK),
                            task.get_schedule_time(since), task.priority)

        # for system tasks and tasks-without-timeout-set, task is appended again
        # in run_*_task() method, for tasks-with-timeout-set, a timeout task
        # with same name is appended in run_task()
        if taskinfo in self.queue:
            return
        self.queue.put(taskinfo)
        event_manager = component.get("EventManager")
        event_manager.emit(mirror.event.TaskEnqueueEvent(taskname))

    def reappend_task(self, task, taskinfo):
        """
        Remove a taskinfo from queue and put it in again,
        to keep the queue in order.

        """
        if taskinfo not in self.queue:
            return
        self.queue.remove(taskinfo)
        self.queue.put(taskinfo)
        event_manager = component.get("EventManager")
        event_manager.emit(mirror.event.TaskEnqueueEvent(taskinfo.name))

    DEFAULT_BUFFER_SIZE  = 10240

    def write_mmap(self):
        data = pickle.dumps(self.queue)
        size = len(data) + 2 + 4
        if not hasattr(self, "buffer") or self.buffersz < size:
            self.buffersz = max(self.DEFAULT_BUFFER_SIZE, size)
            if hasattr(self, "buffer"):
                self.buffer.close()
            self.bufferfd = os.open("/tmp/mirrord",
                                    os.O_CREAT | os.O_TRUNC | os.O_RDWR,
                                    0o644)
            flag = fcntl.fcntl(self.bufferfd, fcntl.F_GETFD)
            fcntl.fcntl(self.bufferfd, fcntl.F_SETFD, flag | fcntl.FD_CLOEXEC)
            os.write(self.bufferfd, '\x00' * self.buffersz)
            self.buffer   = mmap.mmap(self.bufferfd, self.buffersz, mmap.MAP_SHARED, mmap.PROT_WRITE)
            # close bufferfd
            os.close(self.bufferfd)
            self.buffer.write("\x79\x71")
        self.buffer.seek(2)
        self.buffer.write(struct.pack("I", size))
        self.buffer.write(data)

    def stop(self):
        log.info("Stopping mirror scheduler")
        if not hasattr(self, "buffer"):
            return
        self.buffer.close()
        os.unlink("/tmp/mirrord")

    def append_timeout_task(self, taskname, task, time):
        """
        A timeout checking task is added after a task begins to run.

        """
        if not task.running:
            return
        if not task.enabled:
            return
        taskinfo = TaskInfo(taskname, TIMEOUT_TASK,
                            time, task.priority)
        if taskinfo in self.queue:
            return
        self.queue.put(taskinfo)
        event_manager = component.get("EventManager")
        event_manager.emit(mirror.event.TaskEnqueueEvent(taskname))

    def remove_timeout_task(self, taskname):
        """
        It's slow...

        """
        taskqueue  = [ taskinfo for taskinfo in self.queue ]
        for taskinfo in taskqueue:
            if taskinfo.name == taskname and taskinfo.tasktype == TIMEOUT_TASK:
                self.queue.remove(taskinfo)
                return

    def init_general(self, config):
        self.emails    = []
        self.loadlimit = 4.0
        self.httpconn  = 1200
        self.logdir    = mirror.common.DEFAULT_TASK_LOG_DIR
        self.maxtasks  = 10

        if "general" not in config:
            log.error("Error in config file, no `general` section, will use default setting.")
            return
        import re
        emails = re.compile(r"([^@\s]+@[^@\s,]+)")
        emails = emails.findall(config['general']['emails'])
        for email in emails:
            self.emails.append(email)

        self.loadlimit = float(config['general']['loadlimit'])
        self.httpconn  = int  (config['general']['httpconn'] )
        self.maxtasks  = int  (config['general']['maxtasks'] )
        self.logdir    = config['general']['logdir']
        if self.logdir[-1] != os.path.sep:
            self.logdir += os.path.sep

    def reload_config(self):
        log.info("Stopping running tasks...")
        signal.signal(signal.SIGCHLD, signal.SIG_DFL)
        self.stop_all_tasks()
        signal.signal(signal.SIGCHLD, mirror.handler.sigchld_handler)

        log.info("Clearing old data...")
        for taskname, task in self.tasks.iteritems():
            if not task.isinternal:
                del self.tasks[taskname]
        self.queue = Queue()

        log.info("Reloading new configs...")
        self.config = ConfigManager("mirror.ini", need_reload = True)
        self.init_general(self.config)
        self.init_tasks  (self.config)

    def init_tasks(self, config):
        for mirror in config:
            if mirror == 'general':
                continue
            # We think it's default mirror.task.Task
            task_class = TASK_TYPES.get(config[mirror].get("type", None), Task)
            self.tasks[mirror] = task_class(mirror, weakref.ref(self), **config[mirror])
        self.active_tasks = len(
                            [mirror for mirror, task in self.tasks.iteritems() if task.enabled])

    def run_system_task(self, taskinfo):
        event_manager = component.get("EventManager")
        event_manager.emit(mirror.event.RunSystemTaskEvent(taskinfo))
        # after we run the system task, we need to update the queue
        # or else the sleeptime will be invalid
        if taskinfo in self.queue:
            self.queue.remove(taskinfo)
        task = self.tasks[taskinfo.name]
        self.append_task(taskinfo.name, task, time.time())

    def run_task(self, taskinfo, stage = 1):
        if taskinfo.name not in self.tasks:
            return
        task = self.tasks[taskinfo.name]
        # for tasks that is still running when next schedule time
        # is reached (but has no timeout set), we just need to
        # reappend it.
        if task.running and task.timeout <= 0:
            taskinfo.time  = task.get_schedule_time(since = time.time())
            self.reappend_task(task, taskinfo)
        if task.running and ( not task.twostage ):
            log.info("Task: %s is still running and no timeout set, skipped", taskinfo.name)
            return

        event_manager = component.get("EventManager")
        event_manager.emit(mirror.event.PreTaskStartEvent(taskinfo.name))
        task.run(stage)
        if taskinfo in self.queue:
            self.queue.remove(taskinfo)
        log.info("Task: %s begin to run with pid %d", taskinfo.name, task.pid)
        event_manager.emit(mirror.event.TaskStartEvent(taskinfo.name, task.pid))

        if task.timeout <= 0:
            self.append_task(taskinfo.name, task, time.time())
        else:
            self.append_timeout_task(taskinfo.name, task,
                                     task.start_time + task.timeout)

    def stop_task(self, taskinfo):
        """
        Stop a task, it should only be called when that task timeouts.

        """
        if taskinfo.name not in self.tasks:
            return
        task = self.tasks[taskinfo.name]
        if not task.running:
            return
        pid  = task.pid
        # Python's SIGCHLD sometimes has delay in calling its handler,
        # we have to disable sigchld_handler() here.
        # More: http://utcc.utoronto.ca/~cks/space/blog/python/CPythonSignals
        signal.signal(signal.SIGCHLD, signal.SIG_DFL)
        task.stop()
        self.stop_task_manually(task, pid)
        signal.signal(signal.SIGCHLD, mirror.handler.sigchld_handler)

    def stop_task_manually(self, task, pid):
        """
        Without SIGCHLD handler, we have to waitpid() here.

        """
        pid, status  = os.waitpid(pid, 0)
        endstr, code = self.parse_return_status(status)
        task.code    = code
        log.info("Killed task: %s %s %d, pid %d", task.name, endstr, code, pid)
        self.remove_timeout_task(task.name)
        self.task_finished(task)

    def stop_task_with_pid(self, pid, status):
        """
        This is called when we got a SIGCHLD signal.
        Change task's running and pid attr as it's stopped.

        """
        self.roused_by_child = True
        for taskname, task in self.tasks.iteritems():
            if task.isinternal:
                continue
            if task.pid == pid:
                if not task.running:
                    return
                endstr, code = self.parse_return_status(status)
                task.code    = code
                log.info("Task: %s %s %d, pid %d", taskname, endstr, code, pid)
                self.remove_timeout_task(taskname)
                self.task_finished(task)
                return

    def task_finished(self, task):
        """
        Check whether a task needs post process, e.g. two stage tasks.

        """
        event_manager = component.get("EventManager")
        if not task.twostage:
            event_manager.emit(mirror.event.TaskStopEvent(task.name, task.pid, task.code))
            self.task_autoretry(task)
            task.set_stop_flag()
            return
        if task.stage == 1:
            log.info("Task: %s scheduled to second stage", task.name)
            self.run_task(TaskInfo(task.name, REGULAR_TASK, 0, task.priority), stage = 2)
        else:
            event_manager.emit(mirror.event.TaskStopEvent(task.name, task.pid, task.code))
            self.task_autoretry(task)
            task.set_stop_flag()
            task.stage = 1

    def task_autoretry(self, task):
        """
        If a task has a valid `autoretry`, and its interval is before next normal schedule,
        it will be used.

        """
        if task.autoretry <= 0:
            return
        if task.code == 0:
            return

    def stop_all_tasks(self, signo = signal.SIGTERM):
        """
        This method can only be called when mirrord is shut down by SIGTERM or SIGINT.

        NOTE:
        Currently when mirrord is shut down, all running tasks will also be killed.

        """
        event_manager = component.get("EventManager")
        for taskname, task in self.tasks.iteritems():
            if task.isinternal:
                continue
            if not task.running:
                continue
            pid = task.pid
            task.stop(signo)
            # Not sure it is ok...
            pid, status  = os.waitpid(pid, 0)

            endstr, code = self.parse_return_status(status)
            task.code    = code
            event_manager.emit(mirror.event.TaskStopEvent(task.name, task.pid, task.code))
            log.info("Killed task: %s with pid %d", taskname, pid)

    @classmethod
    def get_runnable_priority(cls, current, limit):
        """
        If limit is zero, all priority tasks can be run.
        Else if current value is lower than limit, all priority tasks can be run.
        Else it is a function between target priority and (current / limit).

        """
        if limit <= 0:
            return PRIORITY_MAX
        if current < limit:
            return PRIORITY_MAX
        return (-4.55 * (current * 1.0 / limit)) + 14.55

    @classmethod
    def parse_return_status(cls, status):
        if (status & 0xff) != 0:
            endstr = "killed by signal"
            code   = (status & 0xff)
        else:
            endstr = "ended with return code"
            # See "EXIT VALUES" section in man rsync
            code   = (status >> 8)
        return (endstr, code)
예제 #9
0
class Scheduler(Component):
    CHECK_TIMEOUT = 0x01
    SCHEDULE_TASK = 0x02

    def __init__(self, options=None, args=None):
        # The name is "Scheduler"
        super(Scheduler, self).__init__(self.__class__.__name__)
        # self.tasks contains all tasks needed to run in theory,
        # including the tasks that are not enabled
        self.config = ConfigManager("mirror.ini")
        self.tasks = odict()
        self.queue = Queue()
        self.todo = self.SCHEDULE_TASK
        # the number of tasks that enabled
        self.active_tasks = -1
        self.expect_time = 0
        self.roused_by_child = False

        self.init_general(self.config)
        self.init_tasks(self.config)

    def start(self):
        event_manager = component.get("EventManager")
        event_manager.emit(mirror.event.MirrorStartEvent())
        while (True):
            self.sleep()
            if not self.roused_by_child:
                log.info("I am waking up...")
            self.schedule()

    TODO = {
        REGULAR_TASK: SCHEDULE_TASK,
        SYSTEM_TASK: SCHEDULE_TASK,
        TIMEOUT_TASK: CHECK_TIMEOUT,
    }

    def sleep(self):
        self.append_tasks()
        self.write_mmap()

        nexttask = self.queue[0]
        self.todo = 0
        if nexttask:
            for taskinfo in self.queue:
                if taskinfo.time > nexttask.time:
                    break
                self.todo |= self.TODO.get(taskinfo.tasktype, 0)
        # nexttask.time - time.time() is
        # the duration we can sleep...
        if nexttask:
            sleeptime = nexttask.time - time.time()
            # if system time is updated by ntpdate or other ways,
            # we may get a sleeptime < 0 and thus an "Invalid argument" error,
            # so we need to change it to a valid value
            sleeptime = 0 if sleeptime < 0 else sleeptime
        else:
            sleeptime = 1800  # half an hour
        log.info("I am going to sleep, next waking up: %s",
                 time.ctime(time.time() + sleeptime))
        self.expect_time = int(time.time()) + sleeptime
        self.roused_by_child = False
        time.sleep(sleeptime)

    def schedule(self):
        if self.queue.empty():
            log.info("But no task needed to start...")
            return

        self.init_sysinfo()

        curtime = time.time()
        taskqueue = [taskinfo for taskinfo in self.queue]

        # detect if time has been set back (e.g. by ntpdate) to the right value
        if (not self.roused_by_child and curtime < self.expect_time
                and self.todo & self.SCHEDULE_TASK):
            time_gap = self.expect_time - curtime
            for taskinfo in taskqueue:
                if self.TODO.get(taskinfo.tasktype, 0) != self.SCHEDULE_TASK:
                    continue
                taskinfo.time -= time_gap

        # we do not need microseconds
        curtime = int(curtime)

        if (self.todo & self.SCHEDULE_TASK):
            # to move to zero second
            timestamp = curtime
            timestamp -= curtime % 60
            # next miniute
            end = timestamp + 60
            for taskinfo in taskqueue:
                if self.TODO.get(taskinfo.tasktype, 0) != self.SCHEDULE_TASK:
                    continue
                if taskinfo.time < timestamp:
                    log.info(
                        "Strange problem happened,"
                        "task: %s schedule time is in past,"
                        "maybe because of system time changed, but it's also scheduled...",
                        taskinfo.name)
                    self.schedule_task(taskinfo)
                if taskinfo.time >= end:
                    break
                if taskinfo.time >= timestamp and taskinfo.time < end:
                    self.schedule_task(taskinfo)

        if (self.todo & self.CHECK_TIMEOUT):
            for taskinfo in taskqueue:
                if self.TODO.get(taskinfo.tasktype, 0) != self.CHECK_TIMEOUT:
                    continue
                if taskinfo.time <= curtime:
                    log.info("Task: %s timeouts", taskinfo.name)
                    self.stop_task(taskinfo)
                if taskinfo.time > curtime:
                    break

    def schedule_task(self, taskinfo):
        """
        Schedule a task, but it is not guaranteed that it will really be run, it is
        decided by some conditions, e.g. system load, current http connections.

        NOTE:
        The priority that a task can run is a function of ( current value / limit ).
        See get_runnable_priority(). However an exception is `maxtasks`, if current
        running tasks is reaching `maxtasks`, only specific priority (lower than or
        equal to 2) tasks can still be running.

        """
        task = self.tasks[taskinfo.name]
        if task.isinternal:
            self.run_system_task(taskinfo)
            return

        if task.priority > self.get_runnable_priority(self.current_load,
                                                      self.loadlimit):
            log.info(
                "Task: %s not scheduled because system load %.2f is too high",
                taskinfo.name, self.current_load)
            self.delay_task(taskinfo)
            return
        if task.priority > self.get_runnable_priority(self.current_conn,
                                                      self.httpconn):
            log.info(
                "Task: %s not scheduled because http connections is too many",
                taskinfo.name)
            self.delay_task(taskinfo)
            return
        if self.maxtasks > 0 and self.count_running_tasks(
        ) >= self.maxtasks and task.priority > 2:
            log.info(
                "Task: %s not scheduled because running tasks is larger than %d",
                taskinfo, self.maxtasks)
            self.delay_task(taskinfo)
            return
        log.info("Starting task: %s ...", taskinfo.name)
        self.run_task(taskinfo)

    def init_sysinfo(self):
        """
        Get system info for this turn of schedule().

        """
        self.current_load = loadavg()
        self.current_conn = tcpconn()

    def delay_task(self, taskinfo, delay_seconds=1800):
        """
        If a task is not scheduled due to some reason, it will be
        delayed for `delay_seconds` seconds (wich is default half
        an hour), when task's next schedule time is later than that,
        else it's set to task's next schedule time.

        """
        if taskinfo not in self.queue:
            return
        task = self.tasks[taskinfo.name]
        next_time = task.get_schedule_time(since=time.time())
        if taskinfo.time + delay_seconds > next_time:
            taskinfo.time = next_time
        else:
            # In python objects are passed by reference
            taskinfo.time += delay_seconds
        self.reappend_task(task, taskinfo)

    def count_running_tasks(self):
        """
        Calculate the number of current running tasks.

        """
        running = 0
        for taskname, task in self.tasks.iteritems():
            if task.isinternal:
                continue
            running += task.running
        return running

    def append_tasks(self):
        """
        Append the tasks that are needed to run into self.queue.

        NOTE:
        If a task is currently running or it is not enabled, it will
        not be added to the queue.

        """
        now = time.time()
        for taskname in self.tasks:
            task = self.tasks[taskname]
            self.append_task(taskname, task, since=now)

    def append_task(self, taskname, task, since):
        """
        In some cases a task with same name may be ignored if there
        is a running one, but this is a feature, not a bug...

        """
        if task.running and task.timeout > 0:
            return
        if not task.enabled:
            return
        taskinfo = TaskInfo(taskname,
                            (SYSTEM_TASK if task.isinternal else REGULAR_TASK),
                            task.get_schedule_time(since), task.priority)

        # for system tasks and tasks-without-timeout-set, task is appended again
        # in run_*_task() method, for tasks-with-timeout-set, a timeout task
        # with same name is appended in run_task()
        if taskinfo in self.queue:
            return
        self.queue.put(taskinfo)
        event_manager = component.get("EventManager")
        event_manager.emit(mirror.event.TaskEnqueueEvent(taskname))

    def reappend_task(self, task, taskinfo):
        """
        Remove a taskinfo from queue and put it in again,
        to keep the queue in order.

        """
        if taskinfo not in self.queue:
            return
        self.queue.remove(taskinfo)
        self.queue.put(taskinfo)
        event_manager = component.get("EventManager")
        event_manager.emit(mirror.event.TaskEnqueueEvent(taskinfo.name))

    DEFAULT_BUFFER_SIZE = 10240

    def write_mmap(self):
        data = pickle.dumps(self.queue)
        size = len(data) + 2 + 4
        if not hasattr(self, "buffer") or self.buffersz < size:
            self.buffersz = max(self.DEFAULT_BUFFER_SIZE, size)
            if hasattr(self, "buffer"):
                self.buffer.close()
            self.bufferfd = os.open("/tmp/mirrord",
                                    os.O_CREAT | os.O_TRUNC | os.O_RDWR, 0o644)
            flag = fcntl.fcntl(self.bufferfd, fcntl.F_GETFD)
            fcntl.fcntl(self.bufferfd, fcntl.F_SETFD, flag | fcntl.FD_CLOEXEC)
            os.write(self.bufferfd, '\x00' * self.buffersz)
            self.buffer = mmap.mmap(self.bufferfd, self.buffersz,
                                    mmap.MAP_SHARED, mmap.PROT_WRITE)
            # close bufferfd
            os.close(self.bufferfd)
            self.buffer.write("\x79\x71")
        self.buffer.seek(2)
        self.buffer.write(struct.pack("I", size))
        self.buffer.write(data)

    def stop(self):
        log.info("Stopping mirror scheduler")
        if not hasattr(self, "buffer"):
            return
        self.buffer.close()
        os.unlink("/tmp/mirrord")

    def append_timeout_task(self, taskname, task, time):
        """
        A timeout checking task is added after a task begins to run.

        """
        if not task.running:
            return
        if not task.enabled:
            return
        taskinfo = TaskInfo(taskname, TIMEOUT_TASK, time, task.priority)
        if taskinfo in self.queue:
            return
        self.queue.put(taskinfo)
        event_manager = component.get("EventManager")
        event_manager.emit(mirror.event.TaskEnqueueEvent(taskname))

    def remove_timeout_task(self, taskname):
        """
        It's slow...

        """
        taskqueue = [taskinfo for taskinfo in self.queue]
        for taskinfo in taskqueue:
            if taskinfo.name == taskname and taskinfo.tasktype == TIMEOUT_TASK:
                self.queue.remove(taskinfo)
                return

    def init_general(self, config):
        self.emails = []
        self.loadlimit = 4.0
        self.httpconn = 1200
        self.logdir = mirror.common.DEFAULT_TASK_LOG_DIR
        self.maxtasks = 10

        if "general" not in config:
            log.error(
                "Error in config file, no `general` section, will use default setting."
            )
            return
        import re
        emails = re.compile(r"([^@\s]+@[^@\s,]+)")
        emails = emails.findall(config['general']['emails'])
        for email in emails:
            self.emails.append(email)

        self.loadlimit = float(config['general']['loadlimit'])
        self.httpconn = int(config['general']['httpconn'])
        self.maxtasks = int(config['general']['maxtasks'])
        self.logdir = config['general']['logdir']
        if self.logdir[-1] != os.path.sep:
            self.logdir += os.path.sep

    def reload_config(self):
        log.info("Stopping running tasks...")
        signal.signal(signal.SIGCHLD, signal.SIG_DFL)
        self.stop_all_tasks()
        signal.signal(signal.SIGCHLD, mirror.handler.sigchld_handler)

        log.info("Clearing old data...")
        for taskname, task in self.tasks.iteritems():
            if not task.isinternal:
                del self.tasks[taskname]
        self.queue = Queue()

        log.info("Reloading new configs...")
        self.config = ConfigManager("mirror.ini", need_reload=True)
        self.init_general(self.config)
        self.init_tasks(self.config)

    def init_tasks(self, config):
        for mirror in config:
            if mirror == 'general':
                continue
            # We think it's default mirror.task.Task
            task_class = TASK_TYPES.get(config[mirror].get("type", None), Task)
            self.tasks[mirror] = task_class(mirror, weakref.ref(self),
                                            **config[mirror])
        self.active_tasks = len([
            mirror for mirror, task in self.tasks.iteritems() if task.enabled
        ])

    def run_system_task(self, taskinfo):
        event_manager = component.get("EventManager")
        event_manager.emit(mirror.event.RunSystemTaskEvent(taskinfo))
        # after we run the system task, we need to update the queue
        # or else the sleeptime will be invalid
        if taskinfo in self.queue:
            self.queue.remove(taskinfo)
        task = self.tasks[taskinfo.name]
        self.append_task(taskinfo.name, task, time.time())

    def run_task(self, taskinfo, stage=1):
        if taskinfo.name not in self.tasks:
            return
        task = self.tasks[taskinfo.name]
        # for tasks that is still running when next schedule time
        # is reached (but has no timeout set), we just need to
        # reappend it.
        if task.running and task.timeout <= 0:
            taskinfo.time = task.get_schedule_time(since=time.time())
            self.reappend_task(task, taskinfo)
        if task.running and (not task.twostage):
            log.info("Task: %s is still running and no timeout set, skipped",
                     taskinfo.name)
            return

        event_manager = component.get("EventManager")
        event_manager.emit(mirror.event.PreTaskStartEvent(taskinfo.name))
        task.run(stage)
        if taskinfo in self.queue:
            self.queue.remove(taskinfo)
        log.info("Task: %s begin to run with pid %d", taskinfo.name, task.pid)
        event_manager.emit(mirror.event.TaskStartEvent(taskinfo.name,
                                                       task.pid))

        if task.timeout <= 0:
            self.append_task(taskinfo.name, task, time.time())
        else:
            self.append_timeout_task(taskinfo.name, task,
                                     task.start_time + task.timeout)

    def stop_task(self, taskinfo):
        """
        Stop a task, it should only be called when that task timeouts.

        """
        if taskinfo.name not in self.tasks:
            return
        task = self.tasks[taskinfo.name]
        if not task.running:
            return
        pid = task.pid
        # Python's SIGCHLD sometimes has delay in calling its handler,
        # we have to disable sigchld_handler() here.
        # More: http://utcc.utoronto.ca/~cks/space/blog/python/CPythonSignals
        signal.signal(signal.SIGCHLD, signal.SIG_DFL)
        task.stop()
        self.stop_task_manually(task, pid)
        signal.signal(signal.SIGCHLD, mirror.handler.sigchld_handler)

    def stop_task_manually(self, task, pid):
        """
        Without SIGCHLD handler, we have to waitpid() here.

        """
        pid, status = os.waitpid(pid, 0)
        endstr, code = self.parse_return_status(status)
        task.code = code
        log.info("Killed task: %s %s %d, pid %d", task.name, endstr, code, pid)
        self.remove_timeout_task(task.name)
        self.task_finished(task)

    def stop_task_with_pid(self, pid, status):
        """
        This is called when we got a SIGCHLD signal.
        Change task's running and pid attr as it's stopped.

        """
        self.roused_by_child = True
        for taskname, task in self.tasks.iteritems():
            if task.isinternal:
                continue
            if task.pid == pid:
                if not task.running:
                    return
                endstr, code = self.parse_return_status(status)
                task.code = code
                log.info("Task: %s %s %d, pid %d", taskname, endstr, code, pid)
                self.remove_timeout_task(taskname)
                self.task_finished(task)
                return

    def task_finished(self, task):
        """
        Check whether a task needs post process, e.g. two stage tasks.

        """
        event_manager = component.get("EventManager")
        if not task.twostage:
            event_manager.emit(
                mirror.event.TaskStopEvent(task.name, task.pid, task.code))
            self.task_autoretry(task)
            task.set_stop_flag()
            return
        if task.stage == 1:
            log.info("Task: %s scheduled to second stage", task.name)
            self.run_task(TaskInfo(task.name, REGULAR_TASK, 0, task.priority),
                          stage=2)
        else:
            event_manager.emit(
                mirror.event.TaskStopEvent(task.name, task.pid, task.code))
            self.task_autoretry(task)
            task.set_stop_flag()
            task.stage = 1

    def task_autoretry(self, task):
        """
        If a task has a valid `autoretry`, and its interval is before next normal schedule,
        it will be used.

        """
        if task.autoretry <= 0:
            return
        if task.code == 0:
            return
        curtime = int(time.time())
        next_time = task.get_schedule_time(since=curtime)
        if curtime + task.autoretry < next_time:
            taskinfo = self.queue[task.name]
            taskinfo.time = next_time
            self.reappend_task(task, taskinfo)

    def stop_all_tasks(self, signo=signal.SIGTERM):
        """
        This method can only be called when mirrord is shut down by SIGTERM or SIGINT.

        NOTE:
        Currently when mirrord is shut down, all running tasks will also be killed.

        """
        event_manager = component.get("EventManager")
        for taskname, task in self.tasks.iteritems():
            if task.isinternal:
                continue
            if not task.running:
                continue
            pid = task.pid
            task.stop(signo)
            # Not sure it is ok...
            pid, status = os.waitpid(pid, 0)

            endstr, code = self.parse_return_status(status)
            task.code = code
            event_manager.emit(
                mirror.event.TaskStopEvent(task.name, task.pid, task.code))
            log.info("Killed task: %s with pid %d", taskname, pid)

    @classmethod
    def get_runnable_priority(cls, current, limit):
        """
        If limit is zero, all priority tasks can be run.
        Else if current value is lower than limit, all priority tasks can be run.
        Else it is a function between target priority and (current / limit).

        """
        if limit <= 0:
            return PRIORITY_MAX
        if current < limit:
            return PRIORITY_MAX
        return (-4.55 * (current * 1.0 / limit)) + 14.55

    @classmethod
    def parse_return_status(cls, status):
        if (status & 0xff) != 0:
            endstr = "killed by signal"
            code = (status & 0xff)
        else:
            endstr = "ended with return code"
            # See "EXIT VALUES" section in man rsync
            code = (status >> 8)
        return (endstr, code)