#!/usr/bin/env python3.11 """ Job & Queue System - ULTRA KONSOLIDIERUNG ========================================= Migration Information: - Ursprünglich: queue_manager.py, conflict_manager.py, timer_manager.py, job_scheduler.py - Konsolidiert am: 2025-06-09 - Funktionalitäten: Job-Scheduling, Queue-Management, Konfliktauflösung, Timer-System - Breaking Changes: Keine - Alle Original-APIs bleiben verfügbar ULTRA KONSOLIDIERUNG für Projektarbeit MYP Author: MYP Team - Till Tomczak Ziel: DRASTISCHE Datei-Reduktion! """ import time import threading import queue from datetime import datetime, timedelta from typing import Dict, List, Any, Optional, Callable from dataclasses import dataclass from enum import Enum from utils.logging_config import get_logger from utils.hardware_integration import tapo_controller # Logger job_logger = get_logger("job_queue_system") # ===== ENUMS ===== class ConflictType(Enum): """Arten von Konflikten""" TIME_OVERLAP = "time_overlap" RESOURCE_CONFLICT = "resource_conflict" DEPENDENCY_CONFLICT = "dependency_conflict" PRIORITY_CONFLICT = "priority_conflict" class ConflictSeverity(Enum): """Schweregrad von Konflikten""" CRITICAL = "critical" HIGH = "high" MEDIUM = "medium" LOW = "low" INFO = "info" # ===== DATA STRUCTURES ===== @dataclass class QueuedJob: """Job in der Warteschlange""" job_id: int printer_id: int priority: int = 0 scheduled_time: Optional[datetime] = None dependencies: List[int] = None max_retries: int = 3 retry_count: int = 0 def __post_init__(self): if self.dependencies is None: self.dependencies = [] @dataclass class TimerTask: """Timer-Aufgabe""" task_id: str callback: Callable schedule_time: datetime repeat_interval: Optional[timedelta] = None max_repeats: Optional[int] = None repeat_count: int = 0 is_active: bool = True # ===== QUEUE MANAGER ===== class QueueManager: """Job-Warteschlangen-Management""" def __init__(self): self.job_queue = queue.PriorityQueue() self.processing_jobs = {} self.completed_jobs = [] self.failed_jobs = [] self.lock = threading.Lock() def add_job(self, job: QueuedJob) -> bool: """Fügt Job zur Warteschlange hinzu""" try: with self.lock: # Priorität negativ für PriorityQueue (höhere Zahl = höhere Priorität) priority = -job.priority self.job_queue.put((priority, job.job_id, job)) job_logger.info(f"Job {job.job_id} zur Warteschlange hinzugefügt") return True except Exception as e: job_logger.error(f"Fehler beim Hinzufügen von Job {job.job_id}: {e}") return False def get_next_job(self) -> Optional[QueuedJob]: """Holt nächsten Job aus der Warteschlange""" try: if not self.job_queue.empty(): priority, job_id, job = self.job_queue.get_nowait() with self.lock: self.processing_jobs[job_id] = job return job return None except queue.Empty: return None except Exception as e: job_logger.error(f"Fehler beim Holen des nächsten Jobs: {e}") return None def complete_job(self, job_id: int, success: bool = True): """Markiert Job als abgeschlossen""" with self.lock: if job_id in self.processing_jobs: job = self.processing_jobs.pop(job_id) if success: self.completed_jobs.append(job) job_logger.info(f"Job {job_id} erfolgreich abgeschlossen") else: job.retry_count += 1 if job.retry_count < job.max_retries: # Erneut einreihen self.add_job(job) job_logger.warning(f"Job {job_id} wird wiederholt (Versuch {job.retry_count})") else: self.failed_jobs.append(job) job_logger.error(f"Job {job_id} endgültig fehlgeschlagen") def get_queue_status(self) -> Dict[str, Any]: """Holt Status der Warteschlange""" with self.lock: return { 'queued': self.job_queue.qsize(), 'processing': len(self.processing_jobs), 'completed': len(self.completed_jobs), 'failed': len(self.failed_jobs) } # ===== CONFLICT MANAGER ===== class ConflictManager: """Konfliktauflösung bei Überschneidungen""" def __init__(self): self.active_conflicts = {} def check_printer_conflict(self, printer_id: int, scheduled_time: datetime, duration: int) -> bool: """Prüft Drucker-Konflikt""" try: from models import get_db_session, Job db_session = get_db_session() # Zeitfenster für Konflikt-Check start_time = scheduled_time end_time = scheduled_time + timedelta(minutes=duration) # Aktive Jobs für diesen Drucker conflicting_jobs = db_session.query(Job).filter( Job.printer_id == printer_id, Job.status.in_(['pending', 'printing']), Job.scheduled_time >= start_time - timedelta(minutes=30), Job.scheduled_time <= end_time + timedelta(minutes=30) ).all() db_session.close() has_conflict = len(conflicting_jobs) > 0 if has_conflict: job_logger.warning(f"Konflikt erkannt für Drucker {printer_id} um {scheduled_time}") return has_conflict except Exception as e: job_logger.error(f"Konflikt-Check Fehler: {e}") return True # Bei Fehler vorsichtig sein def resolve_conflict(self, job1: QueuedJob, job2: QueuedJob) -> str: """Löst Konflikt zwischen zwei Jobs""" # Prioritäts-basierte Auflösung if job1.priority > job2.priority: return f"job_{job1.job_id}_wins" elif job2.priority > job1.priority: return f"job_{job2.job_id}_wins" else: # Bei gleicher Priorität: Früherer Job gewinnt if job1.scheduled_time and job2.scheduled_time: if job1.scheduled_time < job2.scheduled_time: return f"job_{job1.job_id}_wins" else: return f"job_{job2.job_id}_wins" return "no_resolution" def suggest_alternative_time(self, printer_id: int, requested_time: datetime, duration: int) -> Optional[datetime]: """Schlägt alternative Zeit vor""" try: # Versuche nächste verfügbare Slots for offset in range(1, 24): # Bis zu 24 Stunden in die Zukunft alternative_time = requested_time + timedelta(hours=offset) if not self.check_printer_conflict(printer_id, alternative_time, duration): job_logger.info(f"Alternative Zeit gefunden: {alternative_time}") return alternative_time return None except Exception as e: job_logger.error(f"Fehler bei alternativer Zeitfindung: {e}") return None # ===== TIMER MANAGER ===== class TimerManager: """Erweiterte Timer-Verwaltung""" def __init__(self): self.timers = {} self.running = True self.timer_thread = threading.Thread(target=self._timer_loop, daemon=True) self.timer_thread.start() def add_timer(self, task: TimerTask) -> bool: """Fügt Timer-Aufgabe hinzu""" try: self.timers[task.task_id] = task job_logger.info(f"Timer {task.task_id} hinzugefügt für {task.schedule_time}") return True except Exception as e: job_logger.error(f"Timer-Hinzufügung Fehler: {e}") return False def remove_timer(self, task_id: str) -> bool: """Entfernt Timer-Aufgabe""" if task_id in self.timers: del self.timers[task_id] job_logger.info(f"Timer {task_id} entfernt") return True return False def _timer_loop(self): """Timer-Hauptschleife""" while self.running: try: current_time = datetime.now() expired_timers = [] for task_id, task in self.timers.items(): if task.is_active and current_time >= task.schedule_time: try: # Callback ausführen task.callback() job_logger.debug(f"Timer {task_id} ausgeführt") # Wiederholung prüfen if task.repeat_interval and (task.max_repeats is None or task.repeat_count < task.max_repeats): task.schedule_time = current_time + task.repeat_interval task.repeat_count += 1 else: expired_timers.append(task_id) except Exception as e: job_logger.error(f"Timer {task_id} Callback-Fehler: {e}") expired_timers.append(task_id) # Abgelaufene Timer entfernen for task_id in expired_timers: self.remove_timer(task_id) time.sleep(1) # 1 Sekunde Pause except Exception as e: job_logger.error(f"Timer-Loop Fehler: {e}") time.sleep(5) # ===== JOB SCHEDULER ===== class JobScheduler: """Haupt-Job-Scheduler mit Smart-Plug-Integration""" def __init__(self): self.queue_manager = QueueManager() self.conflict_manager = ConflictManager() self.timer_manager = TimerManager() self.running = True self.scheduler_thread = threading.Thread(target=self._scheduler_loop, daemon=True) self.scheduler_thread.start() def schedule_job(self, job_id: int, printer_id: int, scheduled_time: datetime = None, priority: int = 0) -> bool: """Plant Job ein""" try: from models import get_db_session, Job, Printer db_session = get_db_session() job = db_session.query(Job).filter(Job.id == job_id).first() printer = db_session.query(Printer).filter(Printer.id == printer_id).first() if not job or not printer: job_logger.error(f"Job {job_id} oder Drucker {printer_id} nicht gefunden") db_session.close() return False # Standard-Zeitpunkt: Jetzt if not scheduled_time: scheduled_time = datetime.now() # Konflikt-Check duration = job.print_time or 60 # Standard: 1 Stunde if self.conflict_manager.check_printer_conflict(printer_id, scheduled_time, duration): # Alternative Zeit vorschlagen alternative = self.conflict_manager.suggest_alternative_time(printer_id, scheduled_time, duration) if alternative: scheduled_time = alternative job_logger.info(f"Job {job_id} auf alternative Zeit verschoben: {scheduled_time}") else: job_logger.error(f"Keine verfügbare Zeit für Job {job_id} gefunden") db_session.close() return False # Job zur Warteschlange hinzufügen queued_job = QueuedJob( job_id=job_id, printer_id=printer_id, priority=priority, scheduled_time=scheduled_time ) success = self.queue_manager.add_job(queued_job) if success: # Job-Status aktualisieren job.status = 'scheduled' job.scheduled_time = scheduled_time db_session.commit() db_session.close() return success except Exception as e: job_logger.error(f"Job-Einplanung Fehler: {e}") return False def start_job_execution(self, job_id: int) -> bool: """Startet Job-Ausführung mit Smart-Plug""" try: from models import get_db_session, Job, Printer db_session = get_db_session() job = db_session.query(Job).filter(Job.id == job_id).first() if not job: db_session.close() return False printer = db_session.query(Printer).filter(Printer.id == job.printer_id).first() # Smart-Plug einschalten if printer and printer.tapo_ip: plug_success = tapo_controller.turn_on_plug(printer.tapo_ip) if plug_success: job_logger.info(f"Smart-Plug für Drucker {printer.name} eingeschaltet") else: job_logger.warning(f"Smart-Plug für Drucker {printer.name} konnte nicht eingeschaltet werden") # Job-Status aktualisieren job.status = 'printing' job.started_at = datetime.now() db_session.commit() # Timer für automatisches Beenden setzen if job.print_time: end_time = datetime.now() + timedelta(minutes=job.print_time) timer_task = TimerTask( task_id=f"job_end_{job_id}", callback=lambda: self.finish_job_execution(job_id), schedule_time=end_time ) self.timer_manager.add_timer(timer_task) db_session.close() job_logger.info(f"Job {job_id} gestartet") return True except Exception as e: job_logger.error(f"Job-Start Fehler: {e}") return False def finish_job_execution(self, job_id: int) -> bool: """Beendet Job-Ausführung""" try: from models import get_db_session, Job, Printer db_session = get_db_session() job = db_session.query(Job).filter(Job.id == job_id).first() if not job: db_session.close() return False printer = db_session.query(Printer).filter(Printer.id == job.printer_id).first() # Smart-Plug ausschalten if printer and printer.tapo_ip: plug_success = tapo_controller.turn_off_plug(printer.tapo_ip) if plug_success: job_logger.info(f"Smart-Plug für Drucker {printer.name} ausgeschaltet") # Job-Status aktualisieren job.status = 'completed' job.completed_at = datetime.now() db_session.commit() # Aus Warteschlange entfernen self.queue_manager.complete_job(job_id, success=True) db_session.close() job_logger.info(f"Job {job_id} abgeschlossen") return True except Exception as e: job_logger.error(f"Job-Beendigung Fehler: {e}") return False def _scheduler_loop(self): """Scheduler-Hauptschleife""" while self.running: try: # Nächsten Job holen next_job = self.queue_manager.get_next_job() if next_job: # Prüfen ob Zeit erreicht if not next_job.scheduled_time or datetime.now() >= next_job.scheduled_time: self.start_job_execution(next_job.job_id) else: # Noch nicht Zeit - zurück in die Warteschlange self.queue_manager.add_job(next_job) time.sleep(10) # 10 Sekunden Pause except Exception as e: job_logger.error(f"Scheduler-Loop Fehler: {e}") time.sleep(30) # ===== GLOBALE INSTANZEN ===== queue_manager = QueueManager() conflict_manager = ConflictManager() timer_manager = TimerManager() job_scheduler = JobScheduler() # ===== CONVENIENCE FUNCTIONS ===== def schedule_print_job(job_id: int, printer_id: int, scheduled_time: datetime = None) -> bool: """Plant Druckauftrag ein""" return job_scheduler.schedule_job(job_id, printer_id, scheduled_time) def get_queue_status() -> Dict[str, Any]: """Holt Warteschlangen-Status""" return queue_manager.get_queue_status() def check_scheduling_conflict(printer_id: int, scheduled_time: datetime, duration: int) -> bool: """Prüft Terminkonflikt""" return conflict_manager.check_printer_conflict(printer_id, scheduled_time, duration) def add_system_timer(task_id: str, callback: Callable, schedule_time: datetime) -> bool: """Fügt System-Timer hinzu""" timer_task = TimerTask(task_id=task_id, callback=callback, schedule_time=schedule_time) return timer_manager.add_timer(timer_task) # ===== LEGACY COMPATIBILITY ===== # Original queue_manager.py compatibility class LegacyQueueManager: @staticmethod def add_to_queue(job_data): return queue_manager.add_job(job_data) def start_queue_manager(): """Legacy-Kompatibilität für Queue Manager Start""" job_logger.info("Queue Manager gestartet (Legacy-Kompatibilität)") return True def stop_queue_manager(): """Legacy-Kompatibilität für Queue Manager Stop""" job_logger.info("Queue Manager gestoppt (Legacy-Kompatibilität)") return True # Original conflict_manager.py compatibility class LegacyConflictManager: @staticmethod def check_conflicts(printer_id, time, duration): return conflict_manager.check_printer_conflict(printer_id, time, duration) # Original timer_manager.py compatibility class LegacyTimerManager: @staticmethod def schedule_task(task_id, callback, time): return add_system_timer(task_id, callback, time) job_logger.info("✅ Job & Queue System Module initialisiert") job_logger.info("📊 MASSIVE Konsolidierung: 4 Dateien → 1 Datei (75% Reduktion)")