1. Proy1.1: Servidor concurrente
El proyecto 1, sobre concurrencia de tareas y paralelismo de datos, se realiza en grupos de máximo cuatro personas. Entregar a más tardar el 28 de octubre a las 23:59. Las entregas tardías se tratarán como se estipula en la carta al estudiante.
1.1. Repositorio y archivos
Deben entregar su solución en el repositorio privado que para ese efecto ya creó cada grupo, dentro de una carpeta exclusiva para este proyecto, dado que el repositorio albergará los dos proyectos del curso. Deben asegurarse de que ambos docentes tengan acceso a dicho repositorio. Todos los miembros del grupo deben demostrar un nivel similar de participación en el desarrollo de la solución. Cada participante debe asegurarse de hacer commits frecuentes, que impliquen un aporte, con mensajes breves pero descriptivos. Debe haber una adecuada distribución de la carga de trabajo, y no desequilibrios como que "un miembro del equipo sólo documentó el código".
Los commits no deben "romper el build", como por ejemplo, introducir errores de compilación. Para el desarrollo de funcionalidades experimentales y para el trabajo en equipo, se recomienda el uso de muchas ramas cortas (branches), una por cada funcionalidad nueva o experimental, que se une (merge) a la rama principal (master) cuando esta acaba exitosamente, y no una rama por participante. Para la revisión se considerarán los commits en la rama principal (master) y uno de ellos rotulado (git tag
) por el equipo con avance01
(o en inglés milestone01
) será usado como referencia. En caso de que surjan, este historial de control de versiones se usará como evidencia para la resolución de reclamos y conflictos.
El código brindado, que es parte del ejemplo patrón productor-consumidor, sigue la estructura de archivos y directorios característica de un proyecto de Unix, descrita en el enunciado de la tarea01, y debe ser mantenida por los miembros del equipo. No es necesario (y no se recomienda) agregar el código de simulación de red a su carpeta de proyecto 01 en el repositorio.
Las clases, métodos, y tipos de datos, deben estar documentados con Doxygen
. La salida de Doxygen
debe estar en una carpeta doc/
y ésta no se debe agregar a control de versiones. Documentar el código no trivial en los cuerpos de las subrutinas. Su solución debe ser modular, dividiendo el código en subrutinas, clases, y archivos. El código debe tener un estilo consistente y no generar diagnósticos del linter (cpplint
).
1.2. Análisis
El objetivo del proyecto es desarrollar un servidor web concurrente y distribuido que permita a sus visitantes obtener la factorización prima de números. En este primer avance el servidor web debe ser concurrente, y la aplicación web debe ser orientada a objetos. En este proyecto el equipo trabaja con una base de código existente, lo cual es el escenario más probable que enfrente la persona profesional al integrarse a la fuerza laboral y para ello reciba una inducción de parte de otros profesionales. Una inducción a este código está disponible en formato de video en el siguiente enlace:
Los miembros del equipo elaboran el documento de análisis, tal como se estipula en la carta al estudiante, con el fin de emular la práctica en la industria donde los profesionales en informática recaban los requerimientos de los clientes. Los miembros del equipo deben recabar los requerimientos a partir de los insumos provistos (este documento y las grabaciones de inducción) y anotarlos en un archivo readme.ext
cuya extensión varía si es en formato Markdown, AsciiDoc, o LaTex. Este documento debe estar en la carpeta para el proyecto 01 en el repositorio de control de versiones.
Al leer el readme.ext
debe quedar claro a un lector ajeno al proyecto, qué problema éste resuelve, y quienes son los integrantes del equipo. Proveer además un manual de uso, que incluye cómo compilar el servidor web, cómo correrlo (por ejemplo, explicar los argumentos en línea de comandos que recibe) y cómo detenerlo (tanto con Ctrl+C como el comando kill
).
Una vez implementada la aplicación de factorización, agregar al inicio del readme.ext
una captura de pantalla de una consulta hecha a través de un navegador. Esta es una práctica común en los repositorios de control de versiones que provee un efecto más llamativo y profesional del proyecto. De hacerlo, guarden las imágenes en una subcarpeta img/
y no en la raíz del proyecto.
En la raíz del repositorio de control de versiones, agregue o modifique su readme.md
para indicar que es el repositorio de dos proyectos para el curso. Para cada proyecto agregue un párrafo que resuma su propósito, y un enlace hacia su respectivo documento de análisis.
1.3. Diseño
Esta primera entrega consiste en modificar el 'código heredado' serial para que el servidor web se comporte de manera concurrente, es decir, que sea capaz de aceptar y atender múltiples conexiones de manera concurrente. El servidor web debe recibir solicitudes de usuarios las cuales consisten de listas de números para los cuales una aplicación realizará su factorización prima, tal y como se les ha solicitado en las tareas individuales 1 y 2.
Su solución debe tener un buen diseño concurrente expresado mediante un diagrama de flujo de datos, es decir, una cadena de producción que resalte la concurrencia y la aplicación del patrón productor-consumidor. Puede tomar el siguiente diseño inicial como punto de partida (clic en la imagen para abrirla). En este diseño las elipses representan hilos de ejecución, los rectángulos nodos en colas, los hilos dentro de un rectángulo indican un equipo, y los rectángulos punteados indican objetos.
El diseño de la Figura 1 es incompleto. Por el contrario, su diseño debe ser concreto y mostrar el recorrido completo por el flujo de datos de al menos dos conexiones (dos sockets). En al menos uno de esos sockets se piden al menos dos solicitudes HTTP. En al menos una de las solicitudes HTTP se debe solicitar al menos dos números a factorizar. Al menos uno de los números a factorizar tiene al menos dos potencias primas. Estos datos se deben representar en los nodos de las colas.
Los nodos en las colas son objetos, y por lo tanto, instancias de una clase. Las clases se deben representar en UML. Por conveniencia estas clases pueden ubicarse en los alrededores del diagrama de flujo de datos para facilitar la lectura del diseño. Dado que los nodos de las colas son instancias de la clase, tendrán campos (atributos) y métodos. Sin embargo, en los nodos del diagrama de flujo de datos, basta con indicar sólo los valores de los campos en el mismo orden. No es necesario indicar los nombres de los atributos.
Los métodos más relevantes para la concurrencia en la cadena de producción corresponden a los métodos de correr los hilos, detenerlos, producir y consumir. Estos métodos pueden diseñarse en pseudocódigo como se ha visto en las lecciones del curso. Recuerde que el diseño debe centrarse en la lógica concurrente y no los detalles algorítmicos de más bajo nivel, tales las factorizaciones, establecimiento de conexiones de red entre clientes y servidor, entre otros.
Los diseños deben guardarse en la carpeta design/
dentro de su proyecto 1. Las imágenes deben exportarse a formato SVG (preferible) o PNG. Dentro de esta carpeta agregue el documento de diseño design/design.ext
, cuya extensión depende del formato (Markdown, AsciiDoc, LaTex), donde se explique de forma resumida su diseño e incruste las imágenes (diagramas) y listados de código (algoritmos).
Al leer el documento de diseño, una persona desarrolladora tendrá una comprensión de alto nivel de la solución ("una visión del bosque"). Es muy útil para la fase de implementación o para futuros miembros de un equipo con el fin de poder comprender el diseño de la solución.
En el documento de análisis para el proyecto, agregue una sección de diseño y un enlace el documento de diseño. Esto permitirá la navegabilidad desde la raíz del repositorio al proyecto 01, y de éste hacia su diseño.
Probablemente notará que en el diseño de la Figura 1 no hay una cadena de producción concurrente completa. Es una observación correcta. Al final de este avance el equipo tendrá la oportunidad de mejorar este diseño, y en el avance siguiente (proy1.2
) la oportunidad de implementar esta mejora haciendo concurrente la aplicación web (la parte derecha de la figura).
1.4. Servidor web concurrente
El 'código heredado' implementa un servidor web que se comporta de manera serial. Como es sabido, los servidores (o servicios) de software son de naturaleza concurrente. El reto del equipo es convertir a este servidor web serial en uno concurrente para que naturalmente pueda atender varias conexiones de clientes al mismo tiempo. Nótese que esta mejora sólo altera el código del servidor (parte izquierda de la Figura 1) y no de las aplicaciones web (parte derecha de la Figura 1). Estas se ven beneficiadas por la concurrencia del servidor aunque las aplicaciones sigan siendo seriales.
Al iniciar, el servidor web recibirá dos argumentos de línea de comandos, ambos opcionales, y el equipo podría agregar otros. El primer argumento es el puerto en el que el servidor web esperará por conexiones de clientes. El segundo argumento se puede llamar max_connections
e indica el número máximo de conexiones de clientes que el servidor web debe permitir de manera concurrente. Si este número no se indica, se puede suponer tantas conexiones como CPUs hay disponibles en la máquina. Una vez que el servidor inicia, se debe crear este número de hilos que se encargarán de atender conexiones. Estos hilos serán instancias de una clase cuyo nombre se sugiere sea HttpConnectionHandler
y se almacenará en la carpeta src/http/
(ver Figura 1).
Para poner los HttpConnectionHandler
en funcionamiento, se deben agregar a la cadena de producción. Esta cadena no existe, puesto que el servidor es serial. Se debe modificar la clase HttpServer
ubicada en la carpeta http/
del código base dado para implementar un patrón productor consumidor. El productor será el hilo principal que produce conexiones aceptadas con los clientes, llamadas sockets que corresponden a objetos de la clase Socket
. Los consumidores de estas conexiones serán los HttpConnectionHandler
.
El código en la carpeta prodcons/
servirá de base para crear nuevas clases de productores y consumidores. Puede examinar como ejemplo el programa simulation/
que también se describió durante las lecciones del curso. El código brindado tiene además documentación y anotaciones de tipo TODO:
que proporcionan una guía sobre lo que se debe implementar.
En el código dado, el servidor serial acepta una conexión, de ella extrae una solicitud HTTP, la responde, cierra la conexión, y continúa con la próxima conexión, y así indefinidamente. En la versión concurrente, este diseño debe iniciar una pequeña cadena de producción como la siguiente:
-
El hilo/objeto
HttpServer
sólo debe aceptar solicitudes y ponerlas en cola, nada más. -
Los hilos/objetos
HttpConnectionHandler
consumen las conexiones. -
Los hilos/objetos
HttpConnectionHandler
extraen todas las solicitudes del cliente (no sólo la primera). Por cada solicitud ensambla los objetosHttpRequest
yHttpResponse
. -
El
HttpConnectionHandler
es un objeto de la capa del servidor, no de aplicación. Por cada solicitud que extraiga, no debe atenderla directamente, sino que delega en las aplicaciones web. Es decir, elHttpConnectionHandler
recorrerá todas las aplicaciones web en busca de aquella encargada de atender la solicitud (si la hay). Para este avance la aplicación web es sólo un objeto, no un hilo de ejecución. Por lo tanto, la aplicación web es un objeto que recibirá la solicitud (los objetosHttpRequest
yHttpResponse
) y calculará la factorización prima de cada número solicitado en el mismo hilo de ejecución delHttpConnectionHandler
. -
Después de obtener la descomposición prima de los números de una solicitud, la aplicación web responderá directamente al cliente usando el socket, pero sin cerrarlo (a menos de que el cliente use el protocolo
HTTP/1.0
). -
Una vez que la aplicación web responde al cliente, el
HttpConnectionHandler
retorna al ciclo de solicitudes para atender más peticiones de factorización que el cliente pueda tener (paso 3). Cuando el cliente no tenga más solicitudes, se cierra la conexión (socket) y elHttpConnectionHandler
consumirá la próxima conexión disponible en la cola (paso 2).
1.5. Aplicación web orientada a objetos
En este primer avance no se requiere que la aplicación web (la que resuelve las factorizaciones primas) sea concurrente. Es decisión del equipo si usan una versión serial (tarea01) o concurrente (tarea02). Sin embargo, la aplicación web debe estar implementada en el paradigma de programación orientada a objetos y siguiendo el patrón modelo-vista-controlador (MVC), en coherencia con el resto de la base de código provisto. De acuerdo a este patrón, el código del servidor web (controlador), la aplicación web (vista), y el cálculo de factorizaciones (modelo), deben estar en objetos separados. Se recomienda una separación en al menos las dos siguientes clases de C++:
-
La aplicación web. Tiene principalmente responsabilidades de una vista ("lo que ve el usuario"). Se le puede dar un nombre como
FactWebApp
. Se encarga de recibir las solicitudes HTTP que emite el cliente, extraer los números del cuerpo de la solicitud, pasarlos al modelo (la clase siguiente), esperar la factorización prima que produce el modelo, ensamblar y despachar las respuestas HTTP que serán las que verán los usuarios. Nótese que ni el servidor web ni la aplicación web deben realizar cálculo alguno relativo a la factorización prima, pues de esto se encarga el objeto modelo. -
El modelo de factorización. Tiene estrictamente responsabilidades de un modelo, por lo tanto, no debe saber nada de web ni redes de computadoras. Se le puede dar un nombre como
PrimeFactModel
,PrimeFactSolver
,PrimeFactCalculator
, oFactCalculator
. Se encarga de encontrar la factorización prima de números representados en aritmética de precisión fija (o si se quiere, precisión arbitraria). Esta clase trabaja con estructuras de datos en memoria. Por ejemplo, recibe colecciones de números y retorna colecciones de números factorizados. No debe producir resultados en formato HTML ni texto formateado. Recuerde que por ser un modelo, esta clase debería poderse reutilizar en otras aplicaciones (ej.: de dispositivos móviles) sin cambio alguno.
Su solución debe realizar los cálculos correctamente. Es decir, la aplicación web responde con una página que tiene las factorizaciones primas. Para efectos de este proyecto, el servidor web produce un único mensaje de respuesta por cada solicitud HTTP del cliente, la cual contiene todos los resultados de los números solicitados. Esto implica que si el cálculo de las factorizaciones toma tiempo al servidor, el cliente deberá esperar.
La aplicación permite ingresar listas de números separados por comas, positivos o negativos, tanto en el URI (barra de direcciones del navegador), como el formulario web. La aplicación valida entradas. Números mal formados, fuera de rango, o entradas mal intencionadas son reportadas con mensajes de error en la página web resultado, en lugar de caerse o producir resultados con números incorrectos.
1.6. Finalización del servidor
Al finalizar el servidor web (con Ctrl+C o el comando kill
), éste debe reaccionar a la señal y hacer la limpieza debida. Por ejemplo, detenerse de aceptar más conexiones de clientes, avisar a los hilos secundarios para que terminen su ejecución, esperar que terminen el trabajo pendiente, y finalmente liberar recursos como estructuras de datos en memoria dinámica, archivos, u otros, sin provocar fugas de memoria ni otros problemas. Para lograr este efecto se recomienda realizar lo siguiente.
-
Convertir la clase
HttpServer
en un objeto singleton. Este es un patrón de software que se aplica a aquellas clases que sólo pueden tener una única instancia (objeto) dentro de un proceso. Sus constructores son privados y un método permite obtener acceso a esa única instancia. La claseLog
ya implementa este patrón. -
Registrar una función libre o método estático, técnicamente conocida como signal handler, que se invoque cuando el proceso reciba la señal
SIGINT
(Ctrl+C) oSIGTERM
(comandokill PID
). Véase la función de bibliotecasignal()
de la biblioteca estándar de C. -
Cuando el signal handler se invoque, consigue la instancia del servidor web y le solicita que se detenga: método
stop()
. -
El método
stop()
del servidor web debe impedir que éste continúe aceptando conexiones (sockets) con nuevos clientes. Este efecto se puede conseguir invocando el método heredadostopListening()
, el cual cierra el socket de aceptar conexiones. Por lo tanto, elHttpServer
falla al tratar de conseguir el próximo cliente, y produce una excepción, regresará al métodostart()
, y finalmente amain()
. Si la excepción no es manejada, el hilo principal terminará, y por consiguiente los demás hilos son destruidos de golpe sin ser esperados. -
En el método
HttpServer::start()
atrape la excepción (si no se hace ya). En su manejo haga la limpieza correspondiente para terminar los hilos secundarios que creó, liberar las estructuras de datos que alojó en memoria dinámica, cerrar archivos, etc. Por ejemplo, para detener hilos secundarios, recuerde enviar la condición de parada a sus colas y esperar a que ellos finalicen.
Para poder probar que su servidor finaliza adecuadamente y libera los recursos, puede correr el ejecutable instrumentalizado (ej.: asan
, tsan
, …) en forma interactiva y presionar Ctrl+C. Si corre el ejecutable en un ambiente simulado (ej.: memcheck
, helgrind
), abra otra terminal, obtenga el identificador de proceso (PID, Process ID) con el comando ps -eu
(Valgrind lo reporta en la forma ===PID===
) y detenga el proceso con el comando kill PID
donde PID
se reemplaza por el número de proceso que obtuvo con ps
. Al detener su proceso, no se deben obtener reportes de errores.
1.7. Pruebas de concurrencia
Una vez que se haya implementado, es necesario probar que el servidor web sea concurrente. Una prueba sencilla es crear dos conexiones, por ejemplo, con dos ventanas de un navegador. Solicitar primero en una ventana uno o varios números difíciles de factorizar, y por lo tanto, que tardan varios segundos. Mientras esa solicitud aún no haya terminado, en la segunda ventana del navegador solicitar uno o varios números pequeños. Si el servidor responde la segunda solicitud antes de que la primera finalice, es un indicio de que es concurrente. Si por el contrario, la segunda solicitud es atendida hasta después que la primera finalice, indica que el servidor web está serializando.
Para determinar que el servidor web realmente atiende varias conexiones de forma concurrente, se puede usar una herramienta de pruebas de estrés (stress testing), como httperf. Para este avance, es necesario demostrar que el servidor realmente responde a varias consultas de forma concurrente. Por lo tanto, se puede tener tanto el cliente como el servidor en la misma máquina, aunque esto no es recomendado. Por ejemplo, supóngase que el servidor web se corre en la máquina local, en el puerto 8080, para atender un máximo de 10 conexiones concurrentes:
$ bin/webserv 8080 10
En otra terminal se puede lanzar la prueba de estrés. El siguiente comando trata de establecer un total de 200 conexiones (--num-conns
) con el servidor anterior a una tasa de 50 conexiones cada segundo (--rate
). Es decir, en el primer segundo tratará de establecer las primeras 50 conexiones, en el siguiente segundo enviará las solicitudes de conexión 51 a 100, y así, hasta que en el cuarto segundo solicite las 200 conexiones.
Sin embargo, el servidor atenderá las primeras 10 solicitudes de conexión de forma concurrente y las restantes que vayan apareciendo, las aceptará pero las pondrá en cola. Ahí tendrán que esperar a que alguna de las conexiones anteriores haya sido atendida por completo para que un HttpConnectionHandler
se libere.
$ httperf --server localhost --port 8080 --uri /fact?number=123 --num-conns 200 --rate 50 --num-call 3 --timeout 1
Una vez que el servidor web acepte una de las 200 conexiones, httperf
enviará de inmediato 3 solicitudes HTTP (--num-call
) por esa conexión. Cada solicitud pedirá el recurso identificado con el URI http://localhost:8080/fact?number=123
. Si el servidor web no le responde una de estas solicitudes en 1 segundo (--timeout
), httperf
lo tomará como una no-respuesta y lo reportará en las estadísticas. En el repositorio de código de httperf se explica con detalle cómo interpretar su salida.
Para la versión serial (el código dado) del servidor web, se tendrán respuestas positivas sólo si se realiza una única solicitud HTTP por conexión, es decir --num-call 1
, ya que esta versión atiende una solicitud y cierra la conexión con el cliente, ignorando las demás solicitudes que éste realice. Para este avance, una vez que se haya implementado concurrencia en el servidor web, se debería tener respuesta de todas las solicitudes que el cliente haga en la misma conexión con el argumento --num-call
mayor a 1.
1.8. Diseño del siguiente avance
Para el segundo avance, la aplicación web debe ser concurrente con el fin de que aproveche de forma más balanceada los recursos de paralelismo del equipo en el que corre. Como se indica en el respectivo enunciado (proy1.2), el equipo debe crear un diseño que logre satisfacer esta necesidad. En la revisión de este primer avance, el equipo puede –y se recomienda– traer preparado su diseño del avance siguiente, para obtener realimentación antes de iniciar la programación del mismo.
1.9. Evaluación
La revisión de avance será mediante una sesión interactiva por videoconferencia. Los miembros del equipo primero realizan una presentación del producto y corren las pruebas de validez y de rendimiento. Luego se revisan algunas regiones de código elegidas por los docentes. Para esta etapa se debe mostrar en pantalla tanto el diseño como el código al mismo tiempo. Todos los miembros del equipo deben participar durante la sesión. Se dará crédito a la evidencia presentada en cada aspecto de este enunciado y se distribuye en los siguientes rubros.
-
[5%] Buen uso y aporte equilibrado del repositorio (commits, ignores, tags) y directorios.
-
[10%] Análisis:
README.md
, problema, integrantes, manual de usuario, captura, navegabilidad. -
[10%] Diseño concurrente: documento de diseño, flujo de datos, clases en UML, pseudocódigo.
-
[25%] Servidor web concurrente mediante el patrón productor consumidor.
-
[15%] Aplicación web orientada a objetos.
-
[15%] Finalización correcta del servidor. Buen uso de la memoria y de la concurrencia (Sanitizers y Valgrind).
-
[10%] Pruebas de concurrencia (
httpef
). -
[5%] Modularización en subrutinas y archivos (
.hpp
,.cpp
). Apego a una convención de estilos (cpplint
). -
[5%] Documentación de interfaces (
doxygen
) e implementaciones (algoritmos).
Recuerde que en todos los rubros se evalúan las buenas prácticas de programación.
2. Proy1.2: Aplicación concurrente
Entregar a más tardar el jueves 11 de noviembre a las 23:59. Las entregas tardías se tratarán como se estipula en la carta al estudiante.
2.1. Repositorio y archivos
Nota: Para optar por la mejora en la calificación del avance 01 tras la revisión, el equipo debe crear los issues y aplicar las correcciones siguiendo los mismos lineamientos para este aspecto indicados en las tareas individuales. Al final de las correcciones marcar el último commit con la etiqueta avance01fix
(o en inglés milestone01fix
).
Los cambios en el código para este segundo avance se deben hacer in place. Es decir, se realizan sobre la misma carpeta del avance 01 del repositorio de control de versiones. Si una corrección del avance 1 interfiere con un requerimiento del avance 2, se deben aplicar los cambios que ayuden a mejorar el producto priorizando el segundo avance. Es decir, se debe tener claro que los avances no son productos diferentes, sino, ir mejorando y madurando un producto cada vez más eficiente y correcto.
Los lineamientos sobre el trabajo en el repositorio y entrega de archivos estipulados en el avance 1, se aplican para este avance. Esto incluye igual participación de los miembros del equipo, políticas de commits y branches, estructura de archivos y carpetas, documentación con Doxygen, modularización, y estilo de código (cpplint
).
2.2. Análisis
En el avance 1 se construyó un servidor web concurrente que podía atender una cantidad arbitraria de clientes de forma simultánea, los cuales solicitan obtener la factorización prima de números enteros, que son calculadas por una aplicación web serial. Sin embargo, este diseño no es óptimo. Por ejemplo, si en una máquina con 8 núcleos de procesador se levanta un servidor web capaz de atender 1000 conexiones concurrentes, podrían ocurrir escenarios como los siguientes:
-
Si en efecto se conectan 1000 usuarios que solicitan una cantidad considerable de trabajo cada uno, el servidor tendrá 1000 hilos (ó "1000 calculadoras") compitiendo por los 8 núcleos, lo que crea una gran competencia y cambio de contexto por ellos, además de mayor consumo de memoria principal.
-
Si se conectan varios usuarios, uno de ellos hace una solicitud que requiere una cantidad considerable de trabajo, digamos una hora, y los demás hacen solicitudes livianas. Los hilos de ejecución que atienden las solicitudes livianas terminarán rápido, mientras que el hilo que se encargue de la solicitud pesada se quedará trabajando por tiempo prolongado en un núcleo de CPU, mientras las restantes siete CPUs estarán desaprovechadas.
En este avance se creará una solución que haga mejor uso de los recursos de la máquina, al hacer concurrente la aplicación web que calcula las factorizaciones primas. La aplicación deberá crear tantas calculadoras de factores primos como CPUs hay disponibles en el sistema. Si el equipo de desarrollo lo quiere, puede ofrecer un tercer argumento de línea de comandos que controle esta cantidad de calculadoras, pero no es obligatorio. La cantidad de factorizadores es un detalle se debe agregar al readme.ext
del proyecto.
2.3. Diseño
En esta segunda entrega, se debe modificar su aplicación web para que no haga un consumo agresivo o desequilibrado de los recursos de la máquina. Esta modificación consiste en hacer a la aplicación web concurrente, para que pueda controlar la cantidad de hilos que realmente consumen CPU en el cálculo de los factores primos, para que estos no superen la cantidad de núcleos de procesador disponibles en el sistema, y para equilibrar mejor la carga de trabajo entre los hilos de ejecución. El equipo de desarrollo debe actualizar su diseño para que cumpla los requerimientos anteriores como se detalla a continuación.
El diagrama de flujo de datos hecho en el avance anterior debe actualizarse para reflejar una optimizada cadena de producción completa que aplique el patrón productor-consumidor. El diseño de la Figura 1 es incompleto, pero servirá para ejemplificar algunos de los cambios que el equipo de desarrollo debe considerar.
El diseño del avance anterior usaba un "factorizador" por cada conexión cliente. En este avance, su diseño debe crear un equipo de tantos factorizadores como CPUs hay disponibles en el sistema. El rol de estos hilos debe estar claramente definido en el patrón concurrente visto en el curso (productores, consumidores, ensambladores, o repartidores).
El diseño debe contemplar cómo se comunican los hilos "factorizadores" con otros hilos de la cadena de producción. Antes de los factorizadores se encuentran los hilos HttpConnectionHandler
. Sin embargo la comunicación entre ellos no es trivial dado que los HttpConnectionHandler
forman parte del servidor web, los factorizadores son parte de la aplicación web, y el servidor web debe trabajar con una cantidad arbitraria de aplicaciones web. Por esto se sugiere la siguiente cadena de producción.
-
Al igual que el avance 1, el servidor sólo acepta solicitudes y las pone en cola, nada más.
-
Los
HttpConnectionHandler
consumen las conexiones. De una conexión, extraen todas las solicitudes HTTP del cliente (no sólo la primera). Sin embargo, en este avance 2, losHttpConnectionHandler
no atienden estas solicitudes HTTP, sino que las ponen en otra cola. -
Un nuevo hilo consume las solicitudes HTTP de la cola anterior. Por cada solicitud determina a cuál aplicación web va dirigida y la pone en la cola correspondiente. El equipo de desarrollo debe determinar el rol del patrón productor-consumidor que mejor se ajusta a esta responsabilidad. Además debe modificar el proceso de registro de las aplicaciones web ante el servidor web para que las aplicaciones puedan proveer sus criterios que decidan cuando una solicitud va dirigida a ellas.
-
Modificar las aplicaciones web para que sean parte de la cadena de producción. Es decir, las aplicaciones web (clase
HttpApp
) serían hilos que toman un rol del patrón productor-consumidor. Las aplicaciones consumen las solicitudes HTTP que son dirigidas hacia ellas. -
Cada vez que la aplicación web de factorización prima consuma una solicitud HTTP, extrae los números en el cuerpo de la solicitud, pero no encuentra la factorización prima de ellos, sino que coloca estos números en cola para enviarlos al equipo de hilos factorizadores. Esto crea una separación entre la vista concurrente (la aplicación web) y el modelo concurrente (los factorizadores). Aunque parece sencillo, el equipo de desarrollo debe diseñar con cuidado y detalle este paso, dado que implica decisiones de descomposición, mapeo, y trazabilidad que tienen un impacto en el rendimiento y deben reflejarse en el diseño de los nodos de varias colas de la cadena de producción. Más adelante se habla sobre este aspecto.
-
Los "factorizadores", que son clases modelo, deben convertirse en hilos de ejecución, y por lo tanto, tomar un rol del patrón productor-consumidor. Cada vez que un "factorizador" extrae trabajo de la cola, calcula la factorización prima y el resultado es puesto en otra cola.
-
Un hilo "empaquetador" recibe las factorizaciones y revisa si la solicitud a la que pertenece está completa. Si lo está la pone en cola.
-
Un hilo "despachador" recibe las solicitudes que están completas, ensambla el mensaje de respuesta HTTP y lo envía a su respectivo solicitante (navegador).
El diseño debe reflejar el recorrido de los datos de ejemplo escogidos en el avance 1 en los nodos de la cola a lo largo de la cadena de producción. Para cada cola de espera debe indicarse su nombre, el tipo de datos almacenado en ellas como una clase en UML (nombre, atributos, y métodos). La elección de los campos de cada nodo deben ser el resultado de una eficiente descomposición y mapeo, y una correcta trazabilidad.
El diseño debe balancear la carga de trabajo lo más equitativamente posible entre las CPUs disponibles en el sistema. Por tanto, deberán tomar decisiones de descomposición y mapeo que afectan a la naturaleza de las solicitudes. Es decir, las solicitudes serán descompuestas en unidades más finas de trabajo. Esto provoca que los datos que hayan en una cola tengan diferente granularidad que los datos de otras colas. Por lo tanto, no habrá una asociación 1 a 1 entre ellos, por lo que el equipo deberá decidir algún mecanismo para mantener la trazabilidad.
La trazabilidad consiste en agregar datos a los nodos de las colas que permitan ensamblar luego las respuestas que serán enviadas a los clientes. La cantidad de datos a agregar debe ser la mínima y el mecanismo escogido debe ser correcto y eficiente. Recuerde que se debe reflejar la trazabilidad a través de los ejemplos a lo largo de la cadena de producción.
Al igual que el avance 1, el diseño concurrente consta del flujo de datos, diagramas de clase UML, y pseudocódigo. El pseudocódigo sirve para detallar las acciones que se realizan en la creación, producción, y consumo a lo largo de la cadena de producción. El texto del documento de diseño debe actualizarse junto con los diagramas como el pseudocódigo anterior.
2.4. Implementación
Se debe aplicar el diseño de la aplicación web concurrente siguiendo los patrones sugeridos (productor-consumidor, modelo-vista-controlador, y singleton) al código solución en C++. La implementación de la solución debe apegarse al diseño elaborado por el equipo en la sección anterior. Se verificará que haya una correspondencia entre los elementos del diseño y los elementos de la implementación durante la revisión.
Su solución debe realizar los cálculos correctamente. Es decir, la aplicación web responde con una página que tiene las factorizaciones primas correctas para las entradas solicitadas y en el mismo orden. Al igual que el avance anterior, la aplicación permite ingresar listas de números separados por comas, positivos o negativos, tanto en el URI (barra de direcciones del navegador), como el formulario web. Además valida entradas. Números mal formados, fuera de rango, o entradas mal intencionadas son reportados con mensajes de error (en la página web resultado), en lugar de caerse o producir resultados con números incorrectos.
Al finalizar el servidor web, interactivamente con Ctrl+C o el comando kill
, éste debe reaccionar a la señal y hacer la limpieza debida de la aplicación concurrente. Esto implica al menos, detenerse de aceptar más conexiones de clientes, avisar a los hilos secundarios para que terminen su ejecución, tanto los que atienden conexiones como los "factorizadores", esperar que terminen el trabajo pendiente, y finalmente liberar recursos como estructuras de datos en memoria dinámica, archivos, u otros.
2.5. Pruebas
El código solución debe demostrar calidad en el uso de memoria y concurrencia a través de pruebas como Sanitizers y Valgrind. Para probar que la solución realiza un adecuado proceso de descomposición y mapeo, se pueden hacer varias consultas. Por ejemplo, una consulta con varios números que requieren mucho procesamiento debería consumir todos los CPUs disponibles y responderse en un tiempo menor que si fuese atendida de forma serial como ocurría en el avance 1. Dos consultas simultáneas, primero una con números pesados más que la cantidad de CPUs disponibles y segundo otra con números rápidos de procesar, provocaría que la segunda no se atienda de inmediato.
Dado que se tiene un mejor diseño concurrente, se puede esperar una mayor tasa de respuesta ante una prueba de estrés. El equipo puede confirmarlo al incrementar los valores en los parámetros de httperf
como la cantidad de solicitudes por conexión (--num-call
) o la tasa de solicitudes por segundo (--rate
). El equipo debe hacer estos incrementos en los parámetros hasta obtener solicitudes no respondidas tanto en el código del avance 1 como el avance 2. Para volver al avance 1 se puede usar la etiqueta, por ejemplo, git checkout avance01fix
y realizar las mediciones. Para volver al último commit se puede con git checkout master
. Los resultados (argumentos de httperf
) como sus salidas, agregarlas a una sección de "Pruebas de concurrencia" al documento de análisis del proyecto.
2.6. Diseño del siguiente avance
Para el tercer avance del proyecto 1, la aplicación web debe escalar la concurrencia y calcular las factorizaciones de forma distribuida, tratando de descomponer y mapear el trabajo lo más equitativamente posible entre procesos ubicados en máquinas conectadas por una red de computadoras. El equipo puede –y se recomienda– traer preparado su diseño del avance siguiente, para obtener realimentación antes de iniciar la programación del mismo.
2.7. Evaluación
La revisión de avance será mediante una sesión interactiva por videoconferencia. Los miembros del equipo primero realizan una presentación del producto y corren las pruebas de validez y de rendimiento. Luego se revisan algunas regiones de código elegidas por los docentes. Para esta etapa se debe mostrar en pantalla tanto el diseño como el código al mismo tiempo. Todos los miembros del equipo deben participar durante la sesión. Se dará crédito a la evidencia presentada en cada aspecto de este enunciado y se distribuye en los siguientes rubros.
-
[5%] Repositorio: Buen uso y aporte equilibrado del repositorio (commits, ignores, tags) y directorios.
-
[20%] Análisis: actualizar readme. Diseño concurrente: documento de diseño, flujo de datos, clases en UML, pseudocódigo.
-
[25%] Servidor: Implementación de la cadena de producción. Agrega las aplicaciones como hilos a la cadena de producción.
-
[25%] Aplicaciones: Implementa tantos hilos de paralelismo de datos como CPUs hay disponibles en el sistema. Descompone en unidades finas. Logra trazabilidad de los mensajes de las aplicaciones.
-
[10%] Finalización correcta del servidor y de las aplicaciones. Buen uso de la memoria y de la concurrencia (Sanitizers/Valgrind).
-
[5%] Pruebas de concurrencia (httperf) con ASan y TSan.
-
[5%] Estilo de código: modularización en subrutinas y archivos (.hpp, .cpp), apego a una convención (cpplint).
-
[5%] Documentación: de interfaces (doxygen) e implementaciones (algoritmos).
Recuerde que en todos los rubros se evalúan las buenas prácticas de programación.
3. Proy1.3: Aplicación distribuida
Entregar a más tardar el jueves 25 de noviembre a las 23:59. Al igual que los avances previos, se aplican las mismas políticas para optar por las mejoras en el avance 2, de manejo del repositorio de control de versiones, participación de los miembros del equipo, documentación, estilo de código, etc.
En el avance 1 se construyó un servidor web concurrente que podía atender una cantidad arbitraria de clientes de forma simultánea, los cuales solicitaban obtener factorizaciones calculadas por una aplicación web serial, lo cual no es óptimo.
En el avance 2 se re-diseñó la solución para hacer un uso más balanceado de las CPUs paralelizando la aplicación web. De esta forma los HttpConnectionHandlers
eran hilos livianos de concurrencia de tareas que esperaban solicitudes pero no las atendían, sino que las trasladaban a la aplicación web, la cual contaba con hilos de paralelismo de datos (calculadoras) que se encargaban de encontrar las factorizaciones. Además se usó una unidad de descomposición fina para mejorar el balance de carga entre los hilos de paralelismo de datos. Esta solución logra hacer un uso eficiente de los recursos de una máquina, pero desaprovecha otras que podrían estar disponibles en una organización o un clúster.
En el avance 3 el objetivo es distribuir la aplicación web para que sea escalable, logre aprovechar varias máquinas que se tengan disponibles en una organización, y reduzca sustancialmente los tiempos de respuesta. De esta forma, si el servidor web recibe una carga sustancial de trabajo, puede responder en menor tiempo y disminuir la probabilidad de que los clientes se impacienten y se desconecten dejando trabajo residual en el servidor.
En este avance se deben distribuir los modelos de la aplicación web, es decir, los "factorizadores". Una parte de la aplicación web corre junto con el servidor web en el mismo proceso, mientras que los "factorizadores" corren en procesos separados, sean en la misma o distintas máquinas. Es como cortar la cadena de producción como se ve en la línea punteada roja del siguiente diagrama.
Los procesos factorizadores, al lado derecho de la división en la Figura 3 rotulados con ProcessN
, deben iniciarse primero. Para poderse comunicar con ellos, deben correr como servidores TCP (véase la clase TcpServer
). Si el equipo lo prefiere, en lugar de servidores TCP, pueden ser servidores web (o más apropiadamente "servicios web"), pero debe tenerse cuidado de no hacer un diseño muy complejo.
El proceso con el servidor web, al lado izquierdo de la división en la Figura 3 rotulado con Process0
, lee de un archivo de configuración la ubicación de los factorizadores. Estas ubicaciones son indicadas como direcciones IP y puertos. El nombre del archivo podría ser provisto como argumento de línea de comandos. Si el nombre del archivo se omite, la aplicación debe crear los factorizadores en la misma máquina, de la misma forma que se hizo en el avance 2.
Cada vez que el servidor web recibe las solicitudes HTTP, las descompone, y distribuye el trabajo entre los factorizadores mediante un mapeo eficiente que balancee la carga. El equipo debe idear este mapeo, y existen esquemas sencillos de implementar.
Una vez que los factorizadores hayan encontrado la factorización prima de los números que reciben, ponen en cola estos resultados para que sean enviados de regreso al servidor web. Aquí ocurre el mecanismo inverso: el proceso con el servidor web también debe poder recibir datos, es decir, tener un hilo servidor TCP (o un servicio web) a la espera de números factorizados. En este escenario, los factorizadores tienen hilos que son clientes TCP (o su correspondientes clientes web si se usan servicios web).
El equipo debe mantener la trazabilidad de las solicitudes a lo largo de la cadena de producción distribuida, con el fin de que el proceso con el servidor web pueda reconstruir los mensajes de respuesta HTTP. Esto se debe reflejar en los diagramas de diseño en los nodos de las colas y en las clases UML.
Al tener distribución, se debería poder dar respuesta a una carga de trabajo mayor. El equipo debe confirmar este indicio al incrementar los valores de estrés de httperf
hasta el punto en que la solución no logre responder la totalidad de las solicitudes. Estos resultados se deben divulgar en el documento de análisis. Para realizar las pruebas se debe usar una red de computadoras, como el clúster de Arenal.
Se dará crédito por los siguientes aspectos a evaluar.
-
[5%] Buen uso y aporte equilibrado del repositorio (commits, ignores, tags) y directorios.
-
[15%] Actualizar el diseño para incluir distribución de la carga de trabajo.
-
[30%] Implementar el paso de mensajes hacia y desde los factorizadores con las clases de la capa de red (
TcpServer
,TcpClient
,HttpServer
). -
[20%] Balancear la carga entre las calculadoras (descomposición y mapeo) y mantener la trazabilidad de las solicitudes.
-
[15%] Detener los factorizadores cuando se detiene el servidor web (con Ctrl+C o el comando
kill
). Buen uso de la memoria y de la concurrencia (Sanitizers y Valgrind). -
[5%] Pruebas de concurrencia (
httpef
) en documento de análisis (readme.ext
). -
[5%] Modularización en subrutinas y archivos (
.hpp
,.cpp
). Apego a una convención de estilos (cpplint
). -
[5%] Documentación de interfaces (
doxygen
) e implementaciones (algoritmos).