sábado, 28 de marzo de 2015

Error al programar mi diccionario que impide concurrencia

Ahora que estoy repasando mentalmente todas las estructuras de datos ya implementadas para poderlas separar en módulos funcionales rígidos y cada módulo tener un servidor, clientes y objetos remotos... he caído en un error en la programación de los diccionarios.

Cuando programé los hilos para que se ejecutase la inserción de las er concurrentemente resultaba que tardaba incluso más que sin hilos. Un hilo bloqueaba a los demás y no se llegaba a generar el paralelismo.

Comenté que se debería a la dependencia entre er y a dependencia entre las estructuras de datos. Pues ahora he dado con otro factor más: el pivote de los diccionarios.

Mis diccionarios son árboles en los que en todas los nodos se insertan listas como en el caso del diccionario de listas, o sólo en los nodos hoja se insertan listas (equivalentes) en el diccionario de er, o sólo en los nodo hoja la er en el diccionario de nombres.

Tienen en común que se recorre el árbol nodo a nodo mediante el puntero pivote. Un único pivote. Si un hilo quiere saber si un string está almacenado tiene que:

- pivote = nodo raíz del árbol.
- para todos los caracteres de la cadena, de izquierda a derecha
    - si algún hijo del nodo que apunta el pivote es el caracter:
        - pivote = nodo hijo que lo contiene.
    - si no, no está almacenada en el árbol.
- si no quedan caracteres por evaluar:
    - sí está almacenado.
- si no, no lo está.

El problema es que no se pueden ejecutar dos consultas simultáneamente ya que el pivote volvería a la raíz sin haber terminado la consulta de otro hilo. La solución que tomé fue la de sincronizar todos los métodos que tocaban el pivote. El resultado es que no existe concurrencia real. Lo mismo ocurre con las otras funciones de modificación, inserción o eliminación ya que todas usan el pivote.

Solución: cada hilo cree su propio pivote privado.

Explotación teórica de la distribución funcional de la aplicación

He pasado unos días leyendo y poniendo en práctica código RMI de java y de aspectj.

Con respecto a aspectj tuve que actualizar mi NetBeans 7.3 al 8.2 porque el único plugging que encontré no era compatible. Aún sigo luchando ya que el código fuente de varios ejemplos, copiados tal cual, no los compila. Aunque el concepto me está gustando.

Y con RMI pues algo parecido. Una vez programé un ejemplo de forma tosca (editor de texto y consola para compilar,  sin NetBeans), y me funcionó a la perfección. Sin embargo, otro día fui a ejecutarlo y ya no funcionaba. No comprendía el motivo si el código era el mismo, asi que me puse a burcar información sobre los posibles errores típicos al desarrollar sobre rmi.
  • Se ha de ejecutar la aplicación pasándole un parámetro de seguridad:
          java -jar -Djava.security.policy=~/.java.policy gr5.jar

Este fichero específica los permisos de acceso al servidor: aplicaciones, usuarios, puertos ... Pero por ahora simplemente le daremos todos los permisos.

Unas páginas web decían que este fichero debería de ir en la aplicación, otros en el directorio raíz de la aplicación. Opté por la segunda opción pero seguía sin funcionar. Por lo menos ya no me denegaba el acceso a los recursos.
  • Olvidar generar los ficheros por RMIC:
Otro error típico era olvidar ejecutar rmic a todos los archivos .class que entendiesen en su código de java.rmi.remote. Es decir, aquellos que se compartirían entre los servidores y clientes. No encontré manera para hacerlo automáticamente con NetBeans con lo que hay que hacerlo manualmente archivo a archivo. Tampoco funcionaba.

  • Se debe estar ejecutando el servidor RMISERVICE:
Comprobaba que el servidor de nombres rmiservice estuviera en funcionamiento. Nada.

  • RMISERVICE debe ejecutarse en el mismo directorio que se depositan los archivos generados por RMIC.
Ese era el problema.

Estas complicaciones me dieron por pensar que cara a un usuario inexperto simplemente ejecutar la aplicación ya supondría ser un lío.

Normalmente las aplicaciones que comparten información entre sí lo hacen siguiendo el protocolo cliente-servidor. Un único servidor e infinitos clientes. Yo no quiero eso. Quiero que todos puedan ser servidores y clientes a la vez.

Para poder coordinarlos y saber qué servicios presta cada aplicación remota y qué clientes esperan objetos remotos, hace falta un servicio coordinador. Pues incluso este servicio no puede ser asignado explícitamente a un pc (más adelante explico cómo se pasan el testigo).

Mentalmente tengo la estructura de como implementarlo.


La nueva versión:

Dos jar. Uno es el proyecto gr de siempre aunque reprogramado en módulos independientes de servicios y clientes. Y el otro es su lanzador y configurador.


Lanzador:

  1. Se encargará de abrir y cerrar los puertos que precisen los jar.
  2. De definir el perfil de ejecución (cliente, participante pasivo, participante activo y desarrollador).
  3. De lanzar en el directorio correcto rmiservice y pararlo al terminar.
  4. De escribir el fichero .java. policy en el directorio raíz.
  5. De lanzar gr con -Djava.security. policy
  6. De centro unificado de configuración como puede ser de la base de datos (usuario, contraseña, ip, puerto) y rutas.
  7. De actualizar los jar y archivos de rmic (según el perfil)
  8. De comunicarse con gr.
Como trabaja con puertos deberá ejecutarse como root.


La aplicación Gr:

Como dije, en cada pc habrá el mismo código. La misma aplicación, solo que, en unas máquinas se ejecutan unos servicios ofreciendo recursos y otras máquinas ejecutan otros servicios. Se comunicarán entre sí mediante rmiservice.

Para hacerlo dinámico, cuando se conecte otro pc se deberán redistribuir los servicios repartiendo el trabajo entre todos los computadores conectados al sistema, deberá haber una sincronización entre ellos, existencia de la función salida programada para reubicar los servicios entre los restantes, etc., debe de haber un servicio coordinador que controle todo esto entre todos los participantes.

Servicios:

Entre muchos más que irán apareciendo podría mencionar:
- Coordinador.
- Actualizador del software.
- Diccionarios de listas.
- Grafo de dependencias.
- Procesado de Er.
- Diccionario de Er.
- Diccionario de Nombres.
- Autómata.
- Almacén procesado er.
- Generador de código java.
- Gestor de funciones autoprogramadas.
- Etc., junto con sus réplicas.

Peefiles:

- Desarrollador: ejecuta directamente archivos class desde el directorio del proyecto de NetBeans. Sólo puede haber uno. Genera los archivos de rmic. Puede ejecutar servicios como uno más.

- Participante activo: ejecuta la aplicación ya empaquetada en jar. Puede ejecutar servicios y para ello necesita de los archivos generados por rmic.

- Participante pasivo: también ejecuta jar pero siempre como cliente de los servicios.

Estos tres necesitan ser root al ejecutar el lanzador para poder abrir y cerrar los puertos.

- Cliente: este operaría a través de un servicio web que se conectaría al sistema.   Sólo puede hacer consultas y no podrá insertar datos.

Comunicaciones:

Entre el lanzador y el Gr la comunicación es local. No puedo decir que uno sea servidor del otro o cliente. Los dos esperarán peticiones del otro. Por ello lo harán a través de dos sockets. En uno escucha el lanzador y en otro el Gr. Serán únicos. Un lanzador y un gr por máquina conectada.

El servicio coordinador se ejecuta en el Gr. RMI ofrece tanta transparencia que no he llegado a comprender cómo podría un servidor comunicarse con un cliente concreto, con lo que recurro de nuevo a dos sockets. Los demás servicios que se ejecutan sí se comunican con rmiservice.

Los puertos que utilizan para comunicarse entre el lanzador-gr y el coordinador-todos los pcs son dinámicos. Me refiero a que cada lanzador administra estos puertos en función de que si los tiene libres ese computador en concreto y busca otros si no fuese así.

Sin embargo debe de existir un puerto fijo en los que todos los participantes del proyecto estarán a la escucha. Lo denomino puerto oyente. Su única función será la de facilitar la ip y puerto del pc que esté ejecutando en ese momento el servicio coordinador a cualquiera que se conecte. Inmediatamente se desconectarán.

Al querer conectar un pc no podremos saber quién es el coordinador en un momento contreto ya que este servicio no tiene un pc fijo.

Si el que se conecta es nuevo en el sistema distribuido, sólo podrá unirse al grupo si cualquiera de los demás pcs solicitan al coordinador que incluya al pc nuevo con ip x. X estará escuchando por su puerto oyente. Al ingresar recibirá del coordinador una lista de sus compañeros.

Si se trata de un pc que se fue y ahora regresa, cogerá su lista de compañeros y probará si hay alguno conectado y a través del puerto oyente le preguntará que quién es el coordinador actual y le solicitará ingreso.

Funcionamiento:

La idea fundamental es que el sistema (me refiero a todos los pcs ejecutando sus aplicaciones) estén siempre en marcha. Si un pc se conecta (y es participante activo) adquirirá servicios que ejecutará y liberará a otro pc que hasta ese momento lo estaba haciendo. Cuanto más pcs estén conectados más distirbuión habrá con lo que si uno cae la recuperación del sistema es más rápida.

Como comenté hay dos tipos de salidas de un pc del sistema: salida programada y la salida inesperada.

- Salida programada: un participante activo decide que deja el sistema y lo comunica al coordinador. El coordinador pasará todos sus servivios a los demás miembros (junto con sus datos) y una vez terminado comunicará a todos que esos servicios los han asumido otros, para que a partir de ese momento cambien sus configurarciones (ips y puertos). Es ahora cuando se puede marchar el solicitante de la salida.

- Salida inesperada: un participante activo desaparece. El coordinador detecta que se ha perdido la conexión y tiene que conseguir que los datos que poseía se recuperen. El coordinador sabe los servicios que estaba ejecutando, los lanza entre los demás miembros del sistema. El problema son los datos. Al no haber sido una salida programada no se han podido pasar los datos procesados al pc que ha asumido el servicio. Si el servicio que murió contaba con servicio réplíca en otro pc, el coordinador les comunicará a todos que el servicio réplica se ha convertido en el servicio principal y otro tomará el rol de réplica. El nuevo servidor pricipal sí posee los datos y los mandará al nuevo réplica. Sin embargo, si no se cuenta con réplica irremediablemente los datos se han perdido. Dependiendo del servicio se podrá recuperar o no usando el resto de los servicios que lo generaron, por ejemplo, los datos del servidor de diccionarios de listas se generaron a través del servidor autómata, éste último volverá a generarlos y mandarlos. En el peor de los casos el coordinador tendrá que mandar el alto a todos los miembros y reiniciarse desde cero TODO EL SISTEMA.

La primera vez que se ejecute sólo habrá un pc trabajando y será el mío, el desarrollador. Al ser sólo uno y no tener nadie en su lista de compañeros asumirá el rol de coordinador automáticamente. Todos sus servicios tabajarán de forma local y será cliente de sí mismo.

Ahora se van conectando participantes, siempre que el coordinador los ingrese. Se irán actualizando las listas de compañeros a todos según los ingresos. Todos deberán de saber quien es el siguiente candidato a coordinador, lo explico: si yo retiro el pc, se quedan los demás conectados entre ellos pero sin coordinador. Si yo hago una salida programada, los servicios que estuvieran ejecutándose en mi pc los asumirian los demás y pasaría el testigo al siguiente PARTICIPANTE ACTIVO y ahora sería el nuevo coordinador. Si la salida es inesperada todos se conectarán al siguiente participante activo de su lista de compañeros y recuperarán la estabilidad del sistema como se comentó anteriormente.

Como sólo hay un desarrollador y soy yo, cuando compile deberán  mandarse al pc coordinador los jar y ficheros rmic para que éste los distribuya como actualización a todos los miembros del sistema.

El que me vuelva a conectar como desarrollador no implica que recupere el rol de coordinador. Como se ha visto, este servicio se va moviendo entre participantes activos del sistema. Y tienen que ser entre participantes activos porque son los que además de estar actualizados, poseen los archivos generados por rmic y al asumir la coordinación, deberá distribuirlos a los nuevos miembros hasta que el desarrollador genere nuevos archivos.

lunes, 23 de marzo de 2015

Programación Distribuida: Aspectj & RMI

He seguido dándole vueltas al asunto de mejorar el rendimiento de la aplicación.

Una solución más inmediata sería hacer que en lugar de cargar las ER según un orden aleatorio, sería ideal hacerlo según el orden de dependencia. Puesto que en este momento, antes de cargar las ER en el autómanta, realiza un grafo de dependencias, simplemente sería usar esta información para ordenar la carga. De esta manera se minimizan las esperas de los hilos de cada ER a cargar.

Otra solución a parte de esta es otra muy radical: programación distribúida (RMI) y programación orientada a aspectos (aspectj).

No voy a explicar en profundidad en qué consiste, entre otras cosas porque aún estoy asimilando conceptos.

Con RMI puedo descomponer la aplicación en servicios y clientes que se intercambian objetos remotos. Cada servicio y cliente se puede ejecutar en un ordenador distinto.

Con Aspecj puedo modular más aún el código teniendo en cuenta los aspectos, por ejemplo, abtrayendo la distribución o la sincronización.

Esta nueva versión del código será una combinación de ambas tecnologías.

En este momento sólo tenemos una gran estructura de datos y dentro de ésta está el diccionario de cadenas, el autómanta, el diccionario de nombres del autómata, el diccionario de ER, el grafo de dependencias... Todas las subestructuras están conectadas a las demás. Creo que precisamente por esa dependencia dificulta el paralelismo de los hilos. Ahora la idea es crear un servidor para cada estructura mencionada y especializarla esclusivamente para ello. Además de estos sercicios habrá otro más, el servicio coordinador. No voy a programar los servicios/clientes como aplicaciones por separados. Todo va  a formar parte de la misma aplicación y ésta podrá unas veces trabajar como servidor del grafo de dependencias y en otras como cliente del diccionario de nombres del autómatas por ejemplo, para ello debe de haber un servicio que coordine todos los servicios y clientes en todos los ordenadores que estén conectados y compartiendo objetos a través de la red.

El caso más simple es teneniendo sólo un computador trabajando: lanzamos la aplicación con la IP y el puerto del servidor de la base de datos (que puede estar en el mismo pc o en otro). Al ser sólo un pc, él mismo es servidor y cliente de todos los servicios. Es decir, intercambia los objetos a nivel local entre los distintos módulos.

Ahora empezamos a conectar más pc y corremos la aplicación en cada uno de ellos: el pc anterior toma el control conviertiéndose en el servidor coordinador y su misión será la de repartir el trabajo entre los ordenadores conectados. En el mejor de los casos hay un pc por cada servicio. Al conectar/desconectar un pc, el pc coordiandor redistribuye la carga.

La idea es que no estén fijados los roles de los pc, sino que puedan cambiar. E incluso si el servidor coordinador (el primero el lanzarse) cae, asuma su papel otro pc.

Ahora los obejtos se intercambiarían remótamente.

Para aumentar de pcs la tarea es sencilla, pero al disminuir, se complica.

Si la salida de un pc es programada, es decir, el pc que abandona le comunica al servidor coordinador su intención, éste copiaría toda la estructura de datos del servicio/servicios que estuviera ofreciendo y lo asignaría a otro/otros pcs y a partir de este momento asumen esta nueva funcionalidad y los clientes ahora se dirigirán a ellos.

Si la salida no es programa, la cosa cambia. Al ser una aplicación que todos los datos procesados están en memoria (en uno o varios pcs de forma distribuida), si cae un servidor todos los datos de éste se pierden.  Sin embargo, se puede remediar con servicios de réplica. Si cae un servicio automáticamente a sus clientes se les asigna la Ip del servidor copia y se le asignará a otro pc esa función y se le pasará toda la estructura para que ahora asuma el rol de réplica nuevo.

En el caso de no contar con réplicas no habrá otro modo que el de reiniciar todos los servicios vaciando sus datos y procesarlos desde el principio con los Pcs que hubiese tras la salida.

martes, 17 de marzo de 2015

Reconocimiento de funciones

Si algún lector es informático sabrá que no todas las ER son programables. Cuando programé la ER 'empieza por a' = a+PALABRA, siendo PALABRA cualquier secuencia de símbolos del abecedario, funcionó correctamente. Sin embargo al programar 'termina por a' = PALABRA+a, no funcionó. Sí que generaba la estructura en el autómata y al reconocer 'arbol' me decia ER='empieza por a'+PALABRA, pero al reconocer 'casa' ER = PALABRA. Es debido a la recursividad de la gramática de la ER en el autómata.

Como lo inserta sin problemas no encontré forma de que de forma automática reconociese que la ER insertada no iba a reconocer las cadenas esperadas.

Si tubiera la función 'termina por (terminación) = boolean' podría seguir reconociendo cadenas sin problemas.

Esto me llevó a concluir de que necesitaba de funciones programables por la misma aplicación, en base a unas directrices generales.

En un principio pensé en la simulación de ejecución de un código con las instrucciones y tipos de datos más básicos. De esta manera sería suficiente ya que se podría autoprogramar en tiempo real y no se tendría que compilar.

En seguida me dí cuenta de que lenguajes de programación ya habían muchos y además contaban con sus correspondientes compiladores y depuradores.

¿Pero cómo puede la aplicación ejecutar código que ella misma ha generado?

Y se añade la complejidad de que el código generado usa palabras que han sido definidas en el autómata.

Nos basaremos en dos capas de código. La superior será la que se trabaje directamente por la aplicación. Es la que definirá la función, sus argumentos y el resultado. En este código serán donde aparezcan las referencias a palabras del autómata o a otras funciones ya programadas. Este código es re utilizable y se podrá guardar.

La otra capa será una trasformación de la capa anterior, asignándoles valores concretos a los argumentos, si existe la expresión regular (palabra) en el autómata se sustituirá por true y en caso contrario por false, se le dará forma de main, se compilará, se ejecutará y se devolverá el resultado como resultado de la función ejecutada.

Por ejemplo:

Suponiendo que existe la función TERMINA (PALABRA, PALABRA) = BOOLEAN

donde PALABRA es una expresión regular ya definida en el autómata, para una nueva función

función GÉNERO_MASCULINO (PALABRA) = boolean

    return TERMINA (PALABRA, o));

fin función

Y queremos ejecutar: 

GÉNERO_MASCULINO (pájaro)

Primero comprobaría los argumentos:

Pájaro es PALABRA según el autómata.

Empezaría a generar código:

Public boolean GÉNERO_MASCULINO (String SPALABRA){

    return true;
}

Public Static void main MAIN_GÉNERO_MASCULINO (String args []){

    System.out.print (GÉNERO_MASCULINO (args [0]);

}

Trabajando recursivamente, al llegar a la función TERMINA, habrá generado su código y ejecutado su main con pájaro y o como argumentos. Se captura la salida del main o el error. En el caso de que exista salida y sea true se sustituirá como en el ejemplo. En el caso contrario o error, por false.

La salida de MAIN_GÉNERO_MASCULINO (pájaro) será true

Paralelismo en la generación del autómata

En la anterior entrada comenté que sería interesante que al insertarse las expresiones regulares en el autómata lo hiciese usando hilos para intentar mejorar la velocidad de carga.

Lo he llevado a cabo pero no he obtenido los resultados que esperaba.

Antes iba cargando todas las expresiones regulares de forma secuencial. Los problemas que surgen de esta implementación son principalmente dos:

  1. Sólo trabaja con una ER cada vez.
  2. La dependencia de unas ER a otras hace que el orden de la secuencia de carga de las ERs no pueda alterarse. Por ejemplo, no se puede definir un Natural si antes no se ha definido un Dígito.
Tras varias implementaciones y búsqueda de mucha información de como trabajar con hilos, conseguí programar una versión de la aplicación que lanzaba un hilo por cada ER al cargar en el autómata y tantos hilos como signos '+' de la ER tuviera más uno, es decir, un hilo por cada subexpresión regular. Cuando una ER encuentra una dependencia que aún no ha sido cargada, bloquea el hilo hasta que se resuelva por sí solo.

El problema de la dependencia queda definitivamente erradicado. Ahora no importa el orden al insertar las ERs.

Sin embargo no se ha ganado tiempo en la carga de todas las ERs, incluso tarda un poco más. Parece ser que apenas trabaja en paralelo y se pasan la mayoría del tiempo bloqueados los hilos esperando que se resuelvan las dependencias y lo que se pueda ganar en determinadas ocasiones que pueda trabajar paralelamente, lo pierde en las comprobaciones de disponibilidad.

A pesar de todo lo considero un gran avance ya que ahora podemos despreocuparnos de las dependencias entre unas ERs y otras.

Puede ser que las secciones críticas de los hilos no estén bien programadas, no soy un experto en el tema. Así que si alguien se anima para revisarlo, es bien recibido.