import time import uuid import threading from datetime import datetime, timedelta from enum import Enum from typing import List, Dict, Any, Optional from flask import current_app from app.models.genetic_algorithm import ( ReservationOptimizer, GeneticAlgorithm, ReservationRequest, Classroom ) from app.models.subject import Commission from app.schemas.optimizer_dto import SpringBootSyncPayloadDTO class OptimizerJobStatus(str, Enum): PENDING = "PENDING" RUNNING = "RUNNING" COMPLETED = "COMPLETED" FAILED = "FAILED" CANCELLED = "CANCELLED" class OptimizerJob: """Representa un trabajo asíncrono en la cola del optimizador heurístico.""" def __init__(self, job_id: str, params: Dict[str, Any]): self.job_id = job_id self.status = OptimizerJobStatus.PENDING self.progress = 0 self.generation = 0 self.total_generations = params.get("generations", 80) self.fitness_score = 0.0 self.total_requests = 0 self.assigned_reservations = 0 self.reservations: List[Dict[str, Any]] = [] self.conflicts: List[Dict[str, Any]] = [] self.error: Optional[str] = None self.created_at = datetime.utcnow() self.started_at: Optional[datetime] = None self.completed_at: Optional[datetime] = None self.execution_time_ms: Optional[float] = None self.params = params def to_dict(self) -> Dict[str, Any]: return { "job_id": self.job_id, "status": self.status.value, "progress": self.progress, "generation": self.generation, "total_generations": self.total_generations, "fitness_score": round(self.fitness_score, 2), "total_requests": self.total_requests, "assigned_reservations": self.assigned_reservations, "reservations": self.reservations, "conflict_count": len(self.conflicts), "conflicts": self.conflicts, "error": self.error, "created_at": self.created_at.isoformat() if self.created_at else None, "started_at": self.started_at.isoformat() if self.started_at else None, "completed_at": self.completed_at.isoformat() if self.completed_at else None, "execution_time_ms": self.execution_time_ms, "params": {k: v for k, v in self.params.items() if k not in ("app",)} } class OptimizerService: """ Servicio de alto rendimiento para el Optimizador Heurístico y Cola de Trabajos Asíncronos. Soporta evaluación concurrente multi-hilo y sincronización interoperable con Spring Boot. """ _jobs: Dict[str, OptimizerJob] = {} _lock = threading.Lock() @classmethod def submit_job( cls, commission_ids: List[int], admin_user_id: int, start_date: Optional[datetime] = None, end_date: Optional[datetime] = None, generations: int = 80, population_size: int = 40, workers: int = 4, app = None ) -> OptimizerJob: """ Registra y despacha un trabajo de optimización en segundo plano (asíncrono). """ job_id = str(uuid.uuid4()) params = { "commission_ids": commission_ids, "admin_user_id": admin_user_id, "start_date": start_date, "end_date": end_date, "generations": generations, "population_size": population_size, "workers": workers } job = OptimizerJob(job_id, params) with cls._lock: cls._jobs[job_id] = job # Si no se pasó app explícitamente, intentar obtener current_app if app is None: try: app = current_app._get_current_object() except Exception: app = None thread = threading.Thread( target=cls._run_job_worker, args=(job_id, app), daemon=True, name=f"optimizer-worker-{job_id[:8]}" ) thread.start() return job @classmethod def _run_job_worker(cls, job_id: str, app): """Worker en segundo plano para ejecutar el algoritmo genético.""" job = cls.get_job(job_id) if not job: return def _execute(): t_start = time.perf_counter() with cls._lock: job.status = OptimizerJobStatus.RUNNING job.started_at = datetime.utcnow() def progress_callback(gen: int, total_gen: int, pct: int, best_fit: float): with cls._lock: job.generation = gen job.total_generations = total_gen job.progress = pct job.fitness_score = best_fit optimizer = ReservationOptimizer( population_size=job.params.get("population_size", 40), generations=job.params.get("generations", 80), workers=job.params.get("workers", 4) ) result = optimizer.optimize_schedule( commission_ids=job.params["commission_ids"], admin_user_id=job.params["admin_user_id"], start_date=job.params.get("start_date"), end_date=job.params.get("end_date"), progress_callback=progress_callback ) t_end = time.perf_counter() exec_time_ms = round((t_end - t_start) * 1000, 2) with cls._lock: job.status = OptimizerJobStatus.COMPLETED job.progress = 100 job.fitness_score = result.get("fitness_score", 0.0) job.total_requests = result.get("total_requests", 0) job.assigned_reservations = result.get("assigned_reservations", 0) job.reservations = result.get("reservations", []) job.conflicts = result.get("conflicts", []) job.completed_at = datetime.utcnow() job.execution_time_ms = exec_time_ms try: if app: with app.app_context(): _execute() else: _execute() except Exception as ex: with cls._lock: job.status = OptimizerJobStatus.FAILED job.error = str(ex) job.completed_at = datetime.utcnow() @classmethod def get_job(cls, job_id: str) -> Optional[OptimizerJob]: """Obtiene un trabajo por su identificador único.""" with cls._lock: return cls._jobs.get(job_id) @classmethod def list_jobs(cls, limit: int = 50) -> List[Dict[str, Any]]: """Lista los trabajos más recientes ordenados por fecha de creación desc.""" with cls._lock: sorted_jobs = sorted(cls._jobs.values(), key=lambda j: j.created_at, reverse=True) return [j.to_dict() for j in sorted_jobs[:limit]] @classmethod def sync_with_spring_boot( cls, payload: SpringBootSyncPayloadDTO, admin_user_id: int = 1 ) -> Dict[str, Any]: """ Procesa una carga de optimización interoperable proveniente de microservicios Spring Boot. Garantiza respuesta conforme al contrato JSON del microservicio. """ t_start = time.perf_counter() # 1. Resolver aulas disponibles if payload.classrooms and len(payload.classrooms) > 0: classrooms = [] for c_dto in payload.classrooms: room = Classroom( room_number=c_dto.roomNumber, capacity=c_dto.capacity, is_virtual=c_dto.isVirtual, is_active=True ) room.id = c_dto.classroomId classrooms.append(room) else: classrooms = Classroom.query.filter_by(is_active=True).all() # 2. Generar ReservationRequests desde el DTO de Spring Boot requests: List[ReservationRequest] = [] for c in payload.commissions: req = ReservationRequest( commission_id=c.commissionId, expected_attendees=c.expectedAttendees, purpose=f"Spring Boot Class: {c.subjectCode} - {c.subjectName}", preferred_start_time=c.preferredStart, preferred_end_time=c.preferredEnd, priority=1, flexibility_hours=2 ) requests.append(req) # 3. Opciones de configuración de algoritmo options = payload.options or {} generations = options.get("generations", 60) pop_size = options.get("populationSize", 30) workers = options.get("workers", 4) ga = GeneticAlgorithm( population_size=pop_size, generations=generations, workers=workers ) start_date = min(c.preferredStart for c in payload.commissions) end_date = max(c.preferredEnd for c in payload.commissions) best_individual = ga.optimize_reservations(requests, classrooms, start_date, end_date) t_end = time.perf_counter() exec_ms = round((t_end - t_start) * 1000, 2) scheduled_assignments = [] for gene in best_individual.genes: scheduled_assignments.append({ "commissionId": gene.commission_id, "classroomId": gene.classroom_id, "startTime": gene.start_time.isoformat() if gene.start_time else None, "endTime": gene.end_time.isoformat() if gene.end_time else None, "purpose": gene.purpose, "expectedAttendees": gene.expected_attendees, "status": "SCHEDULED" }) return { "requestId": payload.requestId, "status": "SUCCESS", "academicTerm": payload.academicTerm, "campusCode": payload.campusCode, "totalCommissions": len(payload.commissions), "scheduledAssignments": scheduled_assignments, "conflictCount": len(best_individual.conflicts), "fitnessScore": round(best_individual.fitness, 2), "executionTimeMs": exec_ms, "timestamp": datetime.utcnow().isoformat() + "Z" }