"""Background job runner with log streaming.""" import threading, time, uuid, queue _jobs: dict[str, dict] = {} _queues: dict[str, queue.Queue] = {} _stream_locks: dict[str, threading.Lock] = {} _lock = threading.Lock() _TTL = 3600 def _cleanup(): """Verwijder voltooide jobs ouder dan TTL uit geheugen.""" cutoff = time.time() - _TTL with _lock: stale = [jid for jid, j in _jobs.items() if j["status"] != "running" and j["ts"] < cutoff] for jid in stale: _jobs.pop(jid, None) _queues.pop(jid, None) _stream_locks.pop(jid, None) def create(tag: str) -> tuple[str, queue.Queue]: _cleanup() jid = uuid.uuid4().hex[:8] q = queue.Queue() with _lock: _jobs[jid] = {"id": jid, "tag": tag, "status": "running", "lines": [], "ts": time.time()} _queues[jid] = q _stream_locks[jid] = threading.Lock() return jid, q def get_queue(jid: str) -> queue.Queue | None: return _queues.get(jid) def log(q, level: str, text: str): if q: q.put({"level": level, "text": text}) def finish(jid: str, status="done"): with _lock: if jid in _jobs: _jobs[jid]["status"] = status def done(q): if q: q.put(None) def stream(jid: str, offset=0) -> dict: job = _jobs.get(jid) if not job: return {"lines": [], "status": "unknown"} with _stream_locks.get(jid, threading.Lock()): q = _queues.get(jid) if q: while True: try: item = q.get_nowait() if item is None: break job["lines"].append(item) except queue.Empty: break return {"lines": job["lines"][offset:], "status": job["status"]} def run(fn, *args): threading.Thread(target=fn, args=args, daemon=True).start()