%PDF- %PDF-
Direktori : /proc/thread-self/root/usr/lib/calibre/calibre/srv/ |
Current File : //proc/thread-self/root/usr/lib/calibre/calibre/srv/jobs.py |
#!/usr/bin/env python3 # License: GPLv3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net> import os, time from itertools import count from collections import namedtuple, deque from functools import partial from threading import RLock, Thread, Event from calibre import detect_ncpus, force_unicode from calibre.utils.monotonic import monotonic from calibre.utils.ipc.simple_worker import fork_job, WorkerError from polyglot.queue import Queue, Empty from polyglot.builtins import iteritems, itervalues StartEvent = namedtuple('StartEvent', 'job_id name module function args kwargs callback data') DoneEvent = namedtuple('DoneEvent', 'job_id') class Job(Thread): daemon = True def __init__(self, start_event, events_queue): Thread.__init__(self, name='JobsMonitor%s' % start_event.job_id) self.abort_event = Event() self.events_queue = events_queue self.job_name = start_event.name self.job_id = start_event.job_id self.func = partial(fork_job, start_event.module, start_event.function, start_event.args, start_event.kwargs, abort=self.abort_event) self.data, self.callback = start_event.data, start_event.callback self.result = self.traceback = None self.done = False self.start_time = monotonic() self.end_time = self.log_path = None self.wait_for_end = Event() self.start() def run(self): func, self.func = self.func, None try: result = func() except WorkerError as err: import traceback self.traceback = err.orig_tb or traceback.format_exc() self.log_path = getattr(err, 'log_path', None) else: self.result, self.log_path = result['result'], result['stdout_stderr'] self.done = True self.end_time = monotonic() self.wait_for_end.set() self.events_queue.put(DoneEvent(self.job_id)) @property def was_aborted(self): return self.done and self.result is None and self.abort_event.is_set() @property def failed(self): return bool(self.traceback) or self.was_aborted def remove_log(self): lp, self.log_path = self.log_path, None if lp: try: os.remove(lp) except OSError: pass def read_log(self): ans = '' if self.log_path is not None: try: with lopen(self.log_path, 'rb') as f: ans = f.read() except OSError: pass if isinstance(ans, bytes): ans = force_unicode(ans, 'utf-8') return ans class JobsManager: def __init__(self, opts, log): mj = opts.max_jobs if mj < 1: mj = detect_ncpus() self.log = log self.max_jobs = max(1, mj) self.max_job_time = max(0, opts.max_job_time * 60) self.lock = RLock() self.jobs = {} self.finished_jobs = {} self.events = Queue() self.job_id = count() self.waiting_job_ids = set() self.waiting_jobs = deque() self.max_block = None self.shutting_down = False self.event_loop = None def start_job(self, name, module, func, args=(), kwargs=None, job_done_callback=None, job_data=None): with self.lock: if self.shutting_down: return None if self.event_loop is None: self.event_loop = t = Thread(name='JobsEventLoop', target=self.run) t.daemon = True t.start() job_id = next(self.job_id) self.events.put(StartEvent(job_id, name, module, func, args, kwargs or {}, job_done_callback, job_data)) self.waiting_job_ids.add(job_id) return job_id def job_status(self, job_id): with self.lock: if not self.shutting_down: if job_id in self.finished_jobs: job = self.finished_jobs[job_id] return 'finished', job.result, job.traceback, job.was_aborted if job_id in self.jobs: return 'running', None, None, None if job_id in self.waiting_job_ids: return 'waiting', None, None, None return None, None, None, None def abort_job(self, job_id): job = self.jobs.get(job_id) if job is not None: job.abort_event.set() def wait_for_running_job(self, job_id, timeout=None): job = self.jobs.get(job_id) if job is not None: job.wait_for_end.wait(timeout) if not job.done: return False while job_id not in self.finished_jobs: time.sleep(0.001) return True def shutdown(self, timeout=5.0): with self.lock: self.shutting_down = True for job in itervalues(self.jobs): job.abort_event.set() self.events.put(False) def wait_for_shutdown(self, wait_till): for job in itervalues(self.jobs): delta = wait_till - monotonic() if delta > 0: job.join(delta) if self.event_loop is not None: delta = wait_till - monotonic() if delta > 0: self.event_loop.join(delta) # Internal API {{{ def run(self): while not self.shutting_down: if self.max_block is None: ev = self.events.get() else: try: ev = self.events.get(block=True, timeout=self.max_block) except Empty: ev = None if self.shutting_down: break if ev is None: self.abort_hanging_jobs() elif isinstance(ev, StartEvent): self.waiting_jobs.append(ev) self.start_waiting_jobs() elif isinstance(ev, DoneEvent): self.job_finished(ev.job_id) elif ev is False: break def start_waiting_jobs(self): with self.lock: while self.waiting_jobs and len(self.jobs) < self.max_jobs: ev = self.waiting_jobs.popleft() self.jobs[ev.job_id] = Job(ev, self.events) self.waiting_job_ids.discard(ev.job_id) self.update_max_block() def update_max_block(self): with self.lock: mb = None now = monotonic() for job in itervalues(self.jobs): if not job.done and not job.abort_event.is_set(): delta = self.max_job_time - (now - job.start_time) if delta <= 0: self.max_block = 0 return if mb is None: mb = delta else: mb = min(mb, delta) self.max_block = mb def abort_hanging_jobs(self): now = monotonic() found = False for job in itervalues(self.jobs): if not job.done and not job.abort_event.is_set(): delta = self.max_job_time - (now - job.start_time) if delta <= 0: job.abort_event.set() found = True if found: self.update_max_block() def job_finished(self, job_id): with self.lock: self.finished_jobs[job_id] = job = self.jobs.pop(job_id) if job.callback is not None: try: job.callback(job) except Exception: import traceback self.log.error(f'Error running callback for job: {job.name}:\n{traceback.format_exc()}') self.prune_finished_jobs() if job.traceback and not job.was_aborted: logdata = job.read_log() self.log.error(f'The job: {job.job_name} failed:\n{logdata}\n{job.traceback}') job.remove_log() self.start_waiting_jobs() def prune_finished_jobs(self): with self.lock: remove = [] now = monotonic() for job_id, job in iteritems(self.finished_jobs): if now - job.end_time > 3600: remove.append(job_id) for job_id in remove: del self.finished_jobs[job_id] # }}} def sleep_test(x): time.sleep(x) return x def error_test(): raise Exception('a testing error')