viernes, 2 de octubre de 2015

Java 8 goodie: Dividiendo elementos en lotes de forma facil (batch jobs)

Después de una hibernación larga, un truco que he podido utilizar recientemente. A ver si me animo y la cosa continúa de vez en cuando.

A veces tenemos una lista de elementos que queremos procesar pero en vez de hacerlo de uno en uno, queremos hacerlo en bloques. Se puede hacer con el típico bucle anidado que va añadiendo a una lista hasta llegar al tamaño del batch..., pero con Java 8 podemos hacerlo de una forma muy muy sencilla usando streams, y usando generics además podemos dejarla reutilizable.

Con algo tal que así (sí, se puede escribir todo en una linea, pero no es una competición de escribir menos)

public static <T> List<List<T>> splitInBatches(List<T> completeList, int batchSize) {
 int blocks = (int) Math.ceil(completeList.size() / (float) batchSize);
 return IntStream.range(0, blocks).mapToObj(count -> {
  int initList = count * batchSize;
  int endList = Math.min(completeList.size(), initList + batchSize);
  return completeList.subList(initList, endList);
 }).collect(Collectors.toList());
}
podemos dividir una lista de elementos y que nos devuelva una lista de listas de elementos, cada una con un tamaño máximo batchSize.
Dado un código

List<String> listaDeCodigos = Arrays.asList("ES","EN","FR"...);
for(List<String> batch : splitInBatches(listaDeCodigos,20))
{
// Hacer algo con el batch de como máximo 20 códigos
}

Podemos procesar la lista de elementos inicial de 20 en 20.

¿Para que nos puede servir? Pues por ejemplo, yo lo he utilizado para procesar una lista de notificaciones pendientes y dividir el trabajo actual de enviar los mails en lotes de X y distribuir los lotes entre los nodos de un cluster. En otro caso lo he utilizado para dividir una consulta SQL muy pesada (de esas que hacen que el servidor de BDD te corte la conexión) en consultas más pequeñas y poder hacerlas de forma concurrente...

No es algo que se use todos los días, pero si alguna vez os pasa, espero que esta solución tan sencilla os sirva.

Happy coding! EJ

viernes, 22 de febrero de 2013

Sesiones, Tomcat y consumo de memoria

Dado que en nuestro caso, además de desarrollar las aplicaciones, gestionamos los servidores de aplicaciones (la parte software), de vez en cuando hago revisiones periódicas del consumo de memoria, versiones de librerías… y en la última revisión encontré algunas cosas interesantes, pero una me llamó bastante la atención:
El consumidor #1 de memoria de los servidores de aplicaciones era… ¡El gestor de sesiones del Tomcat! Aunque parezca una perogrullada, el motivo de mi sorpresa es que nosotros no guardamos apenas datos en sesión, y en muchos casos ni siquiera usamos la sesión para nada puesto que muchas de nuestras aplicaciones son simplemente de consulta.
Así que revisando, revisando, encontré que el problema venía dado por varios factores:

  • Aunque nosotros no usáramos la sesión para nada, algunas llamadas nuestras para comprobar si existía un atributo en la sesión o no, causaban que se crease la sesión sí o sí. Así que un par de if(request.getSession().getAttribute(…)) que se había colado se cambiaron por if(request.getSession(false)!=null && …) para evitar crear sesiones innecesarias.
  • Debido a que ahora tenemos un cluster con “session failover” aunque a la sesión no le metas nada, ocupa un cierto tamaño que al multiplicarse en número empieza a ser significativo. Debido al mismo factor, las sesiones ocupan espacio en ambos nodos del cluster y no se con “fácilmente recoletables”.
  • Dado que las sesiones son creadas “sin querer”, nadie las cierra y por tanto caducan solas agotando el tiempo máximo de vida sin actividad (session-timeout). Así que durante ese tiempo ocupan espacio en ambos servidores del cluster, inútilmente y encima no se pueden recolectar (GC). 
  • Dado que nuestras aplicaciones son públicas, los buscadores las recorren a menudo y en algunos casos sin reutilizar las cookies entre peticiones, así que nos crean una sesión por petición. 
Afortunadamente, para lo bueno y lo malo usamos nuestro propio framework así que introducir cambios para evitar al máximo la creación de sesiones no fue nada complicado. Así mismo, configurar por web.xml que el time-out de las sesiones es de 1 minuto, por si alguna se escapa, y poner un filtro para las aplicaciones solo públicas que si se crea alguna sesión la cierre al acabar cada petición tampoco fue muy complicado (esto último es por qué algunas librerías usadas en algunas aplicaciones pueden crearte sesiones sin que puedas hacer mucho por evitarlo, o si usas JSP o alguna otra tecnología que las cree alegremente).
El resultado final, probado en un nodo antes que en otro, fue que para el mismo tráfico y uso, todo funcionaba igual pero con 1/3 menos de consumo de memoria. Además, ahora un pico de tráfico público, más difícil de controlar, no nos daría tantos problemas mientras las sesiones están esperando a expirar, como pasaba antes, ya que si se crean, el GC las puede liquidar sin problemas.

La moraleja es que aunque creas que no estas usando sesiones, puede que en realidad sí lo estés haciendo y te estén afectando más de lo que creas.

¡Un saludo y happy coding!
E.J.

miércoles, 24 de octubre de 2012

Comparadores, igualdad y error difícil de detectar

Hola,

Después de leer por los internetes la enésima entrada sobre comparar objetos en Java y encontrar de nuevo el error que tienen el 90% de ellas, voy a aportar mi granito de arena para que quede constancia y alguno no se pille los dedos.

Para los que se aburren de leer pronto: Mucho ojo al devolver 0 en el método compare(Object o1, Object o2), tanto al implementar la interfaz Comparable como al crear un objeto Comparator.

La documentación dice que devolver 0 significa que los objetos son iguales, pero eso no es lo mismo que decir que nos da igual como estén ordenados, que eso es lo que entiende mucha gente instintivamente. Así que la pregunta es... ¿que ocurre cuando tenemos una Collection que no permite objetos repetidos ordenada por ese criterio?
La demostración, aquí:
import java.util.Arrays;
import java.util.Comparator;
import java.util.Set;
import java.util.TreeSet;

import lombok.Data;

public class App
{
  @Data
  static class ObjetoOrdenado{
    private final String nombre;
    private final Integer edad;    
  }
  
  static Comparator<objetoordenado> COMPARADOR_POR_EDAD =
     new Comparator<objetoordenado>()
  {
    @Override
    public int compare(ObjetoOrdenado o1, ObjetoOrdenado o2)
    {
      // Haciendo esto tendremos un problema, ya que
      // si devolvemos 0, uno de los elementos desaparecerá
      return o1.getEdad().compareTo(o2.getEdad());
    }
  };
  
  public static void main(String[] args)
  {
    ObjetoOrdenado[] aOrdenar = new ObjetoOrdenado[]{
        new ObjetoOrdenado("John",24)
        ,new ObjetoOrdenado("Jane",29)
        ,new ObjetoOrdenado("Bob",30)
        ,new ObjetoOrdenado("Susan",27)
        ,new ObjetoOrdenado("Kenny",24)
    };
    Set<objetoordenado> ordenados = new TreeSet(COMPARADOR_POR_EDAD);    
    ordenados.addAll(Arrays.asList(aOrdenar));
    for (ObjetoOrdenado o : ordenados)
    {
      System.err.println(o.getEdad() + " - " + o.getNombre());
    }
    System.err.println("They killed Kenny!");
  }
}
->

24 - John
27 - Susan
29 - Jane
30 - Bob


El resultado es que el pobre Kenny desaparece, eliminado por ser considerado "igual" que John al ordenar. La solución es no devolver 0 excepto cuando estemos seguros de que efectivamente son iguales, y si no pues devolvemos un 1 o un -1, lo que sea, pero distinto de 0.

Ésta implementación de compare sería una solución
import java.util.Arrays;
...
    @Override
    public int compare(ObjetoOrdenado o1, ObjetoOrdenado o2)
    {
      // Primero ordenamos por edad
      int temp = o1.getEdad().compareTo(o2.getEdad());
      if(temp==0)
      {
        // Si la edad es la misma, ordenamos por nombre
        temp = o1.getNombre().compareTo(o2.getNombre());
        // Si la edad y el nombre son iguales, ordenamos al azar.
        // Si realmente nombre y edad iguales significa que solo
        // queremos UN objeto, entonces podríamos saltar este paso y devolver 0
        if(temp==0)
        {
         temp = -1;  
        }
      }
      return temp;
    }
  };
...

Es un error difícil de detectar por que podemos tener una colección con todos los objetos, todo va bien, los ordenamos y ¡zas! mágicamente tenemos menos, sin excepciones, sin avisos.

Así que espero que a alguien le sirva para no caer en ello, siguiendo los típicos ejemplos de Internet que suelen explicarlo mal. Por dejarlo claro, yo caí en ello y por eso lo tengo bien aprendido :D.

Happy coding! EJ

viernes, 14 de septiembre de 2012

El problema de N+1 consultas: explicación, caso real y solución propuesta


La entrada de hoy trata sobre un problema recurrente al acceder a bases de datos y especialmente al usar abstracciones por encima, como JPA o Hibernate, sin leer en detalle el funcionamiento interno de estas herramientas.

El problema se da al querer acceder a un objeto y sus N objetos relacionados, normalmente a través de claves foráneas, ya que la estrategia simple implica hacer una consulta para obtener el objeto principal y las claves de los objetos relacionados, y luego hacer N consultas, cada una para obtener un objeto relacionado, usando las claves obtenidas en la primera consulta. De ahí el nombre “N+1 consultas”. La trampa de este problema está en que con pocos datos o en ejemplos simples, el deterioro del rendimiento puede no ser significativo, pero cuando el número de objetos relacionados crece y los objetos relacionados están relacionados a su vez con otros objetos, que a su vez... En fin, la cosa crece exponencialmente y es la causa principal de que la gente se queje de que los ORM son lentos, ya que por defecto éste es su comportamiento y la gente no suele ir más allá.

Para ilustrar el problema usaremos un caso real con el que tuve que lidiar hace relativamente poco: mostrar en detalle los exámenes de un plan de estudios (sí, ya sé que suena a problema de examen de la carrera :) ). La cuestión es que eso que a principio suena tan simple se transforma en:
  • Los planes de estudios se ofrecen en varios campus (es lo que tiene una universidad con varios campus).
  • Para cada plan/campus, hay unas asignaturas que se ofrecen.
  • Cada asignatura, se divide a su vez en grupos-asignatura (por turnos, por letras, por plan ya que una asignatura puede ofrecerse en varios planes).
  • Para cada grupo-asignatura, se definen una serie de exámenes con sus datos.
  • Además, cada examen puede tener lugar en varias localizaciones (normalmente aulas de un mismo edificio).
  • Para componer más la cosa, los grupos-asignatura se juntan en entidades llamadas grupo-horario para poder distribuir los alumnos sin problemas de forma ordenada sin que haya solapamientos de horarios/exámenes etc.
  • Todo esto, filtrado por año académico.
Si queremos obtener estos datos para poder mostrarlos y lo hacemos “al tun-tun”, lo que ocurre es que, partiendo del código de plan de estudios, campus y año académico:
  • Obtenemos los datos del plan de estudios a partir de su código.
  • Obtenemos los datos del campus a partir su código.
  • Obtenemos la lista de códigos de los grupos-horario que se ofrecen para ese año académico filtrando por plan, campus y año.
  • Obtenemos los datos del grupo-horario a partir de su código.
  • Obtenemos los códigos de los grupos-asignatura de cada grupo-horario según el código de grupo-horario y el año académico.
  • Obtenemos los datos del grupo-asignatura por código
  • Obtenemos los datos de la asignatura por la clave foránea que tiene grupo-asignatura.
  • Obtenemos los códigos de los exámenes de cada grupo-asignatura.
  • Obtenemos los datos de cada examen por código
  • Obtenemos los códigos de las localizaciones de cada examen.
  • Obtenemos los datos de cada localización por código.

Cada una de esas lineas implica una consulta de 1 o N elementos, con el retardo extra que ello implica. Y eso si solo nos traemos los elementos necesarios, si usamos un  ORM y no tenemos cuidado con las relaciones que tenemos marcadas como “eager fetching”, podemos hacer muchas más consultas y traernos además multitud de objetos que, encima, no vamos a usar. La solución eficiente en cuanto a minimizar las consultas es, en este caso, sencilla: podemos usar una sola consulta para obtener todos esos datos. Una señora consulta, sí, y con datos repetidos, también, pero en un único viaje por la red, que en muchas veces será lo que más nos importe. La consulta, sin entrar en detalle, sería algo así

  • Plan de estudios
  • ∟ Datos del plan de estudios
  • ∟ Datos del campus
  • ∟ Lista de grupos-horario
    • Datos de grupo-horario
    •  Lista de grupos-asignatura
      • Datos de grupo-asignatura
      •  Datos de asignatura
      •  Lista de exámenes
        • Datos de examen
        •  Lista de localizaciones
          • Datos de localización
Ahora bien, si tenemos los datos de plan de estudio y campus repetidos en cada fila del resultado, los de cada grupo-horario para n filas etc. ¿Cómo hacemos para reconstruir el árbol de objetos sin que sea una pesadilla? Ahí es donde entra en marcha algo como Ibatis/MyBatis, que no sólo nos permite definir libremente las consultas si no que se encarga de “separar” los datos adecuadamente una vez se lo indiquemos en el fichero de “mapeo”. En este caso, al definir la consulta le diríamos que el resultado es un objeto de la clase PlanEstudios que contiene los datos del plan, y una referencia a un objeto de la clase Campus, que contendrá los datos del campus. Además, la clase PlanEstudios contiene una lista de objetos GrupoHorario, que se puede discriminar con la columna correspondiente al código de grupo-horario, el cual contiene los datos del mismo y una lista de objetos GrupoAsignatura el cual...

Una vez hecho así, podemos ejecutar la consulta y ésta, suponiendo que los datos estén bien, devolverá un único objeto de la clase PlanEstudios, con la listade GrupoHorario rellenada, y para cada GrupoHorario sus datos rellenados y sus listas de GruposAsignatura rellenadas etc. Todo el árbol de objetos rellenado y con los datos en su lugar con una sola consulta, no sólo a nivel SQL si no también a nivel de lógica, lo cual es una ventaja importante.

Obviamente, todo tiene su precio y en este caso el precio es introducir el framework Ibatis/MyBatis, y definir el mapeo de las columnas a los campos de los objetos, pero el beneficio es claro en cuanto a claridad en el código y rendimiento. Por otro lado, mencionar que no siempre es posible convertir todas las consultas en una (por ejemplo cuando un objeto está relacionado con dos listas de objetos independientes), pero en este caso lo que se puede hacer es definir la consulta de forma que obtenga los mayores datos posibles de la forma más eficiente minimizando las, en este caso inevitables, “consultas N”.

Cabe también mencionar que las herramientas ORM decentes incluyen opciones (véase la especificación del tipo de fetch y la utilización de “inner join” en  JPA) para poder controlar este tipo de cosas y, si bien de forma más limitada, permiten evitar en algunos casos el problema de las N+1 consultas y/o de la carga de objetos innecesaria. Así que antes de culpar a la herramienta, hay que asegurarse de que una la está usando correctamente.

No incluyo en esta entrada detalles de implementación, para no hacerla aun más extensa, pero espero que sirva para ilustrar un problema que existe y os encontrareis en el mundo real, y una forma de solucionarlo. Si el tema interesa, se podría hacer otra entrada con detalles de ese tipo, pero esa es otra historia ;).


Happy coding! EJ

jueves, 30 de agosto de 2012

Cluster de Tomcats con balanceo y "session failover"

Al hilo del artículo de David González Múltiples Tomcats con Apache y conector HTTP o AJP y el problema mencionado de tener que reiniciar un Tomcat al actualizar aplicaciones y perder servicio, vamos a añadir este articulillo rápido para explicar como montar un cluster de Tomcats con varias aplicaciones intentando no perder servicio al reiniciar. En principio la idea es tener todas las aplicaciones en 1 Tomcat y hacer cluster para que podamos pararlo y re-arrancarlo sin perder servicio. También serviría, con ligeras modificaciones, para tener varios Tomcat, dividiendo las aplicaciones entre ellos, y luego "clusterizárlos". Con eso conseguiríamos las ventajas que menciona David, más las de este artículo.

El primer paso es tener tus aplicaciones montadas en un Tomcat normal y corriente. Si no sabes hacerlo, empieza por ahí ya que éste artículo te será muy complejo sin una buena base.

Vamos a suponer que las maquinas donde quieres desplegar las aplicaciones se llaman app_host1.com y app_host2.com (nombres no publicos pero accesibles desde el servidor donde se encuentra el Apache) y que en ambas escucha el Tomcat con el protocolo AJP por el puerto 8009 o con el protocolo HTTP por el puerto 8080. Podemos usar el protocolo que más nos convenga (a AJP se le supone mejor rendimiento pero cada uno...)

Una vez tenemos claro esto, para dividir la carga entre las dos máquinas y enviarla a los Tomcats, y con "sticky sessions", usaremos el mod_proxy_balancer de Apache, añadiendo este bloque a la configuración en el lugar adecuado (depende de lo que queramos hacer y la versión de Apache, por defecto httpd.conf):
 # Definición del cluster con 2 nodos, usando el protocolo AJP
 <Proxy balancer://app_ajp_cluster>
  BalancerMember ajp://app_hos1.com:8009 route=Tomcat-All.1  
  BalancerMember ajp://app_hos2.com:8009 route=Tomcat-All.2  
  ProxySet stickysession=JSESSIONID|jsessionid scolonpathdelim=On
 </Proxy> 
 # Definición del cluster con 2 nodos, usando el protocolo HTTP
 <Proxy balancer://app_http_cluster>
  BalancerMember http://app_hos1.com:8080 route=Tomcat-All.1  
  BalancerMember http://app_hos2.com:8080 route=Tomcat-All.2  
  ProxySet stickysession=JSESSIONID|jsessionid scolonpathdelim=On
 </Proxy> 
 # Descomenta la siguiente parte para administrar el cluster, si sabes lo que haces.
 # Esta parte sirve para poder consultar/gestionar el estado del cluster
 # ¡¡No debe dejarse abierto al público!!
 #<Location /balancer-manager>
 # SetHandler balancer-manager
 # Order Deny,Allow
 # Deny from all
 # Allow from W.X.Y.Z   #Host del administrator
 # #... otras medidas de proteccion
 #</Location>
 #...
 # Para enviar un contexto al cluster de Tomcats, podemos hacerlo así
 ProxyPass ^/my1app/ balancer://app_ajp_cluster/myapp/ [P,QSA,L]
 # o si queremos afinar más...
 RewriteRule ^/my2app/ balancer://app_ajp_cluster%{REQUEST_URI} [P,QSA,L]
 # Podemos hacer virguerías como deshabilitar acceso a aplicaciones si no es por SSL
 RewriteCond %{SERVER_PORT} =443
 RewriteRule ^/mysslapp/ balancer://app_ajp_cluster%{REQUEST_URI} [P,QSA,L]
 # Y otras magias que permite RewriteCond

* Conviene asegurarse que en el Apache que estemos utilizando están cargados los modulos proxy_module, proxy_ajp_module, proxy_balancer_module, proxy_http_module, rewrite_module, headers_module y status_module.
** NO hay que olvidarse de proteger el balancer-manager si lo abrimos. YOU HAVE BEEN WARNED!!!

Ahora necesitamos definir en la configuración del Tomcat los nombres de las rutas, para lo cual modificaremos el fichero server.xml (o equivalente) y en el nodo  Engine añadiremos el atributo jvmRoute con el valor adecuado en cada nodo. Así pues, en el host app_host1.com el nodo quedará como:

    <Engine
      name="..."
      defaultHost="localhost"
      jvmRoute="Tomcat-All.1"
    >

en el host app_host2.com el nodo quedará como:

    <Engine
      name="..."
      defaultHost="localhost"
      jvmRoute="Tomcat-All.2"
    >


Una vez hecho esto, tenemos un cluster con "sticky sessions", lo cual nos permite tener aplicaciones con sesión y donde todas las peticiones de la misma sesión van al mismo Tomcat. Sin embargo, ¿Qué ocurre si uno de los Tomcat se cae? Pues que las sesiones se perderán y aunque los usuarios podrán seguir accediendo de forma transparente, perderán lo que estaban haciendo. En ciertos casos, como aplicaciones públicas sin identificación, eso no nos importará y el servicio se mantendrá sin incidencias, pero en los casos donde, como mínimo, se guarda el usuario identificado como atributo de la sesión como prueba de login, el servicio se interrumpirá y "echará" a todos los usuarios obligándoles a identificarse de nuevo, perdiendo lo que tuvieran en la sesión.

Como ese resultado no es muy agradable para los usuarios, ¿cómo añadimos "session failover" a nuestra configuración para que eso no pase? Pues existen multitud de opciones (Terracota, Hazelcast...) para hacerlo, pero la más sencilla y barata (aunque no tan fiable) es usar el clustering que viene con el Tomcat.

Para usarlo, dentro del nodo Engine de la configuración del Tomcat (server.xml o equivalente) añadiremos el elemento Cluster, configurado adecuadamente. Antes de mostrar la configuración en sí, analicemos los datos que vamos a necesitar definir:
  • Necesitamos una dirección y un puerto de multicast, para que los nodos se encuentren (en el ejemplo definidas como ${tomcat.mcast.addr} y ${tomcat.mcast.port}). Deben ser los mismos valores para todos los nodos del cluster.
  • Necesitamos el nombre público del host particular para que los otros miembros del cluster se puedan comunicar (en el ejemplo ${tomcat.host}). Esto que parece una obviedad no lo es si añadimos DNS internos, firewalls, etc. Hay que definirlo por que si no, lo más normal es que la JVM asuma por defecto que nuestro host se llama... "localhost" y cuando otro miembro del cluster quiera comunicarse con "localhost"... podeis suponer lo que pasa. Así que hay que asegurarse que ese nombre es correcto y se le pasa correctamente al Tomcat, ya que es la fuente #1 de problemas de configuración del cluster. Avisados quedais.
  • * Hace falta además un puerto para las comunicaciones entre Tomcats, en el ejemplo definido como ${tomcat.nio.port}.
  • Si estos últimos campos son iguales o diferentes para todos los nodos, depende de si los nodos están en la misma máquina o no.
  • En el ejemplo los valores esán definidos como ${xxx} por que el script de arranque ha sido modificado para pasar los valores adecuados en cada nodo del cluster, pero el Tomcat por defecto no rellena estos valores, así que modifica los valores, o el script, o no te te funcionará.

Con todos estos datos, así es como queda el trozo de configuración:

<Engine
 ...
    >
      <Cluster
        className="org.apache.catalina.ha.tcp.SimpleTcpCluster"
      >
        <Manager
          className="org.apache.catalina.ha.session.DeltaManager" />
        <Channel
          className="org.apache.catalina.tribes.group.GroupChannel"
        >
          <Membership
            className="org.apache.catalina.tribes.membership.McastService"
            address="${tomcat.mcast.addr}"
            port="${tomcat.mcast.port}" />
          <Sender
            className="org.apache.catalina.tribes.transport.ReplicationTransmitter"
          >
            <Transport
              className="org.apache.catalina.tribes.transport.nio.PooledParallelSender" />
          </Sender>
          <Receiver
            className="org.apache.catalina.tribes.transport.nio.NioReceiver"
            address="${tomcat.host}"
            port="${tomcat.nio.port}" />
          <Interceptor
            className="org.apache.catalina.tribes.group.interceptors.TcpFailureDetector" />
          <Interceptor
            className="org.apache.catalina.tribes.group.interceptors.MessageDispatch15Interceptor" />
          <Interceptor
            className="org.apache.catalina.tribes.group.interceptors.ThroughputInterceptor" />
        </Channel>
        <ClusterListener
          className="org.apache.catalina.ha.session.ClusterSessionListener" />
      </Cluster>
      <Host
   ...
    </Engine>
* Nota: Esta es la configuración mínima que he conseguido que me funcione. Algunos atributos tienen los valores por defecto, según la documentación, y algunos nodos deberían estar incluidos por defecto, de nuevo según la documentación, pero o la documentación miente o el Tomcat se pasa por el forro de la boina los valores por defecto en algunos casos, escoge la que más te guste. De todas formas, esta no es la única configuración posible y lo único que puedo decir de ella es que se ajusta a los que nos interesa y nos funciona ;).

En caso de querer hacer pruebas para ver si funciona en una sola maquina, lo que habría que hacer es usar dos Tomcat cambiando los puertos y ajustando la configuración adecuadamente.

Se podrían contar más cosas, como el script que arranca los Tomcat pasando los valores adecuados, usar un solo binario para multiples Tomcats detrás de un único Apache... pero como dice un gurú... eso es otra historia.

Happy coding!
EJ.

Disclaimer: Este no es un tema sencillo y no es recomendable para los no-iniciados, así que tratarlo con prudencia ;).

jueves, 15 de septiembre de 2011

Lucene: sin regla ni Compass

Saludos después de una larga pausa donde, básicamente, me he dedicado a la familia y al ocio, para que negarlo. La verdad es que no estaba seguro de si volvería a escribir o no en el blog, ya que mi visión de eso llamado comunidad se va degradando con el tiempo, pero los ánimos me han dado para algunas entradas más, y aquí estamos de nuevo.

Esta entrada debería haber sido algo diferente, ya que mi intención era principalmente comentar la librería Compass, que es una especie de envoltorio de Lucene para facilitar su uso cuando tratamos con objetos Java, pero después de usarla en unos cuantos proyectos y estar relativamente satisfecho con su uso, un ligero problemilla me hizo consultar sus listas de distribución donde buceando entre el spam me encontré con la sorpresa de que su autor declaraba oficialmente que pasaba a hacer otra cosa y que ya no le interesaba trabajar en la librería. Al ser un one-man project, la cosa queda en que Compass es, en estos momentos, “abandonware”, por lo que hacer entradas explicando su uso, ventajas… me parece hacerle un flaco favor al pobre programador que debido a ellas acabe usándola y encontrándose la misma desagradable sorpresa que yo.
Sin embargo, aparte de advertir del estado del Compass, he decido modificar el contenido de la entrada y limitarme a hablar de los conceptos que he aprendido últimamente del Lucene y que son aplicables fuera de Compass.

Así que si alguna vez te toca trabajar con Lucene, espero que estos consejillos te sirvan:
  • Lo más normal, si estas realizando búsquedas en castellano o algún lenguaje con caracteres "raros" para los anglos, es que quieras usar un analizador personalizado (custom analyzer) que utilice, al menos, los filtros ISOLatin1AccentFilter, LowerCaseFilter y StandardFilter. De esta forma tus documentos (así es como llama Lucene a lo que sea que indexes) se indexarán sin tener en cuenta mayúsculas, ni acentos u otros caracteres no-ascii.
  • Eso sí, una vez indexados los documentos de esa forma, ojo con una característica del Lucene muy desagradable: Las búsquedas se deben pasar, habitualmente, por el mismo analizador que al indexar, pero el Lucene ignora al analizador sin decir nada si en las búsquedas se usan comodines (‘*’ o ‘?’). Así que si realizamos una búsqueda sin comodines, los resultados no serán independientes de las mayúsculas o los acentos y nos llevaremos desagradables sorpresas. La excusa oficial, con su pequeña parte lógica, es que el analizador puede cambiar los términos de búsqueda, por ejemplo para buscar  en singular y plural indistintamente, y entonces al hacer esos cambios junto a comodines, el termino final de búsqueda podría no parecerse a lo que quería el usuario. Bueno, vale. ¡Pero déjame elegir! En nuestro caso no puede ocurrir nada de eso, simplemente cambiamos letras mayúsculas por minúsculas y cambiamos á por a … pero Lucene considera que dejar escoger al programador, pobre tonto, es un peligro y no da la opción de desactivar esa “ayudita”. Sin comentarios. La única solución que he encontrado es pasar por esos mismo filtros los términos de búsqueda “manualmente” y antes de pasárselos al Lucene, lo cual me parece una chapuza. Pero avisados quedáis.
  • Otro truco útil es para cuando la gente quiere hacer una búsqueda de palabras que “acaben en”, o sea *algo . Lucene está pensado para búsquedas que “empiecen por”, o sea algo*, y en cambio empezar por un comodín es muy ineficiente. ¿Como solventarlo? Muy sencillo, a la hora de indexar, indexamos ese campo al revés y a la hora de buscar le damos la vuelta al termino de búsqueda, o sea ogla*. Por el módico precio de tener indexado el campo dos veces, volvemos a tener búsquedas eficientes: voilà.
  • Este último caso es un claro ejemplo de una técnica general muy útil cuando trabajamos con búsquedas indexadas, lo que hacemos con Lucene, vaya: En caso de tener problemas al montar el criterio de búsqueda, se puede probar a modificar la forma en que se indexa el contenido para facilitar las cosas. Por ejemplo, tenemos una lista de productos que pueden venderse en distintas tiendas y de esas tiendas hay sucursales en distintas provincias. Si a la hora de indexar, usamos un campo para indexar la lista de tiendas en las que se vende un producto y en otro campo la lista de provincias donde se vende… ¿Cómo podemos saber en qué provincias se vende el producto X en una tienda determinada? La respuesta es que así indexado no lo podemos saber, ya que hemos perdido la relación tienda-provincia. En este caso lo que tenemos que hacer es crear un nuevo campo donde indexemos el par tienda-provincia, que es por el que buscaremos para este tipo de consultas.
  • De igual forma, podemos indexar en varios campos los apellidos junto con el nombre o los apellidos por separado, según como queramos poder buscarlos. A no ser que tengamos un índice de modificaciones muy alto, o un volumen de datos inmenso, no hay que tener miedo en invertir en la indexación a cambio de obtener un rendimiento mucho más alto en la parte presumiblemente más usada: las búsquedas.

Happy coding! EJ

sábado, 2 de julio de 2011

Generando números de versión automáticamente con Maven

En esta entrada de hoy mostraré como implementar un sistema para que los usuarios de nuestras librerías puedan averiguar fácilmente cual es la versión que están utilizando y que se mantenga automáticamente al generar nuestros .jar con Maven.

Introducción
Uno de los “problemas/features” cuando uno usa Maven/Ivy/Grape o sistemas similares para gestionar sus dependencias es que independientemente de la versión, las librerías acaban teniendo un nombre común. Es decir, que la librería org.hibernate:hibernate:3.2.6.ga acaba llamandose hibernate.jar, igual que si fuera la 3.3.0.SP1 o cualquier otra versión. Esta característica ayuda a la hora de reemplazar una versión con otra, pero dificulta averiguar de un vistazo que versiones de librería estamos utilizando.

En caso de que nosotros hagamos una librería y se use a través de uno de estos sistemas, nuestros usuarios tendrán el mismo problema para averiguar en el sistema final qué versión está desplegada. Como somos unos “grandes” programadores, queremos facilitar a nuestros usuarios el averiguar está información, pero como somos unos programadores vagos eficientes no queremos tener que hacerlo a mano cada vez que generamos un .jar, y menos si usamos algo como Maven para gestionar nuestro proyecto. Así pues, ¿como lo podemos hacer?

Implementación
Paso 1:
Lo primero que tenemos que hacer es almacenar el número de versión de forma automática en algún sitio. Un buen sitio para hacerlo es en el fichero Manifest de nuestro .jar, y para hacerlo de forma automática podemos utilizar el plugin de Maven org.codehaus.mojo.buildnumber-maven-plugin. La documentación que tiene no es demasiado extensa, pero investigando un poco podemos averiguar cómo usarlo. En mi caso, dado que utilizo Mercurial como sistema de versiones y el numero de versión que utiliza no es muy significativo (no es un número correlativo) así que lo he sustituido por una marca de tiempo que me da una indicación más fiable. Si usas algo como Subversion, entonces quizá la configuración estándar del plugin ya te sirva Dado que el plugin no se encuentra en el repositorio central de Maven, tenemos que añadir un repositorio de plugins para encontrarlo, de la siguiente forma:
    
        codehaus-snapshot
        Cohehaus snapshot repository
        https://nexus.codehaus.org/content/groups/snapshots-group
        
          true
        
    

Una vez hecho esto, configuramos el plugin para que nos defina unas variables con la información que queremos, así:
    
      org.codehaus.mojo
      buildnumber-maven-plugin
      1.0-beta-5-SNAPSHOT
      
        
          validate        
          
            create
          
        
      
      
        true
        true
        {0,date,dd/MM/yyyy HH:mm:ss}
        
          timestamp
        
      
    


Con esto le decimos que nos añada el timestamp a la variable buildNumber, que es donde el plugin pone su información.
Por último, configuramos el plugin org.apache.maven.plugins.maven-jar-plugin para que use esa información para añadir una nueva entrada en el Manifest de nuestro jar. De la siguiente forma:


 org.apache.maven.plugins
 maven-jar-plugin
 2.3.1      
 
  
   false
   
    ${version}-${buildNumber}
    my.package.MainApp
   
  
 



Una vez hecho esto, si ejecutamos mvn package, por ejemplo, para generar nuestro fichero ,jar y miramos el fichero MANIFEST.MF dentro del directorio META-INF, deberíamos ver algo tal que así:

Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Created-By: Apache Maven
Built-By: usuario
Build-Jdk: 1.6.0_21
Main-Class:  my.package.MainApp
MyLibrary-version: 0.1-SNAPSHOT-25/06/2011 13:33:01

Paso 2:
Bien, ya tenemos almacenada la versión en el .jar. ¿Ahora que hacemos para facilitar que el usuario pueda ver esa información sin tener que abrir el .jar y mirar el manifest? Muy sencillo: la clase my.package.MainApp que es la principal de nuestra aplicación tiene que mostrar esa versión. En mi caso, la librería es una utilidad que no se lanza en linea de comandos, se usa añadiéndola al classpath, así que mi clase MainApp simplemente muestra la versión de la librería.

¿Y como hacemos para leer el manifest del mismo fichero del cual nos estamos ejecutando? Pues averiguando donde se encuentra el .jar en tiempo de ejecución y accediendo a él para leer el fichero manifest. Podemos hacerlo así (código simplificado sin gestión de errores):

String jarFileURL = 
  MainApp.class.getProtectionDomain().getCodeSource().getLocation().toString();
int pos = jarFileURL.indexOf("!");
if(pos!=-1)
{
  jarFileURL = jarFileURL.substring(0,pos);
}
if(!jarFileURL.startsWith("jar:"))
{
  jarFileURL = "jar:" + jarFileURL;
}
URL manifestUrl = new URL(jarFileURL + "!/META-INF/MANIFEST.MF");
Manifest manifest = new Manifest(manifestUrl.openStream());      
return manifest.getMainAttributes().getValue(“MyLibrary-version”);

Y así podemos hacer que al ejecutar java -jar milibreria.jar nos devuelva la versión de la librería, o si lo ponemos en un método público, podemos usar este número para mostrarlo en las aplicaciones que usen la librería y así puedan saber que versión están utilizando, comprobar si existen nuevas versiones etc.

Espero que os sirva, al menos a mi me ha servido, y que vuestros usuarios estén más contentos :).

Happy coding! EJ