jueves, 24 de septiembre de 2015

Diccionario Genérico Distribuido por RMI


En entradas anteriores propuse rehacer la aplicación abstrayendo las estructuras generales para darles forma de servicios, actuando unas veces de servidor y otras de cliente.

Atendiendo simplemente a la estructura del autómata recordamos que estaba compuesto de un diccionario de nombres de las expresiones regulares, otro diccionario de las propias expresiones regulares junto con sus tablas de transiciones y estados finales, un grafo, etc. Del mismo modo el resto de la aplicación usaba distintas subestructuras de datos que en la versión actual se encuentran entrelazadas impidiendo muchas veces trabajar concurrentemente.

Para empezar a desarrollar la idea comencé con algo sencillo, genereralicé el diccionario de modo que ya no está restringido a Strings o a ER como antes. Además añadí la funcionalidad de modificación y borrado. En la versión anterior sólo se podían insertar elementos.


¿Porqué distribuir el diccionario?

En un primer momento la idea de distribución de la aplicación era simplemente asignar a varios pcs un servicio determinado, pero ¿cómo gestionar los servicios réplicas?, ¿recuperación de información?...

El punto de vista que ahora estoy llevando a cabo va más allá. El código de los diccionarios es el mismo en todos los pcs. En cada uno de ellos se ejecuta un gemelo, una versión del diccionario distribuido con su espacio de trabajo particular, es decir, su IP concreta de la máquina y su puerto. Los gemelos se comunican entre sí mediante notificaciones para estar todos actualizados en cuanto a cuando se ha añadido un nuevo gemelo, se va a eliminar o ha muerto, etc.

Cada gemelo gestiona una serie de servicios y réplicas, tanto en su pc como remótamente de otros gemelos.

Parece un poco lío, pero con un ejemplo es más fácil de ver:

El caso más sencillo es trabajar con un único pc. Este ejecuta un sólo gemelo del diccionario con IP ip1 puerto p1.

Hacemos que el gemelo lance un servicio diccionario. En él se irán insertando todas las cadenas y objetos remotos como si de un diccionario normal y corriente se tratase y trabajaríamos con él sin mayor complicación. Todo se ejecuta en la misma máquina.

Ahora podemos añadir una réplica en la misma máquina para poder recuperar datos en caso de que hubieran problemas con el servicio diccionario. Esto es, el gemelo lanza una réplica con la misma IP.

Podemos lanzar tantos servicios y réplicas en la misma máquina como queramos, aunque no tiene mucho sentido. Lo interesante es que los servicios y las réplicas se balancean automáticamente para intentar tener el mismo número de objetos almacenados en cada uno de ellos.

Como decía, en una misma máquina no tiene mucho sentido, pero ¿y si utilizamos más máquinas? Podemos ejecutar un gemelo en la máquina con IP ip1 y puerto p1 y que lance un servicio diccionario, y podemos ejecutar otro gemelo en la máquina con IP ip2 y puerto p2 y que lance una réplica. Esto significa que si tanto el gemelo 1 como el 2, por cualquier motivo cae, el otro es capaz de recuperarse de forma automática y transparente. Si conectamos más pcs y en estos se ejecuta su gemelo y éstos lanzan más servicios diccionarios y réplicas, se irán balanceando repartiéndose la carga facilitando su recuperación en caso de cualquier tipo de error.

Un dato importante a saber es que no hay un gemelo master ni nada por el estilo. Todos tienen la misma categoría, todos los servicios actúan como servidor y cliente a su vez, dependiendo de lo que pretendan hacer. Si un gemelo cae, los demás gemelos son capaces de recuperarse y seguir como si nada.

Se ha dotado de métodos sincronizados para la concurrencia en aquellos que se ejecutan sin mucha complejidad y son rápidos. Sin embargo, los métodos más pesados como pueden ser el de redimensionar, el de comprobar la integridad, recuperar, entre otros, antes de lanzar el método un gemelo determinado, éste lanza una notificación al resto de los gemelos para que se queden bloqueados a nivel de usuario (más adelante explico), ejecuta el método sabiendo que no será molestado con intrusiones de métodos lanzados por otros y vuelve a lanzar la notificación para que se desbloqueen y puedan continuar.

Aunque el propósito es que sea un único sistema que trabaje con varias estructuras de este tipo de modo que todo esté organizado por éste, con la sincronización y bloqueos podemos manipular el diccionario distribuido a través de todos los gemelos a la vez. El sistema cuando tenga que trabajar con el diccionario no le importará con qué gemelo hacerlo, ya que para éste, los resultados son los mismos.

Para lograr esto el código se ha fraccionado en cuatro niveles:

Nivel 0: Se trata de la implementación de las celdas, manipulador y diccionario genérico. El manipulador es para facilitar el uso de un pivote individual por hilo y hacer posible la concurrencia.

Nivel 1:
Se implementan el interfaz, remoto, cliente y servidor de un servicio diccionario.

Nivel 2:
Se implementan servicios y controlServicios, que gestionan los servicios y réplicas que se ejecutan en su máquina.

Nivel 3: Gestionan los servicios y réplicas de una misma máquina con las demás.

Nivel 4: Nivel usuario, tiene implementadas las interfaces, remotos, clientes y servidores para que puedan lanzarse notifiaciones entre los gemelos y ejecutar los métodos indicados por el usuario y de forma transparente se ejecuten en la misma máquina o remótamente según donde se encuentren los datos determinados. Cuando se lanza el bloqueo es este nivel el que se queda inactivo pudiendo trabajar los otros tres niveles independientemente.

Para que esto funcione, todos los gemelos deben de saber de la existencia del resto de gemelos y de todos los servicios y réplicas que han sido lanzados y por quién. Esto está organizado en dos listas que se van sincronizando todos, de este modo, se garantiza la autonomía.


¿Está terminada la implementación?

En el punto actual funciona perfectamente en una sola máquina lanzando todos los servicios y réplicas que queramos. Ahora estoy modificando algunos puntos para hacerlo funcionar con más máquinas. En un principio daba problemas para que pudiera registrar servicios de forma remota. RMIRegistry sólo registra objetos en su misma máquina. Comentar que cada máquina trabaja con su rmiregistry local. Esto hoy mismo ha quedado solucionado. Estoy con la sincronización de las listas de gemelos y servicios-réplicas.

Lo siguiente es la autorecuperación. Ahora mismo se recupera pero de forma manual, es decir, mato yo mismo un servicio y lanzo el método de recuperación y lo hace perfectamente. Lo que falta es que detecte que ha caído un servicio, réplica o gemelo y lo recupere todo sin intervención del usuario/coordinador.

viernes, 12 de junio de 2015

Reconocimiento de tokens de una er ya insertada en la estructura

Hasta ahora el automática nos decía si una cadena cumplía alguna o varias de las expresiones regulares que con anterioridad habían sido procesadas. Lo que no nos decía era cuales habían sido los tokens que había reconocido hasta llegar a los estados finales.

Si por ejemplo:

- palabra: sucesión de símbolos del alfabeto.

- parámetro: puede ser a su vez:
    - seguido de letra
    Número natural

- espacio: un espacio en blanco

Y la expresión comando = ((palabra)(espacio)*)+((palabra)(espacio)((parametro)(parametro)*(espacio)*)

Si le pasamos la cadena 'algo' devolverá 'palabra' y 'comando'. Si se le pasa 'algo -p 1234 -k' nos devolverá 'comando'

Pero ¿y si queremos averiguar sus tokens? En función de sus parámetros nuestra aplicación que utilice este comando podrá hacer una cosa u otra.

Me he basado en el autómata de la versión 2. Este era simple: no trabajaba con hilos ni tenía grafo para las dependencias entre las expresiones regulares.  Después lo implementaré en la versión más reciente, pero ahora he creído más conveniente usar un código más sencillo al tener que tocar el núcleo del autómata.

Lo primero de que me percaté fue que reconocía de más.  12/ me decía que era un número fraccionario. El fallo lo localicé cuando pasaba de un ANDF_LANDA  a AFD al reconocer los estados finales añadía transiciones landa de más.  Queda solucionado.

Lo que se me ocurrió para que reconociese los tokens fue que según se recorre el autómata reconociendo los estados con el carácter correspondiente pudiéramos saber si ese estado concreto pertenece a una sub expresión concreta. Si recordamos la estructura general era un ANDF_LANDA que desde su estado 0 salían transiciones landa a AFD mínimos, una por cada  er. Esto significa que para nuestro ejemplo existirá un autómata mínimo del que su estado final nos dirá que es un comando. El problema es que es mínimo, observando sus estados es imposible saber donde comenzaba o terminaba una sub expresión. 

La solución era evidente, trabajar con otra ANDF_LANDA  que conecte las sub expresiones.

Conforme vaya generándose, cuando inserte, concatene o realice el producto de las sub expresiones irá almacenado y actualizando recursivamente el número de estado que justo en ese momento empieza.

Lo mejor de todo es que las sub expresiones ya están en los diccionarios del autómata y siendo mínimos.

Modificando el código he obtenido una función del autómata que pasándole el nombre de la expresión regular y la cadena a reconocer, nos devuelva una lista de tokens.

Ahora nos informa que [0-3 palabra] [4 - 10 parámetro] [ 12 - 18 parametro]. Significa que según se reconozca la cadena y el número del estado en el que se encuentra en ese momento, ahora podemos saber qué está en ese instante reconociendo.  Si está en el estado 2 está reconociendo el token palabra.

En nuestro ejemplo nos daría una palabra de valor 'algo', un parámetro de valor '-p ', otro parámetro de valor '1234 ' y por último otro con valor '-k'.

Notar que los dos primeros parámetros contienen espacios en blanco (porque así está definido según si er), para saber el valor del número habría que repetir la operación: autómata. GetTokens ("parametro","1234 "), obtendría el autómata especial que hemos descrito y como resultado sería un único token número natural de valor '1234'.

Y ya nos devuelve los tokens de una cadena dada su expresión regular.

miércoles, 3 de junio de 2015

Antes de empezar... reorganizar

Esta noche, para evitar que me entrara sueño en el trabajo, me ha dado por pensar un poco en la aplicación y he llegado a la conclusión de que siguiendo el planteamiento que entradas anteriores propuse para seguir avanzando, se pueden cambiar de lugar algunos conceptos.

Le voy a dar más peso al Lanzador. Me refiero a que éste será el que haga de Oyente, el que haga de Coordinador cuando le corresponda y de servidor de actualizaciones.

Pero aún se me han ocurrido más cosas. Sería ideal llegar con un nuevo equipo, conectarme a la red y mediante Telnet conectarme al puerto Oyente de algún participante del que ya conozca su IP (no necesariamente debe de ser el Coordinador). Una vez conectado habría una conversación entre ese participante y yo en el que me solicitaría el nombre mi nombre de usuario del equipo en el que estoy. Con esta información y usando el Participante el comando scp, usuario y mi IP podrá enviarme el Lanzador.

Pero más aún, he leído cómo generar claves públicas para poderlas usar con SSH de modo que si el Coordinador quiere ejecutar comandos remotos en el resto de los participantes sin estar escribiendo las claves contínuamente, a la vez que se envía el Lanzador se podría enviar las claves públicas correspondientes. Aunque tendría que ver qué pasa cuando cambia un participante de rol y deja de ser Coordinador para serlo otro, supongo que copiando las claves públicas y privadas del antiguo Coordinador al nuevo, funcionaría. No lo sé, habrá que comprobarlo.

Si esto funciona, tras recibir el Lanzador y las claves para que el Coordinador pueda ejecutar comandos remotos sobre mi equipo, podría lanzarme automáticamente el Lanzador y cerrarme la conexión Telnet ya obsoleta. Éste, al ejecutarse por primera vez se percatará de que no tiene la aplicación GR y a través del servicio de actualización de código que ahora ha asumido el Lanzador, lo descargará en mi equipo.

Si consigo programarlo así, simplemente conectandome a un Participante cualquiera obtendría el Lanzador y a través de éste de modo automático, el GR y el Coordinador me generará las claves públicas y privadas para  controlarme remotamente.

Según creo que he entendido, aunque las claves son para evitar estar escribiendo la password del usuario cada vez que se pretenda realizar un ssh, por lo menos una vez (para poder generar las claves) sí es necesario escribirlo. Lo que tengo que pensar es cómo pasar la contraseña de mi equipo al Coordinador de forma segura. Ya veremos, sobre la marcha.

miércoles, 6 de mayo de 2015

Preparando el terreno...

No me puedo poner a programar hasta que no termine los exámenes en junio, pero en momentos de saturación durante el estudio sí que he ido preparando el terreno donde se ejecutará el sistema distribuido.

He preparado dos portátiles conectados a un hub. Los dos corren con Fedora y tienen las IPs fijas. Para poder jugar con conexiones de más equipos, ambos tienen instalado virtualbox para instalar más máquinas virtuales.

Estas máquinas virtuales también corren sobre Fedora. El motivo por el que todos utilizan el mismo sistema operativo es que de este modo no me tengo que preocupar (por ahora) de incompatibilidades, por ejemplo, para abrir y cerrar puertos Fedora usa firewall-cmd, otos utilizan directamente iptables.

Para ahorar memoria, se me ha ocurrido que las máquinas virtuales se ejecuten en modo texto. Esto implica que nuestro sistema ha de poder ejecutarse gráficamente (como hasta ahora) y en modo texto.  Mediante el paso del argumento -t al ejecutar el lanzador se podrá indicar que queremos que lo haga en modo texto.

En modo gráfico primero se ejecutaría el lanzador y desde éste, se ejecutará Gr abriéndose en otra ventana, donde el usuario podrá interactuar con las dos aplicaciones a la vez. Sin embargo, en modo texto tengo pensado usar el lanzador como puente entre el usuario y Gr: al lanzar el lanzador en modo texto mostrará en el terminal los menus, ejecutará los comandos y mostrará los resultados. Una vez que ejecute Gr, irá recogiendo los mensajes que éste le pase y los colocará en el terminal. Habrá un mecanismo para que el usuario pueda saber si está trabajando con el lanzador o con GR, por ejemplo algo como 'LN> ' para el lanzador y 'GR> ' para Gr y la posibilidad de cambiar entre un modo y otro incorporándolo en los correspondientes menus.

Según tengo previsto, las máquinas físicas ejecutarían las aplicaciones en modo gráfico y las virtuales en modo texto. Así podré ir programando y comprobando que funcionan a la vez.

Bueno, pues en junio nos vemos. Hasta entonces un saludo a los lectores y no olviden que pueden comentar.

jueves, 9 de abril de 2015

Interfaz gráfica de LanzadorGr

He implementado lo que sería la interfaz gráfica de la aplicación que se encarga de la configuración y lanzado de ProyectGr, LanzadorGr.

En este momento no tiene ninguna funcionalidad programada pero nos da una idea de cómo está organizado todo.

Antes de llegar a este punto había diseñado otra interfaz más compleja, con botones para abrir y cerrar los puertos manualmente, lanzar y parar los servicios, generar los class con rmic, etc., pero tengo una obsesión, facilidad cara al usuario (aunque sea yo mismo) en la medida de lo posible.

Esta versión (la de la imagen) pretende que todo sea automático.

Modo:

Destinado para indicar el perfil del usuario.

- Participante pasivo: no ejecutará su Gr ningún servicio, siempre actuará como cliente. No le hará falta tener los class de rmic. Ejecutará directamente jar empaquetado.

- Participante activo: su Gr ejecutará servicios remotos, podrá ser coordinador, le hará falta los class de rmic y ejecutará también jar empaquetado.

- Desarrollador: semejante al participante activo pero además podrá ejecutar tanto jar empaquetado como los class compilados del proyecto directamente. Será el único que mediante rmic generará los class de los servicios remotos y los distribuirá.

Y para no tener que identificar a los ordenadores que se encuentren conectados por su IP, una etiqueta Usuario.

Comunicaciones:

Se refiere a los puertos e IPs que se utilizan.

- GR: puerto por el que deberá escuchar ProyectGr localmente a LanzadorGr. Gestionado automáticamente buscando un puerto libre para ello.

- LC: puerto por el que escuchará a ProyectGr, también local y automático.

- OYENTE: puerto que por el que permanecerán a la escucha todos los ProyectGr para las consultas de quién es el coordinador, para los ingresos al sistema a más usuarios. Sólo el desarrollador podrá cambiar su valor.

- RMIREGISTRY: aunque por defecto este servicio funciona a través del puerto 1099, como todos los participantes activos y el desarrollador tendrán este servicio trabajando pero también serán clientes entre ellos, a cada pc se le asignará un puerto distinto. Para ello el coordinador preguntará a todos los miembros si tienen libre un puerto determinado, si todos lo tienen, lo asignará al nuevo pc. En caso contrario buscarán otro puerto.

- BASE DE DATOS: puerto por el que escucha mysql. Este valor sólo puede ser modificado por el desarrollador. También se indica su IP.

- COORDINADOR: son dos puertos, uno por el que escucha el ProyectGr remoto (o local si coincide con que el coordinador es el ProyectGr local) que esté realizando el servicio de coordinador y otro que escucha el ProyectGr local. Son gestionados automáticamente y distintos entre ordenadores. Se indicará el IP que tiene ese servicio.

Base de Datos:

Son los parámetros para poder conectarse a la base de datos de mysql: usuario y contraseña. Estos valores sólo puede modificarlos el desarrollador.

Rutas:

Las rutas de los class y jar.

- Proyecto Nedbeands: sólo para el desarrollador. Ruta hacia el proyecto que contiene toda la estructura con el código fuente, los .java, los .class y .jar.

- Jar: ruta hacia el ejecutable empaquetado.

- Destino rmic: en el caso de ser el desarrollador será la ruta donde se depositarán los archivos generados por rmic. Y en el caso de ser participante activo, ruta donde se guardarán estos archivos al ser enviados por el coordinador desde el desarrollador.

Security.Policy:

Mostrará el contenido de este fichero. La primera vez que se ejecute no existirá pero lo traerá del desarrollador y lo guardará en el directorio home del computador. Sólo el desarrollador podrá modificar su contenido. Es imprescindible para poder compartir objetos remotos.

Distribución de servicios:

Mostrará una lista de todos los servicios (y réplicas) que se estén ejecutando y en qué PC lo hace.

Ejecución de comandos:

El LanzadorGr no hará otra cosa que lanzar comandos por consola, capturará las salidas e interpretará. Usará comandos de Linux como netstat, firewall-cmd o rmic, entre otros.

Ahora mismo sólo trabajo con Fedora, cuando lo extienda a otras distribuciones haré que trabaje en función de su firewall específico o a través de iptables.

ProyectGr:

Si es el desarrollador podrá ejecutar tanto el jar empaquetado o sin empaquetar.

Es aquí cuando si se trata del desarrollador generará todos los class de rmic y los colocará en su ruta correspondiente.

Al ejecutar le pasará el parámetro de seguridad Security.Policy a java.

Y para terminar la aplicación será notificado el coordinador para que redistribuya los servicios y datos generados antes de cerrar la conexión. Después volverá a cerrar todos los puertos que ha utilizado.

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.