Esta plataforma ha sido diseñada para ejecutar de forma segura y remota fragmentos de código enviados por los usuarios, siendo ideal para entornos de coding challenges. Utiliza contenedores Docker para aislar y ejecutar el código, garantizando que cada contenedor se destruya después de la ejecución. Además, está preparada para soportar múltiples usuarios concurrentes, escalando de manera eficiente según la demanda.
- Introducción
- Flujo de Ejecución del API
- Arquitectura de Red y Comunicación
- Ventajas del Enfoque HTTP
- Consideraciones de Seguridad
Esta API es el núcleo de un sistema de ejecución de código remoto, desarrollado para permitir a los usuarios enviar código en varios lenguajes (como Python, JavaScript, C++, entre otros) y obtener los resultados de forma segura. La ejecución se realiza en contenedores Docker aislados que se eliminan automáticamente tras el procesamiento, lo que garantiza que el sistema se mantenga limpio y seguro.
A continuación se detalla el flujo completo desde que se realiza la solicitud hasta que se obtiene el resultado:
- Endpoint: Se envía una solicitud
POST
a/execute
junto con el código a ejecutar y el lenguaje seleccionado. - Validación: El manejador de solicitudes
executeHandler
(definido enapi/main.go
) valida la petición. - Identificación del Trabajo: Se genera un identificador único para el trabajo (Job ID) utilizando UUID, lo que permite rastrear cada ejecución de forma individual.
- Serialización: El código, el lenguaje y el identificador se empaquetan en una estructura
Job
. - Cola de Redis: La estructura se serializa a JSON y se empuja a una cola en Redis denominada
code_jobs
. - Respuesta Inmediata: La API responde al cliente de forma inmediata, devolviendo el Job ID para que el usuario pueda posteriormente consultar el estado del proceso.
- Polling de Redis: Un servicio worker ejecutándose en
worker/main.go
monitorea la colacode_jobs
utilizando el comandoBRPOP
para retirar trabajos de forma bloqueante. - Deserialización: Al recibir un trabajo, el worker deserializa el JSON a un objeto
Job
. - Ejecución del Código: Se invoca la función
executeCode
, encargada de gestionar el proceso de ejecución.
- Almacenamiento Temporal del Código: El worker guarda el código en una estructura en memoria (
codeStore
) asociada a un ID único. - Exposición vía HTTP: Se dispone de un endpoint HTTP (
/code
) que sirve el código almacenado, permitiendo que el contenedor Docker lo recupere. - Lanzamiento del Contenedor:
- Se inicia un contenedor Docker que ejecuta el código en el lenguaje indicado.
- Se utiliza una variable de entorno (
CODE_URL=http://worker:8081/code?id=...
) para que el contenedor sepa dónde obtener el código.
- Ejecución y Captura de Salida:
- Dentro del contenedor, un script recupera el código mediante una petición HTTP al worker.
- El código se guarda en un archivo temporal con la extensión correspondiente (.py, .js, .cpp, etc.).
- El código se ejecuta con las herramientas específicas del lenguaje:
- Python: Se ejecuta con el intérprete
python
. - JavaScript: Se ejecuta con Node.js.
- C++: Se compila con
g++
y se ejecuta el binario resultante. - C#: Se compila con Mono C# compiler (mcs), ideal para ejecución rápida de un solo archivo.
- Python: Se ejecuta con el intérprete
- Se capturan la salida estándar y los errores generados durante la ejecución.
- Los archivos temporales se eliminan tras la ejecución.
-
Comparación de Salidas:
- En caso de ser una submission y no solo una ejecución de código,por cada entrada de prueba, la salida generada por el código se compara contra el resultado esperado.
- Si alguna salida no coincide, el test case falla y se detalla cuál falló (input, output esperado vs. obtenido).
-
Estructura del Resultado:
- Se incluye un resumen por cada test case: éxito o falla, con detalles precisos.
- Captura del Resultado: Una vez finalizada la ejecución, el contenedor devuelve los resultados (salida, errores y tiempos de ejecución) a la aplicación worker.
- Construcción del Resultado: El worker construye una estructura
JobResult
que incluye:- Estado de la ejecución.
- Salida generada.
- Mensajes de error (si existen).
- Información de tiempo.
- Almacenamiento en Redis: El resultado se serializa a JSON y se almacena en Redis bajo la clave
result:{job_id}
con un tiempo de expiración de 24 horas.
- Consulta al Resultado: El cliente puede realizar una solicitud
GET
a/result/{job_id}
para obtener el resultado. - Manejo de Respuestas:
- Si el resultado existe, se devuelve el JSON con el estado, salida, errores, etc.
- Si el trabajo aún está en proceso, se informa que el estado es "pending".
- Si el Job ID no es válido o el trabajo no existe, se retorna un error.
- Red Interna Docker Compose: Todos los servicios (API, Worker, Redis) se ejecutan dentro de una red definida en Docker Compose, facilitando la comunicación entre ellos.
- Comunicación entre Contenedores: El worker se comunica directamente con el demonio Docker mediante el socket, y los contenedores ejecutores alcanzan al worker usando el nombre del servicio "worker" en la red.
- Enfoque HTTP: La utilización de un endpoint HTTP para compartir el código evita problemas comunes relacionados con permisos en sistemas de archivos y montaje de volúmenes.
El sistema adopta un enfoque basado en HTTP para la transferencia de código entre componentes, lo que ofrece varias ventajas:
- Eliminación de Problemas de Permisos: Se evita el complicado manejo de permisos que puede surgir al montar volúmenes entre contenedores.
- Compatibilidad y Fiabilidad: El método HTTP es ampliamente soportado y funciona de manera consistente en entornos Docker.
- Separación de Responsabilidades: La lógica de envío y ejecución del código queda claramente separada, permitiendo una mayor modularidad y escalabilidad.
- Facilidad de Escalado: El sistema es capaz de gestionar múltiples solicitudes concurrentes, gracias al manejo eficiente de colas en Redis y la naturaleza efímera de los contenedores.
- Ejecución Aislada: Cada fragmento de código se ejecuta en un contenedor Docker independiente, lo que minimiza el riesgo de afectaciones al sistema principal.
- Destrucción de Contenedores: Los contenedores son destruidos inmediatamente después de la ejecución, evitando persistencia de código potencialmente malicioso.
- Validación de Entradas: La API valida todas las solicitudes para prevenir inyecciones de código y otros vectores de ataque.
- Monitoreo y Expiración de Resultados: Los resultados se almacenan temporalmente y se eliminan automáticamente después de 24 horas para proteger la privacidad y seguridad de los datos.