Skip to content

java

Spring Cloud Netflix (1)

Todas las empresas desarrollan una o varias actividades que representan su fuente principal de ingresos. Y esos ingresos se dividen en partidas de dinero que deben dedicarse a gastos e inversiones. Para las empresas de gran tamaño una de esas partidas habitualmente se destina al mantenimiento de sus sistemas de información. Sistemas que tradicionalmente han gestionado en una infraestructura propia. Sus propias máquinas, sus propias aplicaciones, sus propios equipos, sus propios problemas.

Para algunas empresas la información es parte fundamental de su actividad. Mantener esa información en un entorno propio les resulta algo natural. No obstante, se plantea la cuestión de si realmente necesitan hacerse responsable de gestionar absolutamente todos sus sistemas. No sólo los de misión crítica, sino los de uso más común. El esfuerzo humano, material y económico necesario para mantener en funcionamiento toda una infraestructura propia puede llegar a ser bastante elevado.

Desde hace más de dos décadas, gigantes como Amazon, Google y Microsoft comenzaron a ofrecer sus infraestructuras para que pudieran ser explotadas por otras empresas. Una oferta que se ha materializado en distintos servicios. Servicios de almacenamiento, servicios de computación, servicios de aplicaciones, … La idea es que las empresas no tengan que invertir en sistemas virtuales, distribuidos, replicados, escalables, tolerantes a fallos y con alta disponibilidad. Conceptos técnicos todos ellos que no se encuentran habitualmente dentro del dominio de su negocio.

Aunque el paradigma ha ido evolucionando con los años, por lo general se acepta que si un sistema o información no se encuentra alojado dentro de una infraestructura propia se dice que está en la nube. Si se almacena un archivo, como una fotografía, en un servidor ajeno se dice que está en la nube. Si se crea un fichero, como un documento, a través de una aplicación alojada en un servidor ajeno se dice que se usa un servicio en la nube. Si se ejecuta una aplicación propia en un servidor ajeno se dice que usa la infraestructura de la nube. Y en general, simplificando bastante, si se realiza cualquier acción que implica el acceso a un servidor ajeno a través de una red se considera que se está trabajando en la nube. Entendiendo en todo caso que dicho acceso se realiza normalmente a través de Internet, e independientemente de que algunas empresas mantengan grandes infraestructuras propias que denominan nubes privadas.

Una de las razones que motivó la aparición de la nube fue la necesidad de disponer de una gran capacidad de cómputo por parte de las empresas que empezaban a ofertar servicios para millones de potenciales clientes conectados a un mismo tiempo. Las empresas necesitaban arquitecturas con una alta escalabilidad horizontal bajo demanda utilizando las capacidades de una infraestructura externa ofrecida por algún proveedor. Las soluciones que surgieron de esa necesidad originó la creación de arquitecturas que se han popularizado hasta convertirse en el marco de referencia para el desarrollo de aplicaciones hoy en día. Evolucionando desde aplicaciones monolíticas basadas en base de datos relaciones hasta arquitecturas de microservicios y base de datos NoSQL.

Una de las compañías que tuvo que acometer la tarea de desarrollar una arquitectura en la nube fue Netflix, el popular servicio de streaming de series, películas y documentales. El código fuente de los servicios básicos que componen su plataforma desarrollada en Java se encuentra disponible de forma abierta bajo licencia Apache 2.0. Sus soluciones se desarrollaron en una época temprana y con unas necesidades muy concretas para la infraestructura de Amazon sobre la que se ejecuta, por lo que no reflejan el estado actual del desarrollo de microservicios ejecutados sobre contenedores. No obstante, la plataforma abierta de Netflix supone un punto de entrada muy interesante para los desarrolladores que quieran conocer el principio de funcionamiento de este tipo de arquitecturas.

Netflix Open Source Software (Netflix OSS) es el conjunto de librerías y herramientas desarrollados por Netflix que posibilita la ejecución de servicios en la nube. Proporciona una serie de componentes básicos que exponen todas las características esperadas de un entorno en la nube.

Spring Cloud es un framework desarrollado en Java por Spring que expone los patrones comunes implementados por los sistemas distribuidos. Proporciona tanto una serie de capas de abstracción a través de interfaces genéricas como una serie de implementaciones concretas de dichas interfaces con librerías propias o de terceros.

Spring Cloud Netflix es una implementación de las interfaces propuestas por Spring Cloud utilizando las librerías de Netflix OSS y Spring Boot.

El objetivo de todos estos frameworks es que los desarrolladores puedan montar un sistema de servicios distribuidos ejecutándose sobre una o más máquinas. Permiten a los desarrolladores centrarse en la lógica de negocio y no en los detalles de la infraestructura necesaria para la ejecución de los servicios. Entendiendo por servicio una aplicación propia, preferentemente estructurada en microservicios. Es decir, dividida en componentes que realizan una única tarea dentro de un dominio específico y que pueden ser invocados por el resto de servicios independientemente del lenguaje de programación en el que se encuentren implementados.

En la práctica, de cara a explotar un conjunto de servicios, se consideran necesarias, o al menos deseables, una serie de características. Estas características tienen como propósito facilitar la configuración, despliegue, comunicación y monitorización de dichos servicios. El enfoque de Spring Cloud Netflix al respecto es que nos responsabilicemos de tener una aplicación distinta arrancada, o varias instancias de las mismas, para cubrir cada una de estas características. Además de responsabilizarnos de que nuestras aplicaciones se comuniquen con ellas para obtener provecho de dichas características. Es decir, somos responsables de todo el software, tanto de infraestructura como de nuestros propios servicios. Spring Cloud Netflix proporciona las aplicaciones necesarias para el servidor y las librerías necesarias para que nuestros servicios se comuniquen con ellas.

Algunas de las características cubiertas por Spring Cloud Netflix son las siguientes:

– Configuración. Cuando se levanta un servicio es habitual que necesite una serie de parámetros de configuración locales propios. La cuestión no es tanto que estos parámetros se distribuyan como parte del servicio, sino que se puedan establecer y modificar, incluso en tiempo de ejecución, para facilitar la explotación del sistema.

Spring Cloud proporciona un servidor de configuración propio que denomina Spring Cloud Config. Y Netflix OSS por su parte proporciona Archaius, su propio servidor de configuración. Utilizar uno u otro es indistinto, Spring Cloud Netflix actúa como puente presentando a las aplicaciones una misma interface para ambos.

– Descubrimiento. En un sistema distribuido se debe conocer el número de instancias levantadas de un servicio, y un servicio debe poder invocar una instancia de otro servicio independientemente de donde se encuentre ejecutándose.

Netflix OSS proporciona Eureka, un servidor de descubrimiento de servicios. Cuando un servicio se levanta lo notifica a Eureka, que lleva el registro de los servicios levantados y permite que los servicios puedan invocarse los unos a los otros a través de su registro.

– Enrutamiento. Los servicios propios de las aplicaciones normalmente son expuestos públicamente a través de determinadas rutas. Rutas que no deben venir impuestas por la infraestructura donde se encuentren en ejecución.

Netflix OSS proporciona Zuul, un servidor que, en otras muchas cosas, permite definir las rutas en las que los servicios son publicados. Cuando un servicio se invoca, Zuul resuelve la petición redirigiéndola a alguno de los servicios registrados en Eureka.

– Monitorización. En un sistema distribuido con múltiples servicios sobre múltiples máquinas es importante disponer de herramientas que muestren el estado en tiempo real de todos ellos.

Netflix OSS proporciona Turbine, una aplicación cliente que muestra en un cuadro de mando el estado de distintas métricas. Se puede configurar fácilmente para que muestre el estado de los distintos servicios registrados en Eureka. Aunque técnicamente es un agregador de streams de Server-Sent Event (SSE), por lo que es bastante abierto.

Por último, comentar que hoy en día el enfoque de Spring Cloud Netflix, que obliga a los desarrolladores a conocer todos los detalles del funcionamiento del servidor y ligar sus aplicaciones con un framework concreto, ha evolucionado hacia arquitecturas en las que los desarrolladores no tienen que responsabilizarse del software del servidor ni añadir dependencias de terceros en las aplicaciones propias, permitiendo centrarse exclusivamente en la lógica de negocio.

Collectors en Java

Las clases que implementan la interface Collector de Java son las encargadas de convertir en conjuntos finitos de información las secuencias de elementos, potencialmente infinitas, generadas por los streams. Son operaciones terminales de reducción dentro de la cadena de procesamiento de un stream. Acumulan los datos producidos y los almacenan en algún tipo de colección, o generan un único valor a partir de ellos. Se utilizan por ejemplo para crear listas a partir de streams, o calcular valores máximos o mínimos. En la práctica son más conocidas por “¡Ah, eso que se pone al final! La cosa esa del .collect(Collectors.toList())” (sic).

Los recolectores fueron introducidos en Java 8 y han sido ampliados en las versiones 9 y 10. La interface básica es java.util.stream.Collector<T, A, R>, que ofrece cuatro métodos básicos con los que se construyen los recolectores. Estos métodos hacen uso del concepto de interface funcional de Java y no implementan las acciones directamente, sino que retornan una función que es la que realmente implementa la acción. Es como ir a una ventanilla para hacer una gestión y que te respondan “sí, es aquí, pero vaya a esa otra ventanilla”.

Las funciones básicas expuestas por los recolectores son muy genéricas, indican cómo construir un contenedor intermedio, cómo añadir elementos a dicho contenedor, cómo combinar dos contenedores para generar un resultado parcial, y cómo calcular el resultado final a partir de los parciales:

  • Supplier<A> supplier(): Este método retorna una función que crea un contenedor intermedio de tipo A. Un contenedor puede ser de cualquier clase que se quiera. No tiene que implementar ninguna interface concreta. Puede ser una Collection clásica de Java, pero también puede ser un StringBuilder por ejemplo.
  • BiConsumer<A, T> accumulator(): Este método retorna la función que acumula elementos de entrada del tipo T en un contenedor intermedio de tipo A.
  • BinaryOperator<A> combiner(): Este método retorna la función que combina dos contenedores intermedios de tipo A y crea un resultado parcial también de tipo A.
  • Function<A, R> finisher(): Este método retorna la función que calcula el resultado final de tipo R a partir de los resultados parciales de tipo A. El tipo R no tiene que implementar ninguna interface concreta.

Una forma muy directa de implementar un recolector es utilizar uno de los dos métodos estáticos of de la interface Collector. Estos métodos admiten como parámetros de entrada las cuatro funciones básicas y las características del recolector. Siendo estas características una combinación de valores del enumerado Collector.Characteristics. Donde el valor CONCURRENT indica que el proceso de recolección puede ejecutarse de forma concurrente, el valor UNORDERER indica que el resultado no depende del orden original de los elementos de entrada, y el valor IDENTITY_FINISH indica que la función de finalización puede omitirse retornándose directamente el contenedor intermedio. Es el mismo principio de definición de características usado para los Spliterators, nombre inspirado seguramente en alguna de las películas de Parque Jurásico.

A modo de ejemplo, el recolector del código siguiente se crea especificando directamente las funciones como parámetros del método Collector.of. Su propósito es retornar la suma de todos los elementos de tipo Integer de una fuente de entrada. La primera función crea un array con un entero que se puede utilizar como contenedor intermedio, la segunda función suma a un contenedor intermedio un elemento de entrada, la tercera función suma al contenido de un contenedor el contenido de otro, y la cuarta función retorna el resultado final de un contenedor.

El recolector anterior puede aplicarse directamente en un stream de enteros para obtener la suma de todos los elementos:

Como es de suponer, los recolectores se pueden crear de una manera más tradicional definiendo una clase que implementa la interface Collector, sin necesidad de utilizar expresiones lambdas como en el código de ejemplo anterior. No obstante, normalmente no suele ser necesario escribirlos, ya que Java proporciona de forma nativa implementaciones de una gran cantidad de recolectores de uso habitual a través de métodos estáticos de la clase java.util.stream.Collectors.

Los recolectores más comunes de la clase Collectors son los que crean colecciones, como los clásicos métodos toList, toSet y toMap que existen desde la versión 8 de Java, más los que se añadieron en la versión 10 como toUnmodifiableList, toUnmodifiableSet y toUnmodifiableMap, alguno más específico como toConcurrentMap, o el más genérico toCollection:

Otros recolectores de gran utilidad ofrecidos por Collectors son los que realizan reducciones, es decir, calculan un único resultado a partir de los elementos de entrada. Ejemplos de este tipo de recolectores son counting, que cuenta el número de elementos, joining, que concatena todos los elementos como una cadena de texto,  maxBy y minBy, que calculan el máximo y mínimo, summingInt, summingLong y summingDouble, que calculan la suma de los elementos, y averagingInt, averagingLong y averagingDouble, que calculan la media de los elementos. Un caso particular es summarizingInt, summarizingLong y summarizingDouble, que calcula todos los valores anteriores a un mismo tiempo, es decir que retorna un objeto que contiene el total de elementos, el máximo, el mínimo y la media.

Otro tipo interesante de recolectores son los de agrupación, que como su propio nombre indica permiten agrupar los elementos de entrada en base a un determinado criterio. La salida de estos recolectores es un Map, donde la clave es el criterio de agrupación y el valor la colección de elementos agrupados.

La clase Collectors  expone más factorías de recolectores aparte de las aquí citadas, por lo que es recomendable echar un vistazo a la documentación del API.

Como nota final, comentar que he decidido traducir collector como recolector, en vez de dejarlo en el inglés original, como se suele hacer con otras palabras, como stream por ejemplo, por la similitud con la palabra en español, pero evitando traducirlo directamente como colector, que para mí tiene otro sentido.

¡Feliz día de la recolección!

Recorridos aleatorios en Java

La forma habitual de desordenar una lista en Java es utilizar el método Collections.shuffle. El inconveniente de este método es que modifica la lista original, un efecto lateral no deseable en el mundo actual donde predominan las colecciones inmutables. La alternativa más obvia es crear una nueva lista, a costa lógicamente de duplicar el consumo de memoria.

Un enfoque distinto es recorrer la lista de forma desordenada. La premisa de tal tipo de recorrido es que se retornen todos los elementos de la lista de manera aleatoria y que cada uno de ellos sólo se retorne una única vez. La condición es que la lista sea de tamaño fijo, preferentemente inmutable; lo contrario podría dar lugar a implementaciones con comportamientos indefinidos no predecibles. Y la restricción es que la operación se ejecute de forma secuencial, no paralela, ya que realizar particiones de la lista no permitiría acceder a todos los elementos de manera aleatoria.

Una implementación de tal tipo de recorrido implica generar un número aleatorio en cada iteración que indique el índice del elemento a retornar. Además de almacenar todos los índices generados para evitar duplicados. Para ello, para una lista de tamaño   n, se puede crear un array de n índices, inicializar dicho array secuencialmente con valores de  a n, en cada iteración i generar un número aleatorio r de i a n - 1, intercambiar en el array el valor en la posición  i por el valor en la posición  r, y retornar el elemento de la lista que se encuentra en la posición indicada por el valor almacenado en la posición  i del array. Es decir, sustituir el elemento de la iteración en curso por otro de forma aleatoria y que aún no se haya retornado.

Partiendo de que en la actualidad se prefiere consumir colecciones con streams, en vez de con bucles for, la solución pasa por la implementación de un iterador, y de forma más concreta de un Spliterator<T>. Esta clase se introdujo con Java 8 para facilitar la construcción de iteradores complejos que pudieran consumir elementos tanto de forma secuencial como paralela, además de tener la capacidad de describirse a ellos mismos indicando cuales son sus características.

SplitIterator<T> es una interface que requiere implementar varios métodos, pero para la mayoría de los casos de uso, que no requieran soportar paralelismo, se puede simplemente extender de Spliterators.AbstractSpliterator<T>. Esta clase sólo requiere implementar el método boolean tryAdvance(Consumer<? super T> action). Un método que es equivalente a los clásicos métodos hasNext y next de los iteradores de Java, ya que a un mismo tiempo consume el elemento en curso e indica si quedan más elementos por consumir.

Una posible implementación de tal algoritmo podría ser la siguiente:

En el código, elements almacena la lista original y size su tamaño,  indexes es el array de índices recorridos y current el número de la iteración en curso. El método tryAdvance consume el elemento en curso, y el método next realiza el proceso de generación del número aleatorio y actualización del array de índices. El código debería resultar bastante sencillo de entender con los comentarios.

Notar el uso de la clase SplittableRandom para la generación de números aleatorios. Esta clase se introdujo en Java 8, junto con SecureRandom cuando la aleatoriedad de la secuencia de números generados deba ser más estricta y cumplir criterios más estrictos como los requeridos en el mundo de la criptografía.

El iterador del código de ejemplo se puede probar creando una lista, una instancia del iterador, y procesando el conjunto como un stream con StreamSupport.stream:

En cada ejecución la salida será distinta, devolviéndose los elementos de la lista original en un orden aleatorio cada vez.

La variable characteristics establece las características de la lista para facilitar a los clientes del iterador la toma de decisiones. IMMUTABLE indica que es inmutable, SIZED que tiene un tamaño prederminado, y NONNULL que no contiene nulos.  De la forma acostumbrada, es recomendable revisar la documentación para ver todas las opciones disponibles. En la práctica sería recomendable diseñar el iterador para un determinado caso de uso evitando tener que pasar dichos parámetros cada vez.

De cualquier forma, la implementación propuesta funciona, pero es muy básica. Por ejemplo, el array de índices se inicializa a costa de recorrerlo completamente, algo que puede evitarse tratando el cero como caso especial, ya que ese el valor que por defecto asigna Java a las variables de tipo int. Si un índice es cero entonces se puede interpretar como que no se ha visitado todavía y que su valor es la propia posición que ocupa, ya que ese es el valor con el que se debería haber inicializado originalmente:

De igual forma sería interesante pensar en reutilizar los arrays de índices cuando se procesen múltiples listas del mismo tamaño de forma secuencial, para evitar la reserva de memoria necesaria para crear dichos arrays por cada lista. Para ello se podría utilizar una variable que almacenase el valor del índice utilizado como caso especial, en vez utilizar el valor constante cero. Al guardar un índice en el array se tendría que almacenar sumándole el valor de dicha variable y compararlo también contra dicho valor. Así, en el primer uso del array el valor de la variable sería cero y el funcionamiento sería el mismo que el del código anterior. Pero al terminar el primer recorrido, y posteriores, se le sumaría a la variable el tamaño de la lista recorrida. En el segundo recorrido, y posteriores, los índices se almacenarían sumándoles dicho valor y se compararían contra dicho valor. Si el índice es menor que dicho valor entonces se consideraría que no se ha visitado. Por seguridad, cada cierto número de recorridos se podría inicializar el array y la variable a cero.

Por último, comentar que en el caso de realmente querer recorrer una lista de forma aleatoria en paralelo, una posible solución sería generar primero el array con los índices desordenados, y luego tratar en paralelo este array de forma particionada, en vez de particionar la lista como normalmente se suele hacer.

Benchmarks en Java con JMH

JMH (Java Microbenchmark Harness) es una herramienta para realizar benchmarks en Java. Se desarrolla como parte de OpenJDK y basa su funcionamiento en Maven. Su propósito es el de medir dos implementaciones distintas de un mismo diseño. Lo que en la práctica quiere decir que se utiliza para escribir dos métodos y ver cuál de ellos es más rápido. JMH tiene en cuenta muchos aspectos del funcionamiento interno de la máquina virtual de Java y se asegura de que el código probado se pruebe correctamente, algo no tan sencillo como a priori pudiera parecer.

Hacer un bucle que ejecute un millón de veces un método e imprimir al principio y final la hora para ver el tiempo transcurrido no sirve para nada. La máquina virtual de Java puede decidir optimizar el código y eliminarlo completamente de la ejecución si concluye que realmente no realiza ninguna operación efectiva. El tiempo medido en ese supuesto sería extremadamente pequeño, pero dentro del contexto de un programa real, donde el código sí que se ejecutase, podría ser muy elevado. Herramientas como JMH hacen todo lo posible para que las pruebas sean fiables.

Para realizar pruebas con JMH hay que generar un proyecto con Maven, preferentemente desde línea de comandos, aunque también se pueden generar desde un IDE. Lo que si se recomienda es utilizar la línea de comandos para ejecutarlos y no añadir ningún componente externo que pueda provocar efectos laterales durante la ejecución de las pruebas.

El comando básico de creación de un proyecto a partir del arquetipo de Maven para JMH es el siguiente:

Como resultado de la ejecución se crea un proyecto con dos ficheros dentro de un nuevo directorio. El primero de ellos es el clásico pom.xml con la configuración del proyecto Maven, y el segundo una clase de ejemplo que puede utilizarse como plantilla para crear benchmarks. El proyecto generado se puede compilar para crear un jar y ejecutarlo con los siguientes comandos dentro del nuevo directorio creado:

Si se está utilizando Java 9 o superior puede resultar en un error java.lang.NoClassDefFoundError: javax/annotation/Generated debido al nuevo sistema de módulos de Java. Este error se puede resolver configurando un parámetro en la máquina virtual de Java que ejecuta Maven, más concretamente indicando que cargue el módulo que contiene la clase que no se encuentra:

Como resultado de la ejecución del benchmark se muestra por consola el progreso de la prueba, que consiste en varias ejecuciones de un proceso de precalentamiento de la máquina virtual, necesario para llevarla a un estado lo más predecible posible, varias ejecuciones de la clase de prueba, y finalmente las estadísticas resultantes.

Se pueden consultar todas las opciones disponibles para la ejecución del benchmark añadiendo el clásico parámetro -help en la línea de comandos con  java -jar target/benchmarks.jar -help. Y es recomendable hacerlo las primeras veces para hacerse una idea de cómo se puede personalizar la ejecución.

Lógicamente la ejecución anterior con las opciones por defecto de una clase de ejemplo que se limita a probar un método vacío no aporta ningún valor. La idea es que se añada dentro del pom.xml la dependencia con el código que se quiera probar y se escriban clases que prueben dicho código. JMH proporciona en su documentación ejemplos de cómo escribir clases de prueba.

La escritura de clases de prueba para JMH se realiza mediante el uso de anotaciones de forma similar a como se realiza en otros frameworks.

  • @Benchmark es la anotación básica que debe aplicarse sobre los métodos públicos que se quieran probar.
  • @BenchmarkMode permite especificar el modo de realizar la prueba, pudiéndose elegir entre Throughput, que llama continuamente a los métodos probados y mide el rendimiento total, AverageTime, que llama continuamente a los métodos probados y mide el tiempo medio de ejecución, SampleTime, que llama continuamente a los métodos probados y mide el tiempo de ejecución tomando muestras aleatorias de la duración de los mismos, SingleShotTime, que llama una sola vez a los métodos probados y mide el tiempo de ejecución sin enmascarar la penalización por el precalentamiento, y All, que ejecuta todos los modos.
  • @OutputTimeUnit permite indicar la unidad de tiempo en que se quiere mostrar los resultados, pudiéndose elegir cualquier valor del enumerado java.util.concurrent.TimeUnit.
  • @Measurement y @Warmup permiten controlar el proceso de prueba y precalentamiento respectivamente mediante parámetros tales como el número de iteraciones a realizar o la duración de las mismas. Otras anotaciones como @Fork, @Threads, @CompilerControl y @Param controlan detalles de más bajo nivel referentes a la creación de hilos de ejecución y la compilación de las clases de prueba.
  • @State se utiliza para crear clases que se puedan pasar como argumentos a los métodos marcados con @Benchmark. En la anotación se debe indicar el ámbito de los objetos creados, pudiéndose elegir entre Thread, para que todas las instancias sean distintas, Benchmark, para que todas las instancias de una misma clase se compartan entre todos los threads, o Group, para que todas las instancias de una misma clase se compartan entre los threads de un grupo. Los grupos se definen con las anotaciones @Group y @GroupThread que permiten probar varios métodos a la vez en una misma iteración.
  • @Setup y @Teardown permiten ejecutar procesos de inicialización y finalización de una clase anotada con @State. De hecho, sólo se pueden utilizar sobre métodos de clases anotadas con @State. Se invocan en función de un argumento de las propias anotaciones, pudiéndose elegir entre Trial, para que se invoquen por cada prueba, Iteration, para que se invoquen en cada iteración de las pruebas, e Invocation, para que se invoquen en cada ejecución del método, aunque este último no se recomienda sin leer la documentación y entender sus implicaciones.

Muchas de las anotaciones son heredables, por lo que es sencillo escribir clases base de las que heredar el comportamiento y crear suites de pruebas. O clases concretas para realizar pruebas más específicas, como en el siguiente ejemplo:

Como se observa, los métodos de prueba no deben contener bucles, sólo la invocación a la funcionalidad que se quiera probar. JMH ya realiza las iteraciones pertinentes. No obstante, si fuera realmente necesario por alguna razón técnica hacer un bucle que invoque a la funcionalidad que se quiere probar, se puede utilizar la anotación @OperationsPerInvocation(n) sobre el método que contiene el bucle para indicar que dicho método realiza n iteraciones.

Otro detalle a tener en cuenta es que resulta conveniente retornar algún valor desde los métodos de prueba, para evitar que la máquina virtual considere el código como muerto y lo optimice eliminando su ejecución. De igual forma, si el valor retornado por el método de prueba no depende del estado de ningún objeto y el resultado es predecible, la máquina virtual puede optimizarlo reemplazándolo por un valor constante, o eliminándolo completamente si concluye que no hay ningún proceso posterior que lo utilice. Para evitar estos y otros problemas se debe utilizar la clase BlackHole de JMH, que consume todo tipo de objetos y hace todo lo posible por garantizar que la prueba sea lo más fiable posible.

Para terminar, comentar además otras dos características interesantes que ofrece JMH. La primera de ellas es un API para Java que permite crear programas que ejecuten el propio JMH desde un main. Y la segunda una serie de profiles, para pruebas más sofisticadas, que permiten controlar aspectos tales como el tiempo consumido por el compilador JIT (Just In Time) o el Garbage Collector, por citar sólo algunas de sus posibilidades.

Reactive Streams en Java (y 3)

Implementar un stream que cumpla todos los requerimientos exigidos por la especificación de Reactive Streams requiere bastante atención a los detalles. Puede llegar a ser complicado verificar que todas y cada una de las reglas de la especificación se cumplen. Afortunadamente, el mismo grupo de trabajo que desarrolla la especificación también ofrece Reactive Streams Technology Compatibility Kit (TCK), una herramienta que prueba implementaciones de reactive streams y verifica si cumplen las reglas.

TCK prueba prácticamente todas las reglas, pero no todas, ya que hay unas pocas reglas que no pueden probarse de forma automatizada debido a la naturaleza de las mismas, como por ejemplo comprobar que un productor realmente genera infinitos elementos. En todo caso, TCK prueba todas las reglas importantes y es de gran ayuda para los desarrolladores.

TCK es una librería de código abierto desarrollada en Java disponible en Maven. No obstante, hay que tener en cuenta que existen dos versiones de la librería, una anterior a Java 9 y otra posterior. La anterior permite probar streams que implementen las interfaces propuestas originalmente por el grupo de trabajo que desarrolla la especificación. La posterior permite probar las nuevas interfaces del paquete java.util.concurrent.Flow ofrecidas de forma nativa por Java 9 y lógicamente es la que se recomienda utilizar hoy en día.

Para incluir TCK en un proyecto basta con añadir la dependencia de Maven correspondiente en el fichero pom.xml de la forma acostumbrada:

TCK ofrece cuatro clases base de las que se puede extender para escribir pruebas basadas en TestNG. La clase PublisherVerification<T> se debe utilizar para probar publicadores. Las clases SubscriberWhiteboxVerification<T> y SubscriberBlackboxVerification<T> se deben utilizar para probar suscriptores, siendo recomendable utilizar la primera, aunque implique más esfuerzo, ya que es capaz de probar algunos casos que la segunda no puede. Y por último, la clase IdentityProcessorVerification<T> que debe utilizarse para probar procesadores. La documentación es detallada y cubre bastantes aspectos del uso de la librería.

Como ejemplo, partamos de la siguiente implementación de un publicador que no hace absolutamente nada:

Para probar el publicador se debe escribir una clase que extienda de PublisherVerification<T>. En el constructor se debe inicializar el entorno de ejecución creando una instancia de TestEnviroment, que permite especificar algunos parámetros, como por ejemplo timeouts o el nivel de detalle del log. Y se debe implementar dos métodos, createPublisher, para crear una instancia del publicador que se quiere probar, y createFailedPublisher, para crear una instancia del publicador que se quiere probar en condiciones específicas de error, aunque esto último se puede ignorar retornando null  desde el método.

La forma de ejecutar las pruebas con TestNG depende del entorno en el que se esté trabajando. Desde Eclipse basta con instalar un plugin desde el Marketplace, que añade opciones que permiten ejecutar directamente las clases de prueba. No obstante, TCK depende de una versión muy antigua de TestNG, por lo que es necesario excluirla y añadir una versión más moderna para que el plugin funcione:

En buena lógica, el resultado de la ejecución de la prueba sobre el productor de ejemplo, que no implementa nada, retorna una gran cantidad de errores:

Los errores devueltos por TCK son de la forma TYPE_spec###_DESC. Donde TYPE puede ser required, optional, stochastic o untested, e indica si la regla es obligatoria, opcional, no verificable, o no implementada. ### es el número de regla, siendo el primer número el apartado de la especificación y los dos últimos el número de la regla dentro del apartado. Y DESC es una breve descripción de la regla.

La implementación original de nuestro productor de ejemplo se puede mejorar implementando una suscripción y retornándola cuando un suscriptor lo solicite, aunque sin implementar aún nada realmente:

Si se vuelve a ejecutar la prueba, se observa como el número de errores se reduce:

Si se estudia la lista de pruebas ejecutadas con éxito se puede observar que algunas pasan de casualidad debido a algún efecto lateral. Por ejemplo, la prueba required_spec109_subscribeThrowNPEOnNullSubscriber se ejecuta con éxito porque el método utiliza directamente el suscriptor recibido, sin comprobar si es nulo, y eso hace que se eleve un NullPointerException (NPE) tal y como exige la regla. La solución obvia es comprobar que el parámetro no es nulo antes de utilizarlo:

El código se puede seguir evolucionando para hacer que implemente más reglas de la especificación, como por ejemplo la relativa a la prueba optional_spec309_requestNegativeNumberMaySignalIllegalArgumentExceptionWithSpecificMessage que exige que se debe señalizar un error con una instancia de IllegalArgumentException si se solicitan cero o menos elementos:

Llegado este punto ya debería quedar la clara la utilidad de TCK, que no sólo se limita a facilitar las pruebas, sino que ayuda a delimitar la funcionalidad pendiente de implementar para cumplir con los requerimientos de la especificación de Reactive Streams. El propio repositorio de código de TCK incluye unas cuantas clases de ejemplo donde se puede observar la complejidad necesaria para cumplir con algunas de las reglas.

En líneas generales, implementar un reactive stream no se considera una tarea abordable sin un estudio previo de la especificación y el conocimiento de aspectos concretos del funcionamiento de Java, como por ejemplo la sincronización de procesos concurrentes. El propio Java proporciona la clase SubmissionPublisher<T> para facilitar la creación de publicadores asíncronos, y se recomienda su utilización si cubre el caso de uso que se debe implementar en vez de realizar un desarrollo partiendo completamente de cero.