Mantén a tus bugs cerca y a tus librerías aún más cerca. Ejemplo de Guava

0
3370

En este tutorial vamos a ver un ejemplo que demuestra cómo es fundamental conocer el funcionamiento de las librerías que usamos en nuestros desarrollo.

0. Índice de contenidos.


1. Introducción

Todos los que desarrollamos sabemos que sin el trabajo de otros en forma de librerías (bibliotecas), poco podríamos hacer, o al menos costaría mucho más esfuerzo y tiempo. Pero también que estas librerías pueden ser un arma de doble filo si no se comportan como nosotros esperamos…Y es que realmente estamos en manos de las librerías, salvo que las hagamos nosotros mismos 🙂 …nos guste o no…

Hace un tiempo escribí un tutorial sobre Guava para poder utilizar el estilo de programación funcional dentro de versiones de Java anteriores a Java 8. Surgió porque en un proyecto en el que he colaborado lo hemos usado masivamente, así que buena parte del código está en manos de la librería de Guava, que es una librería de Google, y que por tanto no hemos implementado nosotros.


2. El problema

En el proyecto usábamos EhCaché para hacer un cacheo sencillo de ciertas funciones que se llaman frecuentemente.

@Cacheable(value = "ffsCache", key = "#configuration")
public List<FareFamilyConditions> getFareFamilyConditionsToDisplay(
  final FareFamilyConditionsConfiguration configuration) throws FfsException{
    return ...;
}

EhCache lo que hace, explicado muy básicamente, es tomar una «key» de entrada, en este caso el objeto configuration de la clase FareFamilyConditionsConfiguration, y obtiene su hash. Si en la tabla de caché no está ese hash, ejecuta el método y toma el resultado, en este caso una lista de POJOS: List y la almacena en su tabla. La próxima vez que se llame con el mismo hash, en vez de ejecutar el método, devolverá el valor del mapa, siempre que las condiciones de caché (expiración) se cumplan. Por cierto, si manejamos anotaciones es porque la caché está configurada con Spring… (más librerías).

¿Con qué problema nos encontramos?

El problema es que detectamos una ocupación exagerada de la caché que no correspondía para nada con unas pequeñas listas de unos pocos elementos de tipo POJO, que era lo que esperábamos. Esto hacía que la caché básicamente no funcionase la caché, algo que no éramos capaces de comprender porque la configuración parecía correcta.

Utilizando un debugger y activando las trazas en modo debug, veíamos que EhCache comenzaba a devolver una gran cantidad de objetos y a navegar sobre ellos. Interiormente tiene unos métodos que recorren todo el árbol de objetos, y para estos casos cuenta con un límite configurable para inspeccionar la caché. Incluso 10.000 objetos se quedaba corto y abortaba el funcionamiento de la caché.

A esto se sumaba que los test de integración de ese método, incluyendo caché, no daba problemas, pero al utilizarlo dentro de otro módulo (que era mucho mayor y su contexto de Spring era un poco «grande»), la caché fallaba por saturación de ésta.


3. La causa

La clave estaba en Guava, aunque mejor dicho, en el uso de las funciones anónimas para generar la lista.

En el tutorial sobre Guava vimos cómo a partir de una lista se puede generar otra filtrada o transformada. Simplemente hace falta la lista original y un objeto de la clase Function que se aplica a cada elemento para obtener otra lista.

Volviendo a nuestro caso real de aplicación, la clase FareFamilyConditionsConfiguration, que es el parámetro de entrada del método cacheado y por tanto «key» de la caché (configuration), tiene dentro una List<Strings> que ha de ser calculada. Para calcularla se hizo con Guava, ya que esa List<Strings> venía de la transformación de una List< LIST_FARE_FAMILYDefType>, que es un tipo algo más complejo. Nosotros sólo queríamos introducir esos objetos y sacar un String de ellos, que formaba parte de una de sus propiedades. Nada mejor que una función anónima generada en un método privado de una clase.

Primero declaramos el modo que genera la función (también lo podíamos haber hecho inline o asignarlo a un objeto):

private Function<LIST_FARE_FAMILYDefType, String> extractFareFamily(){
  return new Function<LIST_FARE_FAMILYDefType, String>(){
    @Override
    public String apply(final LIST_FARE_FAMILYDefType input){
      return input.getFARE_FAMILY();
    }
  };
}

Y esta simpática generación de funciones se usa en el siguiente código

List<String> ffcc = Lists.transform(fareFamilies, extractFareFamily());
FareFamilyConditionsConfiguration  configuration = new ... (ffcc);
ffsService.getFareFamilyConditionsToDisplay(configuration);

En la última línea puedes ver cómo se llama al método que tiene la anotación @Cacheable del que hemos hablado al comienzo.

El problema está en el List<Strings> ffcc, que viene de un List.transform de Guava, y que usa el objeto de tipo Function que hemos indicado antes:

List<String> ffcc = Lists.transform(fareFamilies, extractFareFamily());

Nuestra función espera un objeto en cuyo interior tiene esa lista llamada ffcc… ¿Y qué lista esperamos que sea?

Aquí está el error. Cuando vemos un List<String> esperamos que sea un objeto de tipo lista, pero debemos recordad que java.util.List<E> no es más que una interfaz, por lo que no define nada de lo que almacena internamente. Sólo define unos pocos métodos y el comportamiento de estos, pero no si dentro almacena unos objetos u otros. Puedes consultar la interfaz de Java7 en https://docs.oracle.com/javase/7/docs/api/java/util/List.html

Cuando trabajamos con listas, estamos habituados a usar la clase java.util.ArrayList (https://docs.oracle.com/javase/7/docs/api/java/util/ArrayList.html), que al ser una clase define claramente lo que almacena, y esto no es otra cosa que un vector con los Strings que estamos almacenando.

Lo podemos ver en el código fuente de la clase ArrayList. Básicamente tiene un vector de los objetos contenidos y un indicador del tamaño.

private transient Object[] elementData;
private int size;

Por tanto, si por ejemplo, nuestra lista original fuera [«uno», «dos», «tres»] y usásemos una transformación que añadiese un caracter «_» al final, esperaríamos como resultado un ArrayList que tuviese en su interior [«uno_», «dos_», «tres_»]. Al llamar al get(0), devolvería «uno_». Hasta aquí lo normal.

¿Qué almacena una lista de Guava realmente?

El problema está en que usamos Guava, y en concreto el método estático Lists.transform, cuya documentación podemos ver aquí.

Si nos fijamos en el código fuente, que podemos ver en GitHub, podemos ver que:

  public static  List transform(
      List fromList, Function function) {
    return (fromList instanceof RandomAccess)
        ? new TransformingRandomAccessList(fromList, function)
        : new TransformingSequentialList(fromList, function);
  }

Dependiendo del tipo de lista de entrada, se usa TransformingRandomAccessList o TransformingSequentialList. Si vamos al código de alguna de estas clases, tendremos la respuesta:

private static class TransformingRandomAccessList<F, T> extends AbstractList<T> implements RandomAccess, Serializable {
  final List<F> fromList;
  final Function<? super F, ? extends T> function;
  ...

De momento, por los nombres, podemos ver que se almacena NO la lista transformada, sino la lista original. Y también la función de transformación, sí, esa que hemos creado antes… ni rastro de la lista completamente transformada. Yendo a la implementación del método List, tenemos la prueba irrefutable:

@Override
public T get(int index) {
  return function.apply(fromList.get(index));
}

Efectivamente, la lista que nos devuelve es de tipo lazy, es decir, no se lleva a cabo la transformación hasta que no se llama al «get». Por tanto, internamente a nivel de objetos, dentro de la lista que estábamos devolviendo, no está el resultado, sino la lista original y la función.

¿Sorprendente? Por no conocer bien la librería que estamos usando, hemos pensado que estábamos guardando una información que en realidad no se está guardando…

Pero si se almacena la lista original y la función de transformación y esto es determinista… ¿Cuál es el problema? En realidad no hay problema tal debido al almancenamiento de la lista… pero sí de la función.

El problema de la Function

Además de la lista original guardamos la función… bueno, no pasa nada: en nuestra List<String> tenemos un listado de Strings que no es el que necesitamos, y un objeto más… la función… ¿Tan grave es meter un objeto más? No parece que tenga mucho problema… Bueno, en realidad depende del objeto del que se trate.

Y es que, en nuestro caso, el objeto de tipo Function está definido de forma anónima, es decir, dentro del propio código como una expresión (sin el típico class…). Por tanto es una clase anónima y además interna (inner).

La pega está en que las clases anónimas internas (inner anonymous classes) tiene siempre una referencia a la instancia que alberga esa clase, aunque no se use nunca.

En este código del post de ejemplos de Guava podemos verlo:

@Test
public void addSufix_UsingGuava() {
	final Function<String, String> addSufix = new Function<String, String>() {

		@Override
		public String apply(final String word) {
			return word.concat(SUFIX);
		}

	};
	final List<String> listOfStringsWithSuffix = FluentIterable.from(listOfStrings).transform(addSufix).toList();

	assertTrue(listOfStringsWithSuffix.get(0).endsWith(SUFIX));
}

En este código, addSufix es un objeto proveniente de una clase interna anónima… Mira qué pasa si paramos el debugger y vemos su interior:

guava1

Tiene referencia a otras tres listas que están definidas en la clase que está declarada, es decir, la clase que la alberga.

He aquí el foco del problema: en nuestro ejemplo real con el que he comenzado el post, no eran simplemente 3 listas, que pueden tener más o menos tamaño… La clase que albergaba la función anónima era un servicio de Spring, que tenía además un cliente de Apache CXF inyectado con Spring (en singleton, claro), que tiene decenas y decenas de objetos de configuración en su interior.

El resultado es que al final, la lista devuelta por el Lists.transform de Guava distaba mucho de ser un simple ArrayList con 3 Strings, sino que era un árbol de objeto con miles y miles de objetos relacionados los unos con los otros. Y como se usaba como key de EhCache, éste se dedicaba a hacer el hash de miles de objetos que componían la clave, descartando el proceso por volumen desmedido y haciendo que fallase la caché (si no fallase, es poco probable que el Hash se pudiese repetirse al haber tantos miles de objetos).

La solución paso en este caso por evitar en este caso la declaración de la función con una clase anónima interna, usando por ejemplo:

  • Una clase estática dentro de la clase padre, que tiene sentido por sí misma y no le hace falta la referencia a la clase que la alberga
  • También se podría haber devuelto dentro de un método estático que no tiene referencia a objeto padre.
  • O haber incluido un paso intermedio de transformación de la lista de Guava a una lista de tipo ArrayList.

… y todo esto por fiarnos ciegamente de las librerías en las que nos apoyamos y suponer ciertos comportamientos sin entrar en los detalles.


4.Conclusiones

En este tutorial hemos visto un caso real que nos sucedió por suponer que el comportamiento de una librería que usábamos era el más adecuado. En realidad provocó un problema que costó varias horas en resolverse, y que podría haber causado problemas en producción.

La moraleja es que debemos conocer el comportamiento de las librerías en las que nos basamos, aunque si bien quizá sea imposible, al menos no fiarnos nunca de ellas: debemos tenerlas más cerca que a nuestros propios enemigos

PS: gracias a Alejandro Ortiz, que fue capaz de descubrir el problema descrito en este post.

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad