Files

283 lines
10 KiB
Python

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"
}