feat(sprint4): completar optimizador heuristico concurrente, integracion Spring Boot, correccion de login y fixes visuales

This commit is contained in:
Carlos Tello
2026-09-05 02:20:47 -03:00
parent dd93b8c07a
commit 69d545b718
17 changed files with 1021 additions and 91 deletions
+5 -1
View File
@@ -4,6 +4,7 @@ from .reservation_service import ReservationService
from .user_service import UserService
from .jwt_service import JWTService
from .audit_service import AuditService
from .optimizer_service import OptimizerService, OptimizerJob, OptimizerJobStatus
__all__ = [
'GoogleSheetsImporter',
@@ -11,5 +12,8 @@ __all__ = [
'ReservationService',
'UserService',
'JWTService',
'AuditService'
'AuditService',
'OptimizerService',
'OptimizerJob',
'OptimizerJobStatus'
]
+282
View File
@@ -0,0 +1,282 @@
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"
}