Quantcast
Channel: Adictos al trabajo
Viewing all 996 articles
Browse latest View live

Promociona entre entornos desde Jenkins, si cumples unos mínimos de calidad

$
0
0

En este tutorial veremos como configurar la promoción entre entornos desde jenkins haciendo uso del plugin de promotion y algún aspecto avanzado adicional como bloquear una promoción si el proyecto no pasa los mínimos umbrales de calidad definidos.

Índice de contenidos.


1. Introducción

El objetivo de este tutorial es mostrar como, realizando una configuración muy básica y sin entrar en muchos detalles podemos configurar jenkins para realizar una promoción automática o manual entre entornos de los artefactos generados dentro del ciclo de integración continua.

El flujo es sencillo:

  • el equipo consolida el código fuente en el repositorio de SCM,
  • automáticamente se lanza una build en el entorno de CI que después de comprobar la integridad de la build, esto es:
    • el código compila
    • los tests, unitarios y de integración, pasan,
    • y se generan las métricas de calidad,
    se genera un artefacto.
  • el equipo decide promocionar al entorno de despliegue integración una build concreta generada, que haya pasado por todo el ciclo de integración, incluyendo el hecho de cumplir con unos mínimos de calidad definidos en sonar.

El entorno de despliegue de integración puede ser cualquier contenedor de aplicaciones o incluso la misma aplicación autodesplegada; en esencia es un entorno controlado e independiente en el que el equipo debería realizar las demos al usuario.

2. Entorno.

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 15′ (2.5 GHz Intel Core i7, 16GB DDR3).
  • Sistema Operativo: Mac OS El Capitan 10.11
  • Jenkins 2.7.3
  • Sonar 6.0

3. Ejecución de jenkins en un entorno local.

Paso a paso y haciendo uso de docker nos descargamos primero la última imagen del producto:

docker pull jenkins:alpine

Una vez descargada, el siguiente paso es arrancar un contenedor basado en esa imagen, proporcionando un nombre descriptivo:

docker run -it --name jenkins -p 8080:8080 -p 50000:50000 jenkins:alpine bash

Si accedemos a la ip de la máquina por el puerto 8080, http://192.168.99.100:8080/ veremos una pantalla como la que sigue:

jenkins-promotion-plugin-01

Esto es nuevo de la versión 2 y lo han hecho para asegurar que quien accede la primera vez al sistema y lo configura es el administrador que lo está instalando. La contraseña la podemos encontrar en la consola del arranque del propio jenkins:

INFO:

*************************************************************
*************************************************************
*************************************************************

Jenkins initial setup is required. An admin user has been created and a password generated.
Please use the following password to proceed to installation:

fc81be5b476449dca638cdeab7285407

This may also be found at: /var/jenkins_home/secrets/initialAdminPassword

*************************************************************
*************************************************************
*************************************************************

Una vez añadimos la clave, nos mostrará la siguiente ventana para instalar los plugins recomendados o los que elijamos:

jenkins-promotion-plugin-02

Tanto con una opción o la otra nos mostrará una interfaz con el progreso de la instalación de los plugins:

jenkins-promotion-plugin-03

Una vez concluya el proceso, nos pedirá que demos de alta un primer usuario:

jenkins-promotion-plugin-04

Después de lo cuál, ya tendremos Jenkins preparado para hacer nuestras pruebas:

jenkins-promotion-plugin-05

Si accedemos al host por el puerto correspondiente http://192.168.99.100:8080/ se mostrará la interfaz de login:

jenkins-promotion-plugin-14

4. El plugin en promociones.

Para instalar el plugin debemos ir a “Administrar Jenkins” > “Administrar Plugins”

jenkins-promotion-plugin-07

y buscar entre los plugins disponibles el plugin de “promoted build plugin”

jenkins-promotion-plugin-15

Lo seleccionamos y pulsamos sobre “Descargar ahora e instalar después de reiniciar”

jenkins-promotion-plugin-16

Se mostrará un mensaje como el siguiente y seleccionado “Reiniciar Jenkins cuando termine la instalación y no queden trabajos en ejecución” se reiniciará automáticamente y se instalará el plugin.

jenkins-promotion-plugin-08

Se mostrará un mensaje como el siguiente:

jenkins-promotion-plugin-09

Una vez reiniciado ya podemos crear una nueva tarea

jenkins-promotion-plugin-06

Introducimos un nombre

jenkins-promotion-plugin-10

y un tipo de proyecto, en nuestro caso un proyecto maven

jenkins-promotion-plugin-11

Pulsando “Ok” podremos acceder a la configuración de la tarea, donde podremos comprobar que aparece una opción de “Promote builds when…”

jenkins-promotion-plugin-12

Podemos indicar:

  • un nombre a la promoción,
  • un icono que identificará una build como promocionada usando este tipo de promoción,
  • un listado separado por comas de las personas que podrán aprobar la promoción,

La promoción se basa en la ejecución de acciones y tenemos las siguientes opciones de configuración:

jenkins-promotion-plugin-13

Seleccionando la ejecución de una línea de comandos (shell) o un comando de windows podemos realizar cualquier acción con el artefacto generado por la build.

La recomendación es crear un script del sistema operativo e invocarlo desde aquí parametrizado. No es buena idea incluir en la opción de ejecución del script todo el código de modo tal que todos los jobs tengan el mismo script; es mejor externalizarlo.

Desde el cuadro de comandos se puede acceder al listado de variables que tenemos disponibles para la ejecución del script (http://192.168.99.100:8080/env-vars.html/) y, con ello, elaborar un script más o menos complejo, depende de lo que necesitemos hacer con el artefacto:

jenkins-promotion-plugin-17

En el siguiente ejemplo hacemos una copia del war de la build concreta al directorio de despliegue de un tomcat en un host remoto (remoteHost)

scp /var/autentia/jenkins/jobs/$PROMOTED_JOB_NAME/builds/$PROMOTED_NUMBER/*$PROMOTED_JOB_NAME/archive/*/$PROMOTED_JOB_NAME/*/*.war remoteHost:/opt/tomcat7/webapps/$PROMOTED_JOB_NAME.war

Ya os digo que la recomendación es llevar el contenido del comando a un script del sistema operativo, no mantener el código desde aquí.

A partir de ese momento, sobre una build en concreto podemos acceder a las opciones de promoción:

jenkins-promotion-plugin-19

Y, si estamos autorizados para ello “Aprobar” la promoción

jenkins-promotion-plugin-20

La build promocionada aparecerá con el icono asociado a la promoción:

jenkins-promotion-plugin-21

Y accediendo al job, podemos ver el histórico de promociones.

jenkins-promotion-plugin-22

5. ¿Podemos bloquear una promoción?.

¿Y si estableciéramos que no se puede promocionar salvo que el código del proyecto supere los mínimos de calidad establecidos en sonar?


5.1. Marcar una build como errónea sino cumple los umbrales de calidad de sonar.

Tenemos una primera opción, que podemos hacer de una manera muy sencilla, que consiste en marcar cualquier build como errónea si no pasa los mínimos umbrales de calidad en sonar. Una build marcada como errónea no se puede promocionar, con lo que tendríamos todo cubierto.

Ya hemos visto como instalar plugins en jenkins, en este caso deberíamos instalar:

jenkins-promotion-plugin-18

Una vez instalado debemos configurar la instancia de sonar asociada a la configuración del plugin de Quality Gates

jenkins-promotion-plugin-23

En la configuración del job podemos añadir una acción de tipo “Quality Gates”:

jenkins-promotion-plugin-24

Y te pedirá indicar la clave del proyecto en sonar

jenkins-promotion-plugin-25

Una vez configurado, en la ejecución de las builds si no pasa el Quality Gate, la build se marcará como errónea.


5.2. Impedir una promoción sino cumple los umbrales de calidad de sonar.

El propio equipo de sonar source no está de acuerdo con la opción anterior, puesto que una cosa es no pasar los mínimos de calidad y otra marcar como errónea una build; el resultado de un Quality Gate debería tenerse en cuenta a la hora de promocionar.

La configuración es igual de simple, con el mismo plugin, en vez de añadir la acción a la build lo añadimos a las acciones de la promoción, anteponiéndola a la de la ejecución de la acción de promoción propiamente dicha.

jenkins-promotion-plugin-26

Con ello, no en la ejecución de la build, sino al intentar promocionar la promoción se marcará como errónea si no pasamos el Quality Gate.

jenkins-promotion-plugin-27

6. Referencias.


7. Conclusiones.

Pues ya tenemos todo el ciclo completo… ¿o no?

Recuerda que en Autentia proporcionamos servicios profesionales de soporte a desarrollo en los que podemos ayudarte a la definición de ecosistemas de desarrollo, integración e inspección continua.

Un saludo.

Jose


¿Por qué todo el mundo habla de Jhipster?

$
0
0
En este tutorial vamos a aprender el uso básico de jhipster y por qué la comunidad java tiene el punto de mira en él.

Índice de contenidos

1. Introducción

Ultimamente en la comunidad de desarrolladores se oye hablar mucho de Jhipster, y en este artículo voy a hacer una pequeña introducción sobre que es Jhipster, en que se basa y vamos a hacer un pequeño ejemplo para ver como funciona.

2. Entorno

El tutorial está escrito usando el siguiente entorno:
  • Hardware: Portátil MacBook Pro Retina 15′ (2.5 Ghz Intel Core I7, 16GB DDR3).
  • Sistema Operativo: Mac OS El Capitan 10.11.6

3. ¿Qué es Jhipster?

En la propia web del projecto dicen: “Jhipster is a Yeoman generator, used to create a Spring Boot + AngularJS project” Y realmente es básicamente eso, un generador de projectos basado en Yeoman que nos creará la base para continuar un projecto basado en el siguiente stack de tecnologías:

Lado cliente:

Modelo SPA (Single page application):
  • Responsive Web Design
  • HTML5 Boilerplate
  • Twitter Bootstrap
  • AngularJS
  • Compatible con IE9+ y navegadores modernos
  • Soporte completo para internacionalización con Angular Translate
  • Soporte opcional Sass para diseño CSS
  • Soporte opcional WebSocket con Spring Websocket
Con el framework de yeoman:
  • Fácil instalación de nuevas librerías JavaScript con Bower
  • Construcción, optimización y recarga en tiempo real con Gulp.js
  • Testing con Karma y PhantomJS
¿Y si el modelo de aplicación de una sola página web no es suficiente para nuestras necesidades?
  • Soporte para el motor de plantillas Thymeleaf, para generar páginas web en el lado del servidor

Lado del servidor:

A complete Spring application:
  • Spring Boot para la configuración de la aplicación de forma sencilla
  • Configuración con Maven o Gradle para la construcción, test y lanzamiento de la aplicación con perfiles de desarrollo y producción (out of the box).
  • Spring Security
  • Spring MVC REST + Jackson
  • Soporte opcional WebSocket con Spring Websocket
  • Spring Data JPA + Bean Validation
  • Actualizaciones de base de datos con Liquibase
  • Soporte para Elasticsearch si deseas tener capacidades de búsqueda por encima de la base de datos
  • Soporte para MongoDB si deseas usar una base de datos NoSQL orientada a documentos en vez de JPA.
  • Soporte para Cassandra si deseas usar una base de datos NoSQL orientada a columnas en vez de JPA.

Preparado para ir a producción:

  • Monitorización con Metrics
  • Cacheo con ehcache (cache local) o hazelcast (cache distribuido)
  • Clusterización de sesiones HTTP opcional con hazelcast
  • Recursos estáticos optimizados (filtro gzip, HTTP headers cacheados)
  • Gestión de logs con Logback, configurable en tiempo de ejecución
  • Pooling de conexiones con HikariCP para un rendimiento óptimo
  • Creación de un WAR estándar o un JAR ejecutable
Como podéis ver es realmente impresionante lo que se promete, así que vamos a ver como lo hace y vamos a pasar a realizar un pequeño ejemplo.

4. Requisitos previos

  • Java 8 jvm
  • Maven o Gradel para la construcción de los proyectos
  • Git
  • Node.js que también nos instala npm que será el gestor de paquetes que utilizaremos para el resto de la instalación

5. Comenzando con la instalación

Lo primero va a ser instalar yeoman con:
sudo npm install -g yo
Tras la instalación de yeoman, pasaremos a instalar Bower, Gulp y el propio Jhipster
sudo npm install -g bower
sudo npm install -g gulp
sudo npm install -g generator-jhipster
Tras instalar la infraestructura básica, ya estamos en disposición de comenzar con nuestro pequeño piloto de jhipster

6. Creando el proyecto

Lo primero que vamos a hacer es crear una carpeta donde tener nuestro proyecto con:
mkdir books_jhipster
Con esto ya tenemos nuestro directorio y creamos el esqueleto del proyecto con:
cd books_jhipster
yo jhipster
Lo que vamos a conseguir con esto es lanzar el asistente del generador de jhipster que nos hará unas preguntas para poder configurar de forma correcta nuestro proyecto. Proceso de creación Las preguntas, son bastante claras si sabes que quieres en tu aplicación, en todo caso en la web oficial de jhipster tenemos una descripción de todas y cada una de las opciones del generador. Ok, en un principio ya tenemos la base de nuestra aplicación. Ahora, vamos a añadir unas entidades al modelo de base de datos. Podemos hacerlo mediante un generador de entidades,
yo jhipster:entity [nombre_de_la_entidad]
Que al igual que el generador del proyecto, nos irá pidiendo información sobre la entidad para acabar creándola. De esta forma podríamos crear todas las entidades y relaciones, pero para este tutorial voy a utilizar una herramienta que también proporciona la gente de jhipster que es JDL Studio, que nos permite crear un esquema de entidades de una forma muy sencilla. Vamos a verlo: JDL con el modelo Como se puede ver he definido dos entidades y una enumeración, sus relaciones e indicado como quiere que se paginen dichas entidades dentro de la aplicación, ahora se exporta a un archivo “.jh” que es el que utilizaremos para crear el esquema en nuestra aplicación con:
yo jhipster:import-jdl jhipster-jdl.jh
Jhipster también nos proporciona mecanismos para crear servicios cuando sea necesario añadir funcionalidad por encima de las necesidades básicas del CRUD con:
yo jhipster:service [nombre_del_servicio]

7. Comprobando y arrancando nuestra aplicación

Con todo lo hecho anteriormente, ya tenemos nuestra aplicación funcional para empezar a trabajar sobre ella, veamos que nos ha generado jhipster y como se arranca la aplicación. Jhipster nos genera todos los elementos básicos de la aplicación tanto del backend como del frontend y los deja preparados para que funcione la aplicación out of the box Captura de pantalla 2016-09-14 a las 9.19.15Captura de pantalla 2016-09-14 a las 9.19.45 Como se puede ver ya está todo preparado para lanzar nuestra aplicación, para lo cual utilizaremos el wrapper de maven que tenemos en el directorio de nuestro proyecto con el comando:
./mvnw
Tras unos instantes ya tendremos nuestra aplicación preparada y dispuesta para ser utilizada Captura de pantalla 2016-09-14 a las 9.26.23

8. Conclusiones

Como hemos podido ver en este rápido tutorial, jhipster nos da la habilidad de tener aplicaciones funcionales con múltiples características que siempre nos gusta tener en nuestras aplicaciones como swagger para el testeo y documentación del api, metrics para el monitoreo de la aplicación… disponibles en muy poco tiempo. Además nos proporciona herramientas para el modelado de entidades, test de integración y unitarios pregenerados y muchas cosas más casi sin esfuerzo, hasta aquí bien. Ahora es cuando viene el pero (no demasiado grande). Lo que viene después es arduo (o no tanto), puesto que nos queda hacer la aplicación nuestra, es decir, darle el diseño deseado, personalizar los comportamiento e implementar los casos de usuario que tengamos definidos. La verdad es que nos permite abstraernos de la parte más tediosa y nos proporciona una arquitectura básica que se cimienta en las últimas y más cacareadas tecnologías que existen en la actualidad. ¿Que os parece este enfoque?

9. Referencias

Crear un fondo animado al estilo de “Pasapalabra” con After Effects CC

$
0
0

Este tutorial explica cómo realizar un fondo de letras animadas con Adobe After Effects CC mediante el uso de la expresión “wiggle”

Índice de contenidos


1. Introducción

Como ya conté en el último tutorial, After Effects nos permite añadir expresiones de programación para automatizar acciones o crear nuevas que no tiene de serie. En este caso, utilizaremos la expresión wiggle.

La expresión wiggle es un comando que nos permite generar valores aleatorios para una propiedad. Esta expresión nos sirve tanto para propiedades unidimensionales (por ejemplo rotación), como para las bidimensionales (posición).

Utilizando este comando generaremos un fondo animado de letras al estilo de “Pasapalabra”.


2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 15′ (2,2 Ghz Intel Core i7, 8GB DDR3).
  • Sistema Operativo: Mac OS El Capitán 10.11
  • Entorno de desarrollo: Adobe After Effects CC

3. Crear proyecto

Lo primero que haremos será crear un proyecto y una nueva composición con el tamaño que queramos que tenga nuestro fondo. En este caso, como luego añadiremos la animación a un proyecto full HD, utilizaremos este tamaño.

Elegimos el fondo que queramos para previsualizarlo. En este caso hemos elegido negro.


4. Añadir expresión

Posteriormente, para comenzar con nuestro fondo animado añadiremos un texto con la letra “A” (configuraremos la expresión con esta letra y luego duplicaremos la capa para crear las otras).

A nuestra letra le ponemos la tipografía y color que queramos y desplegamos las características de posición, escala, rotación y opacidad.

Después, pulsando sobre el cronómetro de cada una de ellas con la tecla Alt pulsada sacamos la opción de añadir una expresión.

FOTO 1

Ahora es cuando añadiremos el comando wiggle.

La sintaxis del comando wiggle es la siguiente wiggle(frecuencia, magnitud), es decir, el primer valor corresponde a las veces por segundo en las que cambian los valores y el segundo valor el margen de valor aleatorio respecto al valor original de nuestro texto.

Como queremos que el movimiento de nuestro fondo sea fluido y no realice cambios muy bruscos añadiremos un valor de frecuencia pequeño (entre 0.1 y 1 según la propiedad), y una magnitud alta para que nos ocupe toda la composición.

Foto 2

Una vez añadidos los valores comprobamos que nos gusta el movimiento de nuestra letra y en caso de que no sea así, cambiamos los valores hasta que el resultado sea el que queremos.


5. Duplicar capas

Para crear nuestro fondo animado con las letras del abecedario duplicamos la capa donde tenemos ya incluidos nuestros valores y cambiamos el contenido por otra letra del abecedario, el resultado sería el siguiente:

Foto 3

Por último, precomponemos todas las capas para dejarla en una única seleccionando todas las capas y pinchamos en botón derecho -> Precomponer. Una vez hecho esto sacamos la opacidad pulsando la letra T y le bajamos la opacidad al 60%.

Si vamos a exportarlo con canal alpha para después llevarlo a otro programa es mejor sacarlo con opacidad al 100% y bajarle la opacidad en el programa de destino, para que no se queden las letras en un tono grisáceo.


6. Exportado

Para finalizar, tenemos dos opciones: añadirle un fondo que nos guste (por ejemplo un sólido de algún color) o exportarlo con canal alpha (es decir, sin fondo), para añadirlo posteriormente a otro vídeo que tengamos.

Para exportar la composición pinchamos encima de ella y en la barra superior en composición pinchamos en añadir a la cola de procesamiento.

Foto 4

Si pinchamos en módulo de salida, nos salen las opciones de exportado, si no hemos metido nuestro fondo en after y pretendemos meterlo después, en canales elegiremos la opción RGB + Alfa. Si por el contrario hemos incluido el fondo en After Effects, elegiremos la opción RGB.

Por último, en salida definimos la ubicación de destino de nuestro archivo y le damos a procesar.

El resultado final una vez añadido a un vídeo sería el siguiente:

Foto 5

7. Conclusiones

Este tutorial es sólo uno de los muchísimos usos que se le pueden dar a la expresión wiggle. Una vez conocemos su funcionamiento, ¡sólo es cuestión de investigar!


8. Referencias

Optimización de consultas en MySQL

$
0
0

En este tutorial vamos a dar unas pautas básicas para optimizar consultas pesadas, basándonos en el gestor de bases de datos MySQL.

Índice de contenidos


1. Introducción

Es bastante común que en nuestras aplicaciones haya alguna sección de informes, históricos, búsquedas generales… en la que se acaben consultando una gran cantidad de registros provenientes de varias tablas a la vez. Si estas tablas son grandes (varias centenas de miles de registros, o incluso millones), la optimización de las consultas pesadas supondrá la diferencia entre tener una aplicación fluida o tener una aplicación inmanejable.

Para los ejemplos de este tutorial hemos empleado una base de datos de testing, que podéis obtener aquí. Se trata de una base de datos con unos 300.000 registros de empleados y alrededor de 2,8 millones de registros de salarios. La estructura de las tablas es la siguiente:

employees_schema

2. Entorno

El tutorial se ha realizado usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro Retina 15′ (2,3 GHz Intel Core i7, 16GB DDR3).
  • Sistema Operativo: Mac OS El Capitán 10.11.16
  • Base de datos: MySQL 5.6.24
  • Cliente de MySQL: MySQL Workbench 6.3

3. Diagnóstico de la consulta

Antes de empezar a cambiar nada, lo primero que recomendamos es ejecutar el comando EXPLAIN sobre esa consulta. De esta manera podemos saber qué pasos sigue el gestor de la base de datos para realizarla, y de qué manera accede a las tablas en cada uno de ellos. Con esto podremos identificar posibles cuellos de botella. Si no estáis familiarizados con este comando podéis echar un vistazo a este tutorial de introducción.

Del resultado de este comando, en lo que más nos vamos a fijar en este tutorial es:

  • El orden de las filas: es el orden en que la base de datos consulta las tablas.
  • La columna Key: indica el índice que se está empleando para acceder a esa tabla concreta (si lo hubiera)
  • La columna type: indica el tipo de acceso a la tabla. Los posibles valores, de mejor a peor, son:
    • system
    • const
    • eq_ref
    • ref
    • fulltext
    • ref_or_null
    • index_merge
    • unique_subquery
    • index_subquery
    • range
    • index
    • ALL

    Si algún acceso de la consulta es del tipo index o ALL, deberíamos revisar esa parte de la consulta, a menos que queramos expresamente listar o calcular todos los registros de la tabla.

El resultado de la sentencia EXPLAIN puede mostrarse como una tabla (formato tradicional) o como un JSON. La herramienta MySQL Workbench usa este formato JSON para mostrar un diagrama visual del plan (opción visual explain en la sección de resultados del plan), que nos resulta muy útil para detectar los cuellos de botella, ya que los marca en rojo.

visual_explain

4. Optimizaciones


4.1. Evitar FULL SCANS

Si al ejecutar la sentencia EXPLAIN, comprobamos que el tipo de acceso a alguna de las tablas es de ALL, entonces se trata de un FULL TABLE SCAN, quiere decir que se están leyendo todos los registros de dicha tabla. Deberíamos ver si se puede reescribir la consulta para que se acceda por un índice de la tabla, o valorar la posibilidad de crear un índice para las columnas por las que se está buscando. Esto dependerá de lo frecuente o importante que sea esta consulta en nuestra aplicación.

Si el tipo de acceso es INDEX no es mucho mejor, pues nos indica que se están leyendo todos los accesos de un índice. Se le llama FULL INDEX SCAN y no llega a ser tan grave como un FULL TABLE SCAN, porque se suele hacer en memoria y lee menos información por registro, pero igualmente estamos pasando por todos los nodos. Este tipo de acceso se emplea cuando todas las columnas que queremos obtener forman parte del mismo índice, y por tanto, el optimizador de la base de datos entiende que no necesita ir a las tablas para obtener la información, ya que puede sacarla exclusivamente del índice.


4.2. Uso CORRECTO de los índices

¡Pero… si estoy usando un “índice”!

Hay que ver los índices como diccionarios, donde el propio índice es la palabra y el registro entero de la tabla es la definición. Si queremos buscar la definición de “escuela” y tanteamos por palabras que comiencen por “esc”, no iremos mal encaminados, mientras que si queremos encontrarla buscando palabras que terminen por “la” no nos quedará más remedio que empezar por la primera página e ir pasando por todas hasta que demos con nuestra palabra. Esto explica que una búsqueda LIKE ‘prefijo_índice%’ es indexada y otra LIKE ‘%sufijo_índice’ no lo es.

Esto que parece una tontería cobra especial importancia en el caso de los índices compuestos (formados por varias columnas), donde el orden de las columnas que los forman es totalmente determinante. Con este tipo de índices prefiero usar el ejemplo de la guía telefónica: no seremos muy eficientes buscando el teléfono de alguien de quien sólo conocemos su segundo apellido.

Para tener una primera idea del concepto de índice podéis leer este tutorial.


4.3. Sentencias OR

El optimizador de MySQL no puede usar índices si se está empleando la sentencia OR y alguna de sus restricciones es una columna no indexada. Por ejemplo

SELECT * FROM mi_tabla
WHERE mi_indice = 'valor1' OR columna_no_indexada = 'valor2'

Deberíamos tratar de evitar las sentencias OR, siempre que sea posible.


4.4. GROUP / ORDER BY

Por simplicidad vamos a referirnos a la cláusula GROUP BY, pero teniendo en cuenta que todo se aplica también a ORDER BY.

Esta cláusula puede suponer un verdadero cuello de botella cuando el número de registros a agrupar es muy elevado (independientemente de que se use la cláusula LIMIT, pues esta se aplica después del GROUP BY).

Hay que procurar que todas las columnas presentes en el GROUP BY formen parte del mismo índice de la tabla que se está consultando, en el mismo orden que en la consulta. Si la consulta es muy importante en nuestra aplicación, podemos valorar la posibilidad de definir un índice para optimizarla. Elegiríamos primero la columna/s filtrada en el WHERE, y después aquellas presentes en el GROUP BY.


4.5. Tablas derivadas vs Subqueries

Una subconsulta no es más que una sentencia SELECT dentro de otra sentencia. Una tabla derivada es un tipo concreto de subconsulta que se caracteriza porque está dentro del FROM de la consulta “padre”. El tratamiento de ambas es diferente:

  • En una subconsulta se ejecuta una búsqueda en la base de datos por cada registro de la consulta “padre”
  • En una tabla derivada se realiza una sola consulta a la base de datos, almacenándose los resultados en una tabla temporal en memoria. A esta tabla se accede una vez por cada registro de la consulta padre.

Supongamos que queremos hacer una consulta que nos devuelva, para cada empleado, todas las columnas de su tabla y además el número de empleados que han nacido el mismo día.

Así de primeras, se me ocurren dos maneras de hacerlo:

  • Con una tabla derivada
    select e.*, e2.cuenta as mismo_dia
    from employees as e
    inner join(
    	select e.birth_date, count(*) as cuenta
      from employees as e
      group by e.birth_date
    ) as e2 on e2.birth_date = e.birth_date
    result_derived_no_index
  • Con una subconsulta dentro de la sección SELECT
    select e.*,
    (
    	select count(e2.birth_date) as cuenta
    	from employees as e2
    	where e2.birth_date = e.birth_date
    	group by e2.birth_date
    ) as mismo_dia
    from employees as e
    result_subquery_no_index

Los resultados revelan una gran diferencia de eficiencia entre ambas consultas. A primera vista puede parecer que, tratando con tablas de muchos registros, siempre va a ser mejor la solución de la tabla derivada, por aquello de que una consulta va a ser más eficiente que varios cientos de miles de consultas. Bueno, pues esto no siempre es cierto. Pongamos un ejemplo.

Puesto que estamos usando la fecha de nacimiento para hacer las consultas, vamos a crear un índice con ese campo

CREATE INDEX employees_birth ON employees(birth_date);

Y para subir la complejidad vamos a aumentar los requisitos de nuestra consulta: Además de todo lo anterior, también queremos tener una columna con el número de empleados que nacieron el día antes y otra con los que nacieron el día después. Veamos cómo quedarían las dos consultas resultantes y, lo más importante, el tiempo de ejecución de las mismas:

  • La tabla derivada
    select e.*, e2.cuenta as mismo_dia, e3.cuenta as dia_antes, e4.cuenta as dia_despues
    from employees as e
    inner join(
    	select e.birth_date, count(*) as cuenta
        from employees as e
        group by e.birth_date
    ) as e2 on e2.birth_date = e.birth_date
    inner join(
    	select e.birth_date, count(*) as cuenta
        from employees as e
        group by e.birth_date
    ) as e3 on e3.birth_date = DATE_SUB(e.birth_date , INTERVAL 1 DAY)
    inner join(
    	select e.birth_date, count(*) as cuenta
        from employees as e
        group by e.birth_date
    ) as e4 on e4.birth_date = DATE_ADD(e.birth_date , INTERVAL 1 DAY)
    result_derived_index
  • La subconsulta
    select e.*,
    (
    	select count(e2.birth_date) as cuenta
        from employees as e2
        where e2.birth_date = e.birth_date
        group by e2.birth_date
    ) as mismo_dia,
    (
    	select count(e3.birth_date) as cuenta
        from employees as e3
        where e3.birth_date = DATE_SUB(e.birth_date , INTERVAL 1 DAY)
        group by e3.birth_date
    ) as dia_antes,
    (
    	select count(e4.birth_date) as cuenta
        from employees as e4
        where e4.birth_date = DATE_ADD(e.birth_date , INTERVAL 1 DAY)
        group by e4.birth_date
    ) as dia_despues
    
    from employees as e
    result_subquery_index

¿Por qué oscuro motivo la consulta que realiza 4 accesos a las tablas de la base de datos (tabla derivada) tarda más que la que realiza varios miles de accesos (subconsulta en el SELECT)?

En el primer caso, con tablas derivadas, las tablas que se crean en memoria como resultado de las subconsultas de los INNER JOIN no están indexadas. Esto hace que, para cada registro de la consulta “padre”, deban leerse todas las filas de las tablas derivadas para comprobar cual cumple la condición de igualdad. En una tabla de 300.000 registros supone, en el peor de los casos, 300.000 lecturas en memoria.

En el segundo caso, con subconsultas en el SELECT, para cada registro de la tabla “padre” se realizan 3 accesos indexados a las tablas de las subconsultas. Esto supone, debido a la naturaleza de árbol binario del índice, 18 lecturas en cada una de las 3 subconsultas. En total 54 lecturas contra la base de datos.

Este es otro ejemplo de la importancia de los índices en nuestras consultas.


4.6. INNER JOIN con GROUP / ORDER

Este es un caso digno de mención. En una consulta con varios INNER JOIN es el optimizador quien decide el orden de consulta de las tablas, dependiendo de las restricciones y el uso de los índices. Esta decisión suele ser acertada, con una excepción: puede no ser el orden óptimo para efectuar el GROUP BY (o el ORDER BY).

En este caso necesitamos colocar primero la tabla sobre la que se está haciendo el GROUP BY y forzar al optimizador a que lea primero esa tabla. De esta manera, el GROUP BY se realizará de una sola pasada, debido a que los resultados ya estarán ordenados con respecto al índice. Esto lo conseguimos con STRAIGHT_JOIN, que no es más que un INNER JOIN con la particularidad de que fuerza la lectura de la tabla de la izquierda antes que la de la derecha.

Con los LEFT JOIN no ocurre este problema, pues en este caso la tabla de la izquierda SIEMPRE se leerá antes que su tabla dependiente.


4.7. La sentencia EXISTS, esa gran olvidada

Supongamos que tenemos dos tablas: BIG y HUGE. La tabla BIG tiene miles de registros, y la tabla HUGE tiene miles de millones de registros. La relación entre ambas es tal que para cada registro de BIG puede haber de cero a varios millones de registros en HUGE.

Si queremos obtener los registros de BIG para los cuales se cumplen ciertas condiciones en HUGE, puede que nuestra primera aproximación sea realizar un INNER JOIN de esta forma:

SELECT BIG.*
FROM BIG INNER JOIN HUGE ON HUGE.fk_big = BIG.id_big
WHERE HUGE.campo_huge_1 = 'valor1'
GROUP BY BIG.id_big

Esta consulta tardará más de la cuenta, pese a estar ordenada “de serie” por el id de BIG, debido a que se tienen que recorrer todos los registros resultantes para agruparlos. Esto puede suponer recorrer varios cientos de millones de registros, para mostrar unas miles de agrupaciones. A primera vista parece que estamos realizando muchas lecturas innecesarias.

Una manera mejor de realizar nuestra consulta sería con una subconsulta que emplee la cláusula EXISTS

SELECT BIG.*
FROM BIG
WHERE EXISTS (
	SELECT (*) FROM HUGE
	WHERE HUGE.fk_big = BIG.id_big
	AND HUGE.campo_huge_1 = 'valor1'
)

¿Cual es la principal ventaja? Las consultas con EXISTS suelen ser muy eficientes, ya que mysql interrumpe la consulta cuando encuentra la primera coincidencia. Así evitamos recorrer todos los registros de HUGE que cumplen la condición.

Obviamente, esto sólo es aconsejable cuando estemos accediendo a la tabla HUGE por algún índice.


5. Conclusiones

Como hemos podido ver a lo largo del tutorial, nuestra primera acción ante una consulta pesada deberá ser analizarla con la sentencia EXPLAIN. Después del análisis, intentaremos que todos los accesos (los que se pueda) se realicen a través de índices. Finalmente, si la consulta sigue siendo lenta, intentaremos detectar si el tiempo se nos está yendo en alguna operación innecesaria o mejorable.

Ante todo, no hay mejor estrategia que usar el sentido común.


6. Referencias

Monitorización y análisis de rendimiento de aplicaciones con Dynatrace

$
0
0

En este tutorial vamos a echar un vistazo a Dynatrace, una herramienta que nos permite monitorizar nuestras aplicaciones en un entorno productivo o no.


Índice de contenidos.


1. Introducción

El producto de APM de Dynatrace es uno de los más destacados dentro del cuadránte mágico de Gartner (APM) que define este tipo de herramientas como aquellas que permiten monitorizar 5 dimensiones funcionales básicas:

  • end-user experience monitoring (EUM)
  • runtime application architecture discovery modeling and display
  • user-defined transaction profiling
  • component deep-dive monitoring in application context and
  • analytics

Ya hemos tratado en otros tutoriales otros productos de APM que se encuentran también muy bien posicionados, así AppDynamics y New Relic.

Lo interesante ahora de todas ellas es que dentro de su licenciamiento proporcionan una licencia gratuita para la monitorización de entornos no productivos, con la única limitación del tiempo; así, con toda la potencia de la herramienta puedes monitorizar su comportamiento en tu entorno de desarrollo local, almacenando información, por regla general, de las últimas dos horas. Para otro tipo de entornos, ya productivos, se hace necesario adquirir una licencia comercial del mismo.

En este tutorial vamos a ver como instalar y configurar Dynatrace en un entorno local y daremos un vistazo rápido a sus características de monitorización.

2. Entorno.

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 15′ (2.5 GHz Intel Core i7, 16GB DDR3).
  • Sistema Operativo: Mac OS El Capitan 10.11
  • Dynatrace 6.5

3. Instalación.

Todas las herramientas de APM requiere de la instalación de:

  • un servidor o controlador de eventos, aunque hay algunas como New Relic que este componente solo está disponible en la nube, en el modo on-premise del resto lo primero a hacer es instalar el servidor,
  • un cliente que permita acceder a la información de la monitorización, aunque también en el caso de New Relic solo está disponible en la nube,
  • un agente de usuario para la tecnología con la que trabajemos, en nuestro caso java, que instrumentaliza nuestro código para remitir los eventos que se producen en el mismo hacia el servidor.

Lo primero que deberíamos hacer para instalar Dynatrace es registrarnos en la web para poder realizar una prueba gratuita, proporcionando la siguiente información:

dynatrace-01

Una vez registrados nos llegará un correo electrónico con la siguiente información, junto con la licencia de uso del producto:

dynatrace-02

Pulsando sobre el enlace accederemos a la página de descarga del producto y seleccionando el SO correspondiente podremos descargar el correspondiente paquete de instalación.

dynatrace-03

Desde la propia página podemos acceder a la información necesaria para la instalación, en nuestro caso al seleccionar el paquete para Mac, debemos ejecutar el jar como sigue:

dynatrace-04

Tras lo cual, se descomprime el paquete y disponemos del producto “instalado” en el mismo directorio dynatrace-X.X correspondiente a la versión

El siguiente paso es arrancar todos los servicios como sigue:

cd /~/dynatrace-6.5
./dtserver -bg
./dtfrontendserver -bg
./dtcollector -bg

Podemos comprobar que se han levantado correctamente ejecutando un grep sobre los procesos “dt”:

ps -A | grep dt

A continuación debemos descargar el cliente para activar la licencia, para ello desde la parte inferior de la página de descarga podemos acceder al siguiente paso:

dynatrace-042

Que nos permitirá realizar una descarga del paquete cliente.

dynatrace-041

Con ello tendremos ya descargados dos paquetes, el servidor y el cliente:

dynatrace-10

Al abrir el dmg podemos proceder a realizar la instalación:

dynatrace-05

Lo primero que debemos hacer es asignar el fichero de la licencia:

dynatrace-06

Si todo va bien, nos confirmará la importación de la misma.

dynatrace-07

Que requrirá el reinicio del servidor.

dynatrace-08

El reinicio se realizará automáticamente

dynatrace-09

Al reiniciar se mostrará dentro del cliente una ventana como la que sigue, para proceder a la descarga y configuración del agente:

dynatrace-11

Debemos seleccionar el tipo de agente, en nuestro caso java:

dynatrace-12

Y configurar el tipo de servidor que vamos a monitorizar:

dynatrace-13

Seleccionamos el SO para el cuál realizar la descarga del paquete del agente; tened en cuenta que lo normal será tener el servidor montado en un entorno y la monitorización de los servidores de aplicaciones en otro, aquí lo estamos montando todo en la misma máquina:

dynatrace-14

Una vez seleccionado podremos descargar el agente para java:

dynatrace-15

Una vez descargado no tenemos más que ejecutar el jar para descomprimirlo:

dynatrace-17

A continuación, el propio wizard muestra un ejemplo de cómo configurar el arranque de Tomcat para añadir la configuración del agente

dynatrace-19

Que llevado a los parámetros de arranque del Tomcat que podemos lanzar desde cualquier IDE, en nuestro caso Eclipse, sería lo siguiente:

dynatrace-18

No tenemos más que incorporar el siguiente argumento:

-agentpath:/Applications/development/dynatrace-agent-6.3/agent/lib64/libdtagent.dylib=name=Tomcat_Monitoring,server=localhost:9998

El cliente permanecerá esperando la información que el agente remitirá al servidor una vez arranquemos nuestro Tomcat

dynatrace-20

En el arranque del Tocmat deberíamos ver las siguientes trazas:

dynatrace-201

Y automáticamente el cliente detectará el envío de información, indicando que la configuración es correcta.

dynatrace-21

Mostrando a continuación un resumen de la configuración del agente.

dynatrace-22

Quedando el agente registrado como sigue:

dynatrace-23

4. Un vistazo rápido.

En el primer acceso al cliente nos mostrará un resumen de los agentes instalados y las transacciones monitorizadas.

dynatrace-24

Para que se muestre la siguiente información, hemos realizado una serie de peticiones a la aplicación desplegada en el tomcat y el cliente descubre automáticamente el tipo de petición y el servidor.

dynatrace-25

Pulsando sobre el nodo, se muestra en una ventana emergente un resumen desde el cual podemos acceder al detalle de las transacciones o, en terminología de Dynatrace, los PurePaths.

Hemos forzado un cierto número de errores para comprobar que los monitoriza y, del mismo modo, también hemos forzado un sleep en la única petición satisfactoria para comprobar que somos capaces de encontrar el origen de la demora.

dynatrace-26

A continuación podemos ver como acceder, pulsando sobre la fila, al detalle de la transacción para ver donde se va el tiempo de respuesta.

dynatrace-27

Desde el dashboard podemos acceder a un listado de los errores producidos para comprobar los mensajes de respuesta al cliente.

dynatrace-28

Y desde el resumen de los mensajes a las transacciones que los originaron.

dynatrace-29

Es interesante comprobar como la herramienta en base a la información de la transacción puede generar un diagrama de secuencia de las clases implicadas en la misma

dynatrace-30

Generando un diagrama como el siguiente:

dynatrace-31

Y más interesante aún ver cómo podemos acceder al código fuente de un método en concreto, responsable de la falta de rendimiento:

dynatrace-32

Decompilando el código fuente como se ve a continuación:

dynatrace-33

Si probamos a desplegar una aplicación con “un poco más de chicha” podemos comprobar como el cliente nos muestra un diagrama de los componentes implicados en las transacciones, en este caso, un nodo adicional de base de datos.

dynatrace-43

Podemos acceder al detalle de las transacciones y comprobar como aparece en la parte superior derecha un resumen de puntos calientes que afectan al rendimiento de la transacción, en este caso los accesos a base de datos:

dynatrace-42

Desde las opciones de la transacción podemos acceder a un listado de todas las consultas realizadas desde la transacción a base de datos.

dynatrace-44

Así podemos comprobar los tiempos de respuesta y decidir si es necesario dedicar tiempo o no a optimizar las mismas.

Una última característica bastante interesante es la posibilidad de exportar un PurePath para remitirlo, en cualquier momento, ocultando la información de los posibles objetos sensibles de la sesión del usuario, al equipo de desarrollo para su visualización en local, sin necesidad de acceso al servidor.

dynatrace-34

Para ello, pulsando sobre la transacción, podemos exportarla indicando la siguiente información:

dynatrace-35

5. Referencias.


6. Conclusiones.

No es una herramienta más, es una de las 3 mejores posicionadas y que proporciona mucho más de lo que hemos visto en este simple tutorial: cuadros de mando, alertas, agentes de máquina y base de datos, agentes de EUM que permiten recibir información del tiempo de renderización de las páginas en cliente (incluso snapshots de las mismas), posibilidad de disponer de un entorno de instalación dockerizado,…

Toda una joya, de ahí que su licencia para entornos productivos lo valga!

Un saludo.

Jose

Polymer Day 2016

$
0
0

El pasado 21 y 22 de octubre tuvo lugar en Madrid el PolymerDay 2016. En él se dieron cita los mayores representantes del mundo front especialmente bregados en esta tecnología.

A casi nadie se le escapa ya que los WebComponentes han llegado para quedarse. Y mientras que ciertos frameworks front nacen, cambian y mueren a velocidades de vértigo, los componentes web se imponen y son el futuro, por un hecho muy simple: son el estándar.

De lo que allí se contó, me gustó casi todo, pero por no abrumar, me quiero quedar con tres hechos:

  • Grandes empresas lo están usando en producción
  • Polymer 2.0 ya está a la vuelta de la esquina
  • Los componentes deben organizarse en una arquitectura.

Polymer en producción

Durante el evento fueron numerosos los ejemplos de lo que se está haciendo con Polymer y como estas empresas lo usan en aplicaciones en producción. Destacaron dos: una gran entidad financiera con colores azules, que aunque no se mencionó su nombre todos sabíamos quienes eran, y otra que me dejó impresionado por lo ingeniosa de una solución a un problema concreto.


Victor Sánchez nos contó como su startup SyncRTC había montado un videowall de 45 m2 para el Instituto de Empresa, para que un profesor se pusiera en video conferencia simultáneamente con todos sus alumnos, y la clase fuera online. Cada componente donde se veía la cara del alumno en tiempo real es un WebComponent basado en el estándar de WebRTC. Todos los componentes orquestados, con las opciones de votación, de tiempos de participación, etc… se representaban en la aplicación. Me pareció un uso genial de la tecnología de WebComponentes aplicados a solucionar un problema concreto.

Polymer 2.0 bajo el capó.

Rob Dodson nos presentó como la nueva versión de Polymer 2.0 sigue la especificación v1, y nos acerca algo más al código vainilla. A lo largo del evento, fueron varios los ponentes que dejaron caer la idea de que según se avance en la implementación del estándar por parte de los navegadores, irá haciendo cada vez menos falta Polymer, llegando incluso algún día a ser totalmente innecesario. Pero hoy por hoy, nos aporta muchas ventajas. Para diciembre/enero podremos disfrutar de la nueva versión de Polymer 2.0 que ya se está cociendo, y que dará lugar a componentes más ligeros.

Arquitectura de componentes

Cada componente por sí sólo es una entidad, que debe ser agnóstica de dónde está contenida, pero que hay que proveerla de los mecanismos para poder recibir entradas desde fuera, y que está se comunique desde dentro.

Con los WebComponents se recomienda que la comunicación fuera-dentro se haga mediante una propiedad, mientras que de dentro-fuera con un evento. Pero estos WC tienen que relacionarse y cohesionarse de una forma lógica. Aquí es donde los patrones de diseño vienen una vez más a solucionar problemas clásicos.

Javier Vélez Reyes sentó cátedra proponiéndonos una arquitectura basada en componentes, muchos de los cuales no tenía representación gráfica, y cómo se relacionan entre sí. Esperaba su charla con mucho interés y no me defraudó. En esta ocasión, no se limitó a tener sus pensamientos abstractos en el platónico mundo de las ideas, si no que nos contó como lo estaba trayendo al mundo de las cosas pasando del racionalismo al que nos tiene acostumbrados a un ejercicio meramente empírico, que la verdad, tenía muy buena pinta.

En resumen

Evidentemente se quedan muchas cosas en el tintero, pero un simple artículo no da para todas las charlas que allí se dieron: se habló de rendimiento y de cómo mejorarlo, de accesibilidad y componentes, aplicaciones offline first usando PounchDB cuyo concepto me encantó, y el uso de Polymer en combinación con Firebase.

La charla de mi compañero Rubén Aguilera de que Angular 2.0 con Polymer mejor, y viceversa, fue una demostración de como alguien es capaz de dar una charla en directo y picar código a la vez. Y aunque compartimos tesis y puntos de vista, no por ser conocida por mi su exposición de argumentos, deja de ser una excepcional participación, de la que espero muchas personas hayan extraído un aprendizaje.

Son muchos los temas tratados, y ahora quedan unas ganas locas de experimentar con lo aprendido.

Explicación al reto de concurrencia. Solución con StampedLock

$
0
0
La mayoría de programadores encontró un punto ciego con la sentencia “arr[size++]=e;“. De alguna forma pensamos que el tamaño se actualizará después de la asignación. En este artículo veremos este bug básico de concurrencia y presentamos una solución con StampedLock de Java 8.

Éste artículo es una traducción al castellano de la entrada original publicada, en inglés, por Dr. Heinz Kabutz en su número 242 de The Javatm Specialists’ Newsletter. Puedes consultar el texto original en Javaspecialists’ Newsletter #242: Concurrency Puzzle Explained, Solved With StampedLock

Este artículo se publica traducido en adictos, con permiso del autor, por David Gómez García, (@dgomezg) consultor tecnológico en Autentia, colaborador de JavaSpecialists e instructor certificado para impartir los cursos de JavaSpecialists en español.
Bienvenidos a la edición número 242 de The Javatm Specialists’ Newsletter, enviado desde una lluviosa Creta. Siempre nos sorprendemos con las primeras lluvias de verdad de la temporada. ¿Cómo se atreve el agua a caer desde el cielo? ¿y qué es eso gris de ahí arriba?. Nuestros olivos necesitan un sorbo de agua para engordar sus frutos para la cosecha. Lamentablemente, gracias a los fuertes vientos, buena parte de nuestra cosecha está convirtiendose en compost. Bueno, quizá el próximo año podamos llenar nuestras cubas de nuevo. Hasta entonces, ¡racionaremos los suministros!Me alegra poder dar la bienvenida a Burkina Faso a la lista de paises suscriptores confirmados, dejando el contador en 138 paises. El canal de Slack de Java Specialists (en inglés) está que se sale. Casi 2.000 miembros ya, intercambiando grandes ideas, aprendiendo cosas nuevas. Tenemos miembros en 22 husos horarios. Lo que significa que puedes encontrar a alguien para hablar de Java en tiempo real a cualquier hora del día o de la noche. Para unirte, rellena este formulario con tus datos y automágicamente recibirás una invitación de Slack (pero por favor, comprueba tu carpeta de SPAM). NOVEDAD Hemos actualizado nuestro curso de “Advanced Topics” que cubre Reflection, Java NIO, Estructuras de datos, Gestión de memoria y algunos otros temas útiles para dominar si eres un experto en Java. Dos días de diversión y aprendizaje extremos: “Extreme Java – Advanced Topics”. Este curso también lo impartimos en castellano.

El problema de concurrencia explicado, solucionado con StampedLock

Nos hemos divertido este último mes. Propuse un reto de concurrencia que ha tenido a expertos en Java de todo el mundo rascándose la cabeza. Tanto es así, que también publiqué algunas pistas útiles. Lo que más me sorprendió de las respuestas es la cantidad que no se había molestado en ejecutar el código. En vez de ello, lo revisaron y pulsaron en “Responder”. Ninguna de estas respuestas capturó el bug que lo hacía fallar de forma consistente. Lo peor es que perdieron una oportunidad fantástica de aprender algo nuevo. Cuando se buscan errores de concurrencia, el primer paso es tratar de reproducirlo. Esto puede resultar bastante difícil. Por ejemplo, el código que me envió Jack no fallaba en mi MacBook Pro. Lo tuve que ejecutar en mi servidor 2-4-1 con una CPU más antigua para conseguir que el bug se reprodujese. Aquí tenéis una versión con un probabilidad mayor de hacer aparecer el fallo, adaptado para mi amigo Stuart Marks:
import java.util.*;

public class MyArrayListStuart {
  private final Object READ_LOCK = new Object();
  private final Object WRITE_LOCK = new Object();
  private int[] arr = new int[10];
  private int size = 0;

  public int size() {
    synchronized (READ_LOCK) {
      return size;
    }
  }

  public int get(int index) {
    synchronized (READ_LOCK) {
      rangeCheck(index);
      return arr[index];
    }
  }

  public boolean add(int e) {
    synchronized (WRITE_LOCK) {
      if (size + 1 > arr.length)
        arr = Arrays.copyOf(arr, size + 10);

      arr[size++] = e;
      return true;
    }
  }

  public int remove(int index) {
    synchronized (WRITE_LOCK) {
      rangeCheck(index);

      int oldValue = arr[index];

      int numMoved = size - index - 1;
      if (numMoved > 0)
        System.arraycopy(arr, index + 1,
            arr, index, numMoved);
      arr[--size] = 0;

      return oldValue;
    }
  }

  private void rangeCheck(int index) {
    if (index >= size)
      throw new IndexOutOfBoundsException(
          "Index: " + index + ", Size: " + size);
  }

  private static volatile boolean goAdd;

  public static void main(String[] args) {
    for (int i = 0; i < 100000; i++) {
      goAdd = false;
      MyArrayListStuart list = new MyArrayListStuart();
      new Thread(new Main(list, true)).start();
      new Thread(new Main(list, false)).start();
      new Thread(new Main(list, false)).start();
      new Thread(new Main(list, false)).start();
    }
  }

  static class Main implements Runnable {
    MyArrayListStuart list;
    boolean update;

    public Main(MyArrayListStuart list,
                boolean update) {
      this.list = list;
      this.update = update;
    }

    @Override
    public void run() {
      if (update) {
        while(!goAdd);
        goAdd = false;
        for (int i = 1; i < 1000; i++) {
          list.add(i);
        }
        for (int i = 1; i < 250; i++) {
          list.remove(7);
        }
      } else {
        // wait until we're certain
        // index 6 has a value
        while (list.size() < 7) {goAdd = true;}
        for (int i = 1; i < 1000; i++) {
          int x;
          if ((x = list.get(6)) != 7) {
            System.out.println(x +
                " and " + list.size());
          }
        }
      }
    }
  }
}
El fallo de concurrencia se manifiesta más fácilmente con el código modificado. Lo podemos reproducir de forma consistente. Es casi imposible hacer esto con errores de tipo happens-before. Esos suelen mostrar su fea cara en producción, no en un pequeño test como el nuestro. Los errores de visibilidad suelen aparecer y no se recupera uno de ellos, por ejemplo, si tenemos un bucle cerrado en el que leemos un campo desprotegido. En nuestro caso, el error es mucho más básico. La segunda pista estaba en que ocurría incluso sin el remove(). Así que podiamos ignorar tranquilamente el método remove() y centrarnos en el método add(). Una tercera pista estaba en que el redimensionado no tenía nada que ver con el error. Incluso si dimensionamos desde el principio el array con un tamaño de 1.000, también podía fallar. La primera respuesta correcta la dió Andreas Senft. Reconoció que se trataba simplemente de un error de concurrencia común que no tenía nada que ver con cachés, happens-before, visibilidad, etc… Simple y llanamente una condición de carrera en el método add(). Si miramos dentro de add(), veremos la sentencia arr[size++] = e;. La mayoría de programadores leen esto como “Asigna e en el elemento del array y actualiza el tamaño”. En sus cabezas, esto es lo que ocurre:
arr[size] = e;
	size = size + 1;
Aunque sería más acertado verlo de la siguiente forma:
int temp = size;
  size = size + 1;
  arr[temp] = e;
Ahora es obvio que size se actualiza primero y después se asigna el elemento en el array. Si, entre la llamada a size = size + 1; y arr[temp] = e; el hilo deja su tiempo de ejecución de CPU, el hilo de lectura podría tratar de leer un elemento que no existe todavía, dado que está bloqueado en un monitor diferente. Si reemplazamos el código por el siguiente, entonces los errores se producirán con menos frecuencia (pero todavía es incorrecto):
arr[size] = e;
	size++;
Demostrar que es también incorrecto será más complicado. El test que teníamos en el artículo anterior no será suficiente. El problema con el código estaba en que tenemos distintos locks guardando campos que mantienen una invariante entre ellos. La motivación detrás de esto era tratar de evitar que los bloqueos de escritura bloqueen los hilos que sólo quieren leer. Pensé que éste sería un buen caso de uso para el StampedLock de Java 8. Si todo esto te resulta nuevo, ¿puedo sugerirte apuntarte a mi curso Extreme Java – Concurrency and performance for Java 8? (que también impartimos en español). ¡Aprenderás en tres días más de lo que crees que es posible!

IntList con StampedLock

StampedLock se añadió con Java 8 para darnos la posibilidad de realizar lecturas optimistas sobre campos. Esto es útil cuando tenemos varios campos con una invariante entre todos ellos. En nuestro ejemplo, esos son los campos arr y size. IntList es el resultado de un trabajo en colaboración entre Peter Levart, Henri Tremblay, Hanko Gergely, Alex Snaps y yo mismo. Cada uno de nosotros ha ayudado a refinar un poquito la solución final. Es un trabajo en curso así que, por favor, decidme si encontráis una forma de mejorarlo todavía más. Casi seguro que tiene jugosos bugs aún por descubrir. Para empezar tenemos size(). Dado que lo que nos preocupa es la visibilidad del campo size, lo único que necesitamos es llamar al método tryOptimisticRead(). Éste método realiza una lectura volatile sobre el campo dentro de un StampedLock, que tiene el mismo efecto que entrar en un bloque synchronized, y por tanto garantiza que veremos el valor correcto del campo size. El método get() es el más complejo. Intentamos leer el elemento del array en una variable local entre las llamadas a tryOptimisticRead() y validate(), asegurándonos que no causamos un ArrayIndexOutOfBoundsException en el proceso. Si somos lo suficientemente rápidos, podemos hacerlo antes de que otro hilo obtenga el lock de escritura. Verás que intentamos la lectura optimista tres veces y, si no lo conseguimos, cambiamos a usar un lock de lectura pesimista. Este cambio puede ser bueno, pero también puede penalizarnos si lo hacemos demasiado. La función trimToSize() se supone que nos muestra como podemos utilizar tryOptimisticRead() para evitar obtener un lock de escritura si no lo necesitamos. El método add() es sencillo, casi como utilizar un ReentrantLock. Tenemos dos funciones de borrado. El método remove() simple es análogo a add(), utilizando un lock pesimista para la lectura exclusiva. El método removeWithUpgrade() obtiene un lock de lectura y, si el índice no está fuera de rango, intentamos convertirlo en un lock de escritura. Un ReentrantReadWriteLock produciría un deadlock, pero con StampedLock lo conseguiremos. Este lock condicional de escritura es útil cuando tenemos una alta probabilidad de no necesitar el lock de escritura. De todas formas, en nuestro caso esperamos que la mayor parte de las veces el índice estará dentro del rango. Por eso, el método remove() sería probablemente una solución mejor. Más rápido y más fácil de comprender y de mantener. Vamos al código. Divertíos. Discutiremos esto en el canal #newsletter del grupo JavaSpecialists.slack.com. Por favor, rellena tus datos aquí para unirte (sin cuotas, sin obligaciones, excepto “ser agradables”).
import java.util.*;
import java.util.concurrent.locks.*;

public class IntList {
  private static final int OPTIMISTIC_SPIN = 3;
  private final StampedLock sl = new StampedLock();
  private int[] arr = new int[10];
  private int size = 0;

  /**
   * The size() method cannot be used to perform compound
   * operations, such as getting the last element of a list.
   * This code could fail with an IndexOutOfBoundsException:
   *
   * <pre>
   *   Thread1:
   *     while(true) {
   *       list.get(list.size()-1);
   *     }
   *
   *   Thread2:
   *     for (int i = 0; i < 2000; i++) {
   *       for (int j = 0; j < 100; j++) {
   *         list.add(j);
   *       }
   *       for (int j = 0; j < 50; j++) {
   *         list.remove(0);
   *       }
   *     }
   * </pre>
   */
  public int size() {
    // Internally, the tryOptimisticRead() is reading a volatile
    // field.  From a memory visibility perspective, reading a
    // volatile field is like entering a synchronized block.
    // We will thus not have an issue with stale values of size.
    // Note 1: volatile != synchronized.  Volatile does not
    // magically make compound operations on fields mutually
    // exclusive. Race conditions are more probable on volatile
    // fields.
    // Note 2: We could also have made size volatile.  From a
    // visibility perspective the tryOptimisticRead() will work,
    // but not if size was a long or if it could have
    // intermediate values that broke invariants.
    sl.tryOptimisticRead();
    return this.size;
  }

  public int get(int index) {
    for (int i = 0; i < OPTIMISTIC_SPIN; i++) {
      long stamp = sl.tryOptimisticRead();
      int size = this.size;
      int[] arr = this.arr;
      if (index < arr.length) {
        int r = arr[index];
        if (sl.validate(stamp)) {
          rangeCheck(index, size);
          return r;
        }
      }
    }
    long stamp = sl.readLock();
    try {
      rangeCheck(index, this.size);
      return this.arr[index];
    } finally {
      sl.unlockRead(stamp);
    }
  }

  public void trimToSize() {
    long stamp = sl.tryOptimisticRead();
    int currentSize = size;
    int[] currentArr = arr;
    if (sl.validate(stamp)) {
      // fast optimistic read to accelerate trimToSize() when
      // there is no work to do
      if (currentSize == currentArr.length) return;
    }
    stamp = sl.writeLock();
    try {
      if (size  arr.length)
        arr = Arrays.copyOf(arr, size + 10);

      arr[size++] = e;
      return true;
    } finally {
      sl.unlockWrite(stamp);
    }
  }

  // just to illustrate how an upgrade could be coded
  public int removeWithUpgrade(int index) {
    long stamp = sl.readLock();
    try {
      while (true) {
        rangeCheck(index, size);
        long writeStamp = sl.tryConvertToWriteLock(stamp);
        if (writeStamp == 0) {
          sl.unlockRead(stamp);
          stamp = sl.writeLock();
        } else {
          stamp = writeStamp;
          return doActualRemove(index);
        }
      }
    } finally {
      sl.unlock(stamp);
    }
  }

  public int remove(int index) {
    long stamp = sl.writeLock();
    try {
      rangeCheck(index, size);
      return doActualRemove(index);
    } finally {
      sl.unlock(stamp);
    }
  }

  private int doActualRemove(int index) {
    int oldValue = arr[index];

    int numMoved = size - index - 1;
    if (numMoved > 0)
      System.arraycopy(arr, index + 1, arr, index, numMoved);
    arr[--size] = 0;

    return oldValue;
  }

  private static void rangeCheck(int index, int size) {
    if (index >= size)
      throw new IndexOutOfBoundsException(
          "Index: " + index + ", Size: " + size);
  }
}
He ejecutado algunas pruebas de rendimiento comparando éste con una versión correctamente sincronizada del MyArrayList que usamos en el número anterior. El método get() es basicamente el doble de rápido y usa bastante menos CPU de sistema gracias a la reducción del número de cambios de contexto voluntarios. Los métodos add() y remove() son ligeramente más lentos, pero como esperaba, size() es ridículamente rápido. He añadido éste como un ejercicio a mi curso de concurrencia. Veamos cómo de bien se las apañan programadores Java buenos y experimentados cuando vean StampedLock por primera vez :-) Saludos Heinz.

Monitorización y análisis de rendimiento de aplicaciones con PinPoint APM

$
0
0

Después de analizar muchas herramientas comerciales de APM nos encontramos con PinPoint, un proyecto open source para crear una herramienta de APM.


Índice de contenidos.


1. Introducción

Tenemos muchas referencias de herramientas de monitorización de tipo APM y en este mismo site podéis consultar revisiones de muchas de ellas: New Relic AppDynamics y Dynatrace.

En esta ocasión vamos a realizar una revisión de una herramienta open source bautizada como PinPoint, cuya traducción es “determinar con precisión”.

Creemos que es una herramienta que vale la pena tener en consideración puesto que, aunque no tiene todas las características de sus competidores comerciales, todas se basan en la misma arquitectura, y aunque quizás requiere un poco más de esfuerzo en su instalación, los resultados de la monitorización son igual de interesantes.

2. Entorno.

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 15′ (2.5 GHz Intel Core i7, 16GB DDR3).
  • Sistema Operativo: Mac OS El Capitan 10.11
  • PinPoint APM 1.6.0-SNAPSHOT

3. Instalación.

Podemos llevar a cabo una instalación muy simple puesto que, a día de hoy, disponemos de la versión 1.5.2 dockerizada y disponible en docker hub.

Con seguir los pasos documentados podemos, en un par de minutos, comprobar como con una aplicación de ejemplo, se puede comenzar a ver los resultados de la monitorización de la misma.

Si bien, en este tutorial, vamos montarnos el entorno de desarrollo del producto lo cuál nos permitirá examinar las últimas características del mismo.

Lo primero que necesitaremos es bajarnos el código fuente del proyecto PinPoint de github.

A continuación debemos configurar una serie de variables de entorno apuntando a las siguientes versiones de la JDK:

export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_25.jdk/Contents/Home
export JAVA_6_HOME=/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home
export JAVA_7_HOME=/Library/Java/JavaVirtualMachines/jdk1.7.0_11.jdk/Contents/Home
export JAVA_8_HOME=$JAVA_HOME

Y ejecutar, con el soporte de maven, un clean install

mvn clean install -Dmaven.test.skip=true

Maven invoca internamente tanto a tareas de ant como de npm, con lo que igualmente debemos tenerlos instalados y con permisos de ejecución.

Si, como yo, no tenéis instalada ya una versión 6 de la jdk en Mac podéis obtenerla aquí.

Una vez compilado y generados los correspondientes artefactos volveríamos a tener dos posibilidades:

  • arrancarlo dockerizado con el soporte de una configuración para docker-compose que se distribuye en el propio repositorio, o
  • arrancarlo manualmente, que es la opción que vamos a seguir, puesto que la idea es seguir “trasteando” con la herramienta.

Para continuar deberíamos descargarnos la última versión de hbase, una base de datos distribuida, no relacional, que forma parte del proyecto Hadoop. Si bien en el propio arranque si no la encuentra se la descarga automáticamente.

Para finalizar tenemos que ejecutar los siguientes scripts:

  • Para levantar la base de datos:

    quickstart/bin/start-hbase.sh
  • Para crear e inicializar las tablas de la base de datos:

    quickstart/bin/init-hbase.sh
  • Para arrancar el servidor:

    quickstart/bin/start-collector.sh
  • Para arrancar el cliente:

    quickstart/bin/start-web.sh
  • Podemos arrancar una aplicación de ejemplo que nos permitirá generar transacciones de monitorización más o menos costosas y comprobar su captura desde el cliente:

    quickstart/bin/start-testapp.sh

Una vez arrancado todo podemos acceder a:

http://localhost:28080/

se debería mostrar una interfaz como la siguiente:

pinpoint-01

Y para ejecutar la aplicación de ejemplo a:

http://localhost:28081/

se debería mostrar una aplicación en la que podemos acceder a distintas ejecuciones para generar trazas:

pinpoint-02

4. Un vistazo rápido.

Una vez hayamos generado tráfico se generará un dashboard como el siguiente en el que lo primero que debemos hacer es seleccionar la aplicación:

pinpoint-03

Una vez seleccionado, podemos ver el gráfico de componentes desde el cual se ha monitorizado el tráfico, se puede observar el número de transacciones correctas y no y una criticidad de las mismas por tiempo de ejecución.

pinpoint-04

En esta versión, la usabilidad de la interfaz es mejorable puesto que, para acceder al detalle de las transacciones, la única opción es seleccionar en el gráfico superior derecho un área de puntos. Se podría haber planteado el acceso a las mismas igualmente desde el gráfico de acumulados.

pinpoint-041

Pero una vez lo conocemos, podemos acceder al detalle de cada una de las transacciones para hacer un drill down sobre una en concreto para ver toda la pila de ejecución de la misma.

pinpoint-05

Con ello podemos ver el origen de un error, como en el siguiente caso

pinpoint-06

Podemos arrancar una aplicación nuestra, configurando el tomcat con los siguientes parámetros, para comprobar cómo monitoriza el acceso a base de datos:

-javaagent:/Applications/development/pinpoint/pinpoint-master/quickstart/agent/target/pinpoint-agent/pinpoint-bootstrap-1.6.0-SNAPSHOT.jar -Dpinpoint.applicationName=TMS -Dpinpoint.agentId=ECLIPSE_TOMCAT8
pinpoint-051

Nada más arrancar podremos comprobar que se muestra la aplicación en el listado de las mismas y, en cuanto comencemos a navegar por la misma comprobaremos que el gráfico de componentes se va generando y podemos acceder a las estadísticas.

pinpoint-07

Si seleccionamos un área del gráfico que contenga una transacción algo costosa, podemos comprobar el detalle de la misma:

pinpoint-10

Si en la pila hay consultas a base de datos, se puede acceder a la consulta realizada pulsando sobre el icono de “SQL”:

pinpoint-11

5. Referencias.


6. Conclusiones.

Tanto la interfaz de usuario como su usabilidad se puede mejorar bastante. De hecho hay momentos en los que el solape de capas o acciones de “volver atrás” hacen que se pierda la visibilidad de ciertos componentes.

Si bien, siendo un proyecto open source, lo más interesante es disponer de funcionalidades que las licencias comerciales no soportan, como disponer de alta disponibilidad monitorizando los diferentes nodos de tu aplicación en un solo controlador. La interfaz permite filtrar la información por agente:

pinpoint-08

Y, del mismo, modo acceder al detalle de la información enviada por cada uno de ellos:

pinpoint-09

Una herramienta a tener en cuenta y sobre la cuál iremos haciendo un seguimiento.

Un saludo.

Jose


Criptografía y seguridad

$
0
0

Hoy vamos a empezar con este tutorial una serie de artículos sobre conceptos básicos de seguridad y criptografía. La idea es conocer los conceptos que son necesarios para comprender términos más complejos que son las piezas sobre las que se asienta la seguridad informática hoy en día. En muchos casos, lo que ocurre es que es el desconocimiento de la base lo que nos impide la comprensión de conceptos más complejos. Por ejemplo, el desconocimiento de cómo funciona la criptografía de clave pública nos impide comprender la firma digital, lo que a su vez nos impide comprender de qué y cómo nos protege el protocolo https de algunas amenazas comunes a nuestra privacidad.

Para eso voy a daros la tabarra con varios conceptos teóricos. Luego intentaremos hacer algunos ejemplos para aplicar lo que hemos aprendido.

Entorno

Este tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro Retina 15′ (2.5 Ghz Intel Core I7, 16GB DDR3)
  • Sistema Operativo: Mac OS El Capitan 10.11.6
  • Entorno de desarrollo: IntelliJ IDEA 2016.2
  • Librerías:
    • Bouncy Castle – bcprov-jdk15on-1.55.jar

1. ¿Qué es la criptografía?

La criptografía es la técnica de construir y analizar protocolos que permiten que terceras personas sean capaces de leer mensajes que se desea que permanezcan privados. Hoy en día la criptografía se ocupa de varios aspectos de la seguridad de la información como pueden ser:

  • Confidencialidad: Sabemos que la información únicamente es accesible por aquellos a los que va dirigida.
  • Integridad de los datos: Sabemos que la información es correcta y no ha sido modificada por terceros.
  • Autenticidad: El remitente de la información tiene una identidad que puede ser verificada.
  • Vinculación: Sabemos que el creador de la información es el remitente no siendo posible que haya sido un tercero.

Para lograr esos objetivos se utilizan una serie de técnicas que vamos a ir describiendo a lo largo de estos tutoriales.


2. Conceptos básicos

Hay unos cuantos conceptos básicos que tenemos que conocer antes de entrar en materia.

  • Cifrado: Es el proceso de codificar la información de tal forma que sólo pueda ser leída por terceros autorizados utilizando una clave de cifrado. Esos terceros usan el proceso contrario (descifrado) junto con la clave para acceder a los contenidos del mismo.
  • Texto plano: Son los datos antes de ser cifrados. Aunque se le llame “texto” pueden ser también datos binarios como imágenes, ejecutables, etc.
  • Texto cifrado: Es el texto plano después de pasar por el proceso de cifrado.
  • Clave de cifrado: Es una secuencia de caracteres que se utiliza para realizar el cifrado de los datos y sin la cual no es posible descifrarlos. Idealmente la clave debe permanecer secreta y ser conocida sólo por los comunicantes, aunque ya veremos que el proceso es diferente en la criptografía de clave pública.
  • Algoritmo de cifrado: Son el par de algoritmos capaces de convertir el texto plano en texto cifrado y viceversa utilizando una o varias claves de cifrado.

Es sumamente importante que se nos meta en la cabeza lo siguiente: La seguridad de un algoritmo de cifrado reside en su clave. Cuando nuestro sistema requiere utilizar datos cifrados siempre vamos a utilizar un algoritmo de cifrado público y bien conocido. Esto permite que el algoritmo en sí haya sido sometido a un análisis a fondo por parte de especialistas que tienen un nivel de conocimiento profundo de las técnicas de criptoanálisis. Nuestro sistema va a ser seguro mientras la clave se mantenga privada y sólo sea conocida por quien debe serlo. El caso contrario (la llamada “seguridad por oscuridad”) no está recomendada para la mayoría de los casos y siempre deberíamos actuar como si un eventual atacante conociera perfectamente el algoritmo que se ha utilizado en el cifrado.

Los algoritmos de cifrado presentan obsolescencia. Según pasa el tiempo la potencia del hardware disponible aumenta, se descubren nuevas técnicas de criptoanálisis y se descubren vulnerabilidades en algoritmos existentes. Esto hace que sea necesario estar al día por si es necesario cambiar de algoritmos o usar versiones de los mismos con una mayor longitud de clave.

Una vez tenemos claros estos conceptos básicos podemos entrar en harina y comenzar a hablar de los dos grandes grupos de algoritmos de cifrado: los algoritmos de cifrado simétrico y los de cifrado asimétrico.


3. Cifrado simétrico y cifrado asimétrico

Los algoritmos de cifrado simétrico son aquellos en los que se usa la misma clave para el cifrado y para el descifrado. Comparados con los algoritmos de cifrado asimétrico son de ejecución rápida y con claves bastante más pequeñas. No obstante tienen un gran problema: Es necesario que ambas partes de la comunicación compartan la misma clave.

En los algoritmos de cifrado asimétrico existe un par de claves para llevar a cabo el proceso: Una clave pública que se distribuye a todo aquel que la necesite y una clave privada que ha de ser conocida sólo por el poseedor del par de claves. Estos algoritmos permiten que cualquier tercero cifre un mensaje con la clave pública que sólo podrá ser descifrado con la clave privada. Este tipo de cifrado es mucho más lento que el cifrado simétrico.

En este tutorial vamos a centrarnos en el cifrado simétrico. Dejaremos el cifrado asimétrico para próximos tutoriales.


3.1. Tipos de algoritmos de cifrado simétrico

Existen dos grandes grupos de algoritmos de cifrado simétrico:

  • Algoritmos de cifrado por bloques: Aplican su transformación sobre bloques de datos de longitud fija.
  • Algoritmos de cifrado de flujo: Combinan el texto plano con un flujo de clave (keystream) que se genera con ayuda de la clave de cifrado y que se aplica sobre el texto plano dígito a dígito.

Algunos algoritmos de cifrado de bloques muy utilizados son AES, Blowfish, Twofish, DES o TripleDES. DES está considerado como inseguro y el autor de Blowfish recomienda utilizar Twofish. La opción más utilizada a día de hoy es utilizar AES con longitudes de clave de 128, 192 y 256 bits.

En cuanto a los algoritmos de cifrado de flujo se suelen utilizar los algoritmos de cifrado de bloques en los modos de operación OFB, CFB y CTR, que transforman el cifrador efectivamente en un cifrador de flujo. El algoritmo RC4, aunque muy usado hasta hace poco a día de hoy no está considerado como seguro y va a ser eliminado de la próxima especificación de TLS. La alternativa actual como algoritmo de flujo está representada por la suite eSTREAM.


3.2. Modos de operación

Un algoritmo de cifrado de bloques aplica la transformación de texto claro a texto cifrado sobre bloques de datos de longitud fija. Algoritmos como DES, TripleDES o Blowfish utilizan un tamaño de bloque de 64 bits. Alternativas más modernas como AES o Twofish utilizan uno de 128 bits

Un cifrado por bloques sólo permite cifrar texto de una longitud igual a la del bloque que tiene definido. Para cifrar textos más largos el mensaje se divide en bloques de esa misma longitud añadiendo posiblemente un padding o relleno para completar en el último bloque con los bits faltantes hasta llegar al tamaño múltiplo del tamaño de bloque.

Los bloques generalmente no se concatenan uno detrás de otro tal cual sino que mezclan utilizando lo que llamamos modo de operación. Esta característica aplica únicamente a los algoritmos de cifrado por bloques y es sumamente importante para mantener la seguridad de la clave. En los modos de operación generalmente se utiliza un vector de inicialización para dotar de cierta aleatoriedad al resultado.

El vector de inicialización se considera un dato público y nunca debería ser a su vez cifrado. Además ha de ser aleatorio y debe generarse uno nuevo en cada aplicación del cifrado. Generalmente tiene el mismo tamaño que el tamaño de bloque del algoritmo de cifrado utilizado.

Existen diversos modos de operación sobre los que no entraremos demasiado en detalle

Los modos ECB (Electronic CodeBook) y CBC (Cipher Block Chaining) operan estrictamente sobre bloques. Si nuestro texto claro no tiene una longitud que sea un múltiplo exacto del tamaño del bloque se añadirá al último bloque un relleno o padding para llegar a esa longitud.

Los modos CFB (Cipher FeedBack), OFB (Output Feedback) y CTR (CounTeR) utilizan el algoritmo de cifrado para generar lo que llamamos un flujo de clave o keystream, que se mezclará con el texto plano para ir produciendo el texto cifrado. Esto convierte el cifrado por bloques esencialmente en un cifrado de flujo por lo que se elimina la necesidad de padding.

Generalmente se recomienda utilizar los modos CBC o CTR. El modo ECB no debería utilizarse en ninguna aplicación criptográfica.


3.3. Modos de operación autenticados

Los modos de operación normales presentan un gran problema: un potencial atacante puede interceptar textos cifrados enviados al receptor, modificarlos levemente y volverlos a enviar para observar los resultados. Esto se conoce como ataque por texto cifrado escogido (chosen ciphertext attack).

Para evitar esto se utilizan lo que se conoce como modos de operación autenticados. En ellos se protege el texto cifrado con algún tipo de algoritmo de resumen tipo HMAC. Los modos de operación autenticados se encargan de este proceso de forma transparente al usuario. Algunos de ellos son:

  • GCM (Galois Counter Mode): Libre de patentes, rápido y además soportado por TLS. (link)
  • OCB (Offset Codebook Mode): El más rápido de todos. Con patentes para uso comercial. (link)
  • EAX: Libre, fácil de implementar y ligero. Eso si, es lento en comparación con el resto. (link)
  • CCM (Counter mode with CBC MAC): Implementado en el estándar WPA2, se considera la alternativa libre de patentes a OCB aunque es mucho más lento. (link)

La recomendación aquí sería utilizar los modos GCM (libre) y, si nuestro desarrollo lo permite, OCB.


4. Implementando todo en Java. Librerías Bouncy Castle

Java viene con un conjunto básico de proveedores criptográficos “out-of-the-box”. Sin embargo si queremos tener una mayor variedad de algoritmos y operaciones nos vemos obligados a utilizar librerías de terceros. De lejos lo más utilizado en aplicaciones Java son las librerías de Bouncy Castle, que además de ser libres pertenecen a una organización sin ánimo de lucro australiana, lo que nos evitará tener que bajarnos los archivos que permiten el uso de cifrado fuerte que aplican sobre los productos estadounidenses.

Las librerías Bouncy Castle cuentan con un provider que se integra con la JCA de Java por si queremos utilizarlas de esta manera y además cuentan con un API ligero que es el que vamos a utilizar en estos ejemplos.

Para descargarse las librerías podemos hacerlo directamente desde la página del proyecto o añadir la dependencia de maven a nuestro código:

<dependency>
      <groupId>org.bouncycastle</groupId>
      <artifactId>bcprov-jdk15on</artifactId>
      <version>1.55</version>
  </dependency>

5. Ejemplo: Cifrado en modo CBC

Vamos a implementar una función que cifra un texto plano en modo CBC utilizando una clave y un vector de inicialización aleatorio. Comenzaremos utilizando un algoritmo TripleDES con una longitud de clave de 192 bits y un vector de inicialización de 64. Más adelante comentaremos los puntos interesantes del código. Al final del tutorial hay un enlace a GitHub con los fuentes que se han utilizado.

public static void main(String[] args) throws UnsupportedEncodingException,
    InvalidCipherTextException {
      // Create the cipherer
      BlockCipher engine = new DESedeEngine();
      BlockCipher cipherInOperationMode = new CBCBlockCipher(engine);
      PaddedBufferedBlockCipher cipher = new PaddedBufferedBlockCipher(cipherInOperationMode,
        new PKCS7Padding());

      // Get the plaintext bytes
      System.out.println("Texto plano: " + PLAINTEXT);
      byte[] plainText = PLAINTEXT.getBytes("utf-8");
      System.out.println("Texto plano (longitud en bytes): " + plainText.length);

      // Generate the random key and initialization vector
      SecureRandom randomGenerator = new SecureRandom();
      byte[] key = new byte[24];              // 24 bytes = 192 bits
      byte[] iv = new byte[8];                // 8 bytes = 64 bits
      randomGenerator.nextBytes(key);
      randomGenerator.nextBytes(iv);

      // Init the cipherer with the key and initialization vector
      KeyParameter keyParameter = new KeyParameter(key);
      ParametersWithIV parametersWithIV = new ParametersWithIV(keyParameter, iv);
      cipher.init(true, parametersWithIV);

      // Allocate space for maximum output size when creating output array
      int maxOutputSize = cipher.getOutputSize(plainText.length);
      byte[] cipherText = new byte[maxOutputSize];

      // Do the ciphering
      int finalOutputSize = cipher.processBytes(plainText, 0, plainText.length, cipherText, 0);
      cipher.doFinal(cipherText, finalOutputSize);

      // Append the IV at the beginning of the ciphertext
      byte[] ciphertextWithIVBytes = new byte[iv.length + cipherText.length];
      System.arraycopy(iv, 0, ciphertextWithIVBytes, 0, iv.length);
      System.arraycopy(cipherText, 0, ciphertextWithIVBytes, iv.length, cipherText.length);

      System.out.println("Texto cifrado (bytes): " + Arrays.toString(ciphertextWithIVBytes));
      System.out.println("Texto cifrado (longitud en bytes): " + ciphertextWithIVBytes.length);
  }
  • En un primer momento se inicializa el cifrador. La librería implementa funcionalidades como los modos de operación y el padding mediante un patrón decorador. En este caso estamos construyendo un cifrado mediante el algoritmo TripleDES (DESedeEngine), lo ponemos en modo de operación CBC (CBCBlockCipher) y le añadimos padding y buffer (PaddedBufferedBlockCipher).
  • Más adelante generamos una clave y un vector de inicialización aleatorios con la longitud deseada utilizando la clase SecureRandom. No se considera una buena práctica utilizar el constructor de la clase al que se proporciona un byte[] así que se hace la llamada sin parámetros. A continuación se inicializa el cifrado con esos mismos parámetros.
  • Se crea el array de salida tomando como tamaño el valor que nos devuelve el método getOutputSize, que es el tamaño máximo que va a tener la salida dada la longitud de la entrada. Como hemos seleccionado un modo de operación con padding este tamaño puede ser diferente al tamaño real de la salida más adelante.
  • Se hace el cifrado llamando a los métodos processBytes y doFinal. Ambos devuelven un entero con el número de bytes que se han devuelto. En caso de que no hemos seleccionado un modo de operación con padding esto nos ayudará a recortar el resultado a su tamaño real. La llamada a doFinal procesa el último bloque y añade el padding si es necesario.
  • El resultado final se monta concatenando el vector de inicialización (recordemos: sin cifrar) al texto cifrado

La clave y el vector de inicialización son aleatorios, por lo que la ejecución varía. En nuestro caso devuelve lo siguiente:

Texto plano: This is a simple symmetric cryptography test using the bouncy castle library
  Texto plano (longitud en bytes): 76
  Texto cifrado (bytes): [126, -58, -116, -48, 14, 71, -119, 82, (...)
  Texto cifrado (longitud en bytes): 88

Nótese que la longitud del texto cifrado es múltiplo del tamaño del bloque, que es de 64 bits (8 bytes). Si le restamos los 8 bytes del vector de inicialización el texto plano ocupa un total de 80 bytes (10 bloques).

Se puede cambiar el algoritmo de cifrado y la longitud de la clave de forma muy sencilla. Por ejemplo, vamos a utilizar un cifrado AES con una longitud de clave de 256 bits (32 bytes). El tamaño del vector de inicialización será en este caso el mismo que el tamaño del bloque del cifrado AES: 128 bits (16 bytes).

public static void main(String[] args) throws UnsupportedEncodingException,
    InvalidCipherTextException {
      // Create the cipherer
      BlockCipher engine = new AESEngine();
      BlockCipher cipherInOperationMode = new CBCBlockCipher(engine);
      PaddedBufferedBlockCipher cipher = new PaddedBufferedBlockCipher(cipherInOperationMode,
        new PKCS7Padding());

      // Get the plaintext bytes
      // (...)

      // Generate the random key and initialization vector
      SecureRandom randomGenerator = new SecureRandom();
      byte[] key = new byte[32];              // 32 bytes = 256 bits
      byte[] iv = new byte[16];               // 16 bytes = 128 bits
      randomGenerator.nextBytes(key);
      randomGenerator.nextBytes(iv);

      // (...)
  }

Lo único que hemos hecho es cambiar el engine de cifrado por la clase AESEngine y las longitudes de clave y vector de inicialización. En este caso el resultado es:

Texto plano: This is a simple symmetric cryptography test using the bouncy castle library
  Texto plano (longitud en bytes): 76
  Texto cifrado (bytes): [67, -123, 29, -10, -113, -92, -14, -34, 118, 88, 56, (...)
  Texto cifrado (longitud en bytes): 96

Nótese que en este caso el resultado final también tiene una longitud que es múltiplo de 16 bytes, que es el tamaño de bloque. Para completar el ejemplo vamos a comentar el código correspondiente al descifrado. En la ejecución añadiremos este código a continuación del correspondiente al cifrado.

// Extract the iv from the ciphertext
  byte[] decipherIV = new byte[16];
  byte[] textToDecipher = new byte[ciphertextWithIVBytes.length - 16];
  System.arraycopy(ciphertextWithIVBytes, 0, decipherIV, 0, 16);
  System.arraycopy(ciphertextWithIVBytes, 16, textToDecipher, 0, textToDecipher.length);

  // Reuse the cipherer to do the deciphering with the cipher key and recovered iv
  KeyParameter decipherKey = new KeyParameter(key);
  ParametersWithIV decipherInitParams = new ParametersWithIV(decipherKey, decipherIV);
  cipher.reset();
  cipher.init(false, decipherInitParams);

  // Allocate space for the deciphered text
  int maxDecipheredOutputSize = cipher.getOutputSize(textToDecipher.length);
  byte[] decipheredOutputBytes = new byte[maxDecipheredOutputSize];

  // Perform the deciphering
  int finalDecipheredTextSize = cipher.processBytes(textToDecipher, 0, textToDecipher.length,
    decipheredOutputBytes, 0);
  finalDecipheredTextSize += cipher.doFinal(decipheredOutputBytes, finalDecipheredTextSize);

  // Remove the padding
  byte[] unpaddedDecipheredOutputBytes = new byte[finalDecipheredTextSize];
  System.arraycopy(decipheredOutputBytes, 0, unpaddedDecipheredOutputBytes, 0,
    finalDecipheredTextSize);

  System.out.println("Texto descifrado: " + new String(unpaddedDecipheredOutputBytes, "utf-8"));

Cosas que comentar aquí:

  • En primer lugar es necesario separar el vector de inicialización del texto cifrado. Como viaja en claro se toman los primeros 16 bytes (correspondientes al vector de inicialización del algoritmo que estamos utilizando) y se separan del texto cifrado.
  • Se utiliza la misma clave con la que hemos cifrado para descifrar (lógico). Por supuesto esta clave ha de permanecer a buen recaudo.
  • Se inicializa el cifrador en modo descifrado (primer parámetro a false). Aquí puede instanciarse un nuevo objeto, a gusto del usuario. Para el ejemplo vamos a reutilizar el mismo objeto.
  • Del mismo modo que en el cifrado, se crea un array con el máximo tamaño posible de la salida y se llama a la operación de descifrado en dos pasos. En este caso guardamos un contador con el tamaño real del resultado.
  • Utilizamos ese contador para eliminar los bytes finales que corresponden al padding. Con el array resultante creamos un String y, voilà! Tenemos nuestro texto descifrado.

Nuestro resultado de la ejecución:

Texto plano: This is a simple symmetric cryptography test using the bouncy castle library
  Texto plano (longitud en bytes): 76
  Texto cifrado (bytes): [50, 93, 13, 22, 78, -70, 116, -51, (...)
  Texto cifrado (longitud en bytes): 96
  Texto descifrado: This is a simple symmetric cryptography test using the bouncy castle library

6. Ejemplo: Cifrado en modo GCM

El cifrado en modo de operación GCM (Gaulois-Counter mode) se realiza de un modo muy similar. Vamos con el código fuente resumido en el que subrayaremos las diferencias.

public static void main(String[] args) throws UnsupportedEncodingException,
    InvalidCipherTextException {
      // Create the cipherer
      BlockCipher engine = new AESEngine();
      AEADBlockCipher cipher = new GCMBlockCipher(engine);

      // Get the plaintext bytes
      // (...)

      // Generate the random key and nonce
      SecureRandom rng = new SecureRandom();
      byte[] key = new byte[256/8];
      byte[] nonce = new byte[96/8];
      rng.nextBytes(key);
      rng.nextBytes(nonce);

      // Init the cipherer with key, mac size and nonce
      KeyParameter keyParameter = new KeyParameter(key);
      AEADParameters parameters = new AEADParameters(keyParameter, 128, nonce);
      cipher.init(true, parameters);

      // Allocate space for maximum output size
      // (...)

      // Do the ciphering
      // (...)

      // Append the nonce at the beginning of the ciphertext
      // (...)

      // TODO: Note that output length is (nonce_size + input_length + mac_size)

      // **** DECIPHERING ****

      // Split the ciphertext with nonce
      // (...)

      // Reset the cipherer and init for deciphering
      // (...)

      // Allocate space for the deciphered text
      // (...)

      // Perform the deciphering
      // (...)

      // There is no padding to be removed
  }
  • Para aplicar el modo de operación GCM se envuelve el algoritmo base (en este caso AES) con un wrapper de tipo GCMBlockCipher, que implementa la interfaz AEADBlockCipher. Es necesario mencionar que no podríamos utilizar aquí un algoritmo DES o TripleDES, ya que GCM sólo se puede usar con algoritmos cuyo tamaño de bloque sea de 128 bits (como por ejemplo AES o Twofish).
  • Se genera una clave adecuada para el algoritmo base. Como vamos a cifrar utilizando un algoritmo AES escogemos la longitud de clave más larga: 256 bits.
  • Se genera un nonce, que a todos efectos será equivalente al vector de inicialización y tendrá una longitud que para este modo de operación se recomienda que sea de 96 bits.
  • Se escoge un tamaño para la MAC que se va a ir generando. Se recomienda en este caso que sea de 128 bits, que es el tamaño máximo soportado.
  • El tamaño de la salida será igual al tamaño de la entrada + tamaño del nonce + tamaño del MAC.
  • El descifrado se realiza de la misma forma que en el cifrado anterior. En este caso no hay un padding que tengamos que eliminar.

La propiedad más interesante de estos modos de operación es que cuando se modifica cualquiera de los bytes del texto cifrado y se trata de descifrar ese texto la rutina de descifrado interpreta el texto como incorrecto y ni siquiera trata de descifrarlo. En nuestro caso si modificamos el array con los bytes del texto cifrado y tratamos de realizar el descifrado se produce la siguiente excepción:

Exception in thread "main" org.bouncycastle.crypto.InvalidCipherTextException: mac check in
    GCM failed
  	at org.bouncycastle.crypto.modes.GCMBlockCipher.doFinal(Unknown Source)
  	at (...)

Esto mitiga el ataque mediante texto cifrado escogido que hemos mencionado más arriba.


7. Rendimiento

Como curiosidad hemos hecho un pequeño experimento sobre el rendimiento del cifrado con diferentes algoritmos, modos y tamaños de clave. Los resultados varían en cada ejecución, pero aquí una muestra:

AlgoritmoLongitud de claveModoCifrado (ns)Descifrado (ns)
DES64CBC50365234
TripleDES128CBC81557997
TripleDES192CBC1065711000
AES128CBC14912075
AES192CBC17142186
AES256CBC18672335
TwoFish128CBC1942218624
TwoFish192CBC2743428786
TwoFish256CBC3557236263
AES128GCM2168220986
AES192GCM2039920706
AES256GCM2049221364
TwoFish128GCM2771828370
TwoFish192GCM3307334306
TwoFish256GCM3861237476

Es reseñable la diferencia de velocidad entre el modo de operación CBC y el modo autenticado GCM. Si el rendimiento es un factor determinante en nuestra aplicación o ejecutamos sobre un hardware muy restringido deberíamos plantearnos la posibilidad de utilizar un cifrado no autenticado, asumiendo los riesgos que derivan de esa elección.

Otro valor que llama la atención es la rapidez con la que se ejecuta el algoritmo AES en la máquina en la que se ha realizado la prueba. Esto es debido fundamentalmente a que en la actualidad muchas CPU incluyen aceleración por hardware para esta operación, que es estándar en el protocolo TLS.


8. Conclusiones

Hemos aprendido las generalidades y los términos básicos de la criptografía simétrica y, además, hemos aprendido lo que no hay que hacer cuando usamos este tipo de técnica. Por último, hemos aprendido cómo se hace todo esto utilizando una librería de gran difusión en el mundo Java, como son las Bouncy Castle.

En próximos tutoriales nos adentraremos en el mundo de la criptografía de clave pública y exploraremos algunas de sus aplicaciones como son los certificados digitales o el protocolo SSL/TLS. ¡Hasta entonces!


Referencias

Mavenización de proyectos legacy

$
0
0

En este tutorial vamos a aprender una serie de nociones básicas a la hora de añadir soporte Maven a un proyecto de tipo java heredado, ayudándonos a gestionarlo y a automatizar tareas como su despliegue.

Índice de contenidos

1. Introducción

A lo largo de nuestra vida tendremos que mantener proyectos que ya se encuentren en proceso de desarrollo, pero en numerosas ocasiones estos proyectos no incluirán soporte Maven.

Maven es una herramienta a través de la cual podemos gestionar nuestro proyecto software, automatizando tareas como la compilación del mismo, el despliegue, la ejecución de sus test, la generación de documentación, etc.

Si queremos que el trabajo llevado a cabo una vez el proyecto caiga en nuestras manos sea mucho más cómodo, en caso de no estar basado en Maven, podemos mavenizarlo. Para ello, vamos a ver una serie de claves sobre la herramienta que nos ayudarán bastante.

2. Estructura de un proyecto Maven

Todo proyecto de tipo Maven tiene una estructura de directorios muy característica, la cual podemos ver a continuación:

estructura_maven

Cada una de las carpetas de la jerarquía contiene lo siguiente:

  • src: código fuente, ficheros de configuración, recursos y demás. Es decir, todo salvo el directorio de salida (target) y el pom.xml
  • main: desarrollo de la aplicación, independientemente de los test
  • test: desarrollo de los test de la aplicación
  • config: ficheros de configuración en el proyecto
  • java: clases java que contienen el código fuente de la aplicación
  • resources: recursos que se incluirán en el empaquetado y serán usados por la aplicación, como pueden ser ficheros de texto o scripts
  • webapp: contiene todos los ficheros correspondientes a la propia aplicación web que no sean código java

El primer paso de la mavenización de nuestro proyecto será crear la nueva estructura de directorios y migrar todos los ficheros a la misma.

3. El fichero pom.xml y la gestión de dependencias

Este fichero debe ir en el directorio raíz del proyecto, y contiene la descripción y la configuración del mismo. Aquí incluiremos las dependencias correspondientes a las librerías necesarias en el proyecto, los repositorios a los que pertenecen, las propiedades de entorno en base a los distintos perfiles e información sobre el tipo de empaquetado, principalmente.

También añadiremos información relativa al proyecto, como la versión en la que se encuentra o qué módulos engloba. A su vez, cada uno de estos módulos deberá contener en su directorio raíz su propio fichero pom.xml.


3.1. Etiquetas básicas

Algunas de las etiquetas básicas que contiene el fichero pom.xml son las siguientes:

  • project: Es la etiqueta que recoge todas las demás.
  • modelVersion: Indica qué versión de la especificación POM se está utilizando.
  • groupId: Identificador único del grupo de proyectos dentro de la organización en la que nos encontremos. Suele coincidir con el nombre base del paquete java, añadiendo el nombre del proyecto.
  • artifactId: Identificador único del artefacto correspondiente al proyecto. Por defecto será el nombre de paquetería.
  • version: Versión del proyecto en la cual estamos trabajando. Se le puede añadir el sufijo SNAPSHOT para indicar que es una versión en desarrollo.
  • packaging: Tipo de empaquetado del proyecto (jar, war, esb…)
  • name: Nombre del proyecto.
  • description: Descripción del proyecto.
  • dependencies: Dentro de esta etiqueta incluiremos las dependencias necesarias, cada una de ellas precedida de la etiqueta dependency. En la web http://mvnrepository.com/ podremos encontrar cualquiera de ellas, facilitándonos la plataforma el texto que debemos añadir en el pom.xml para incluirlas en el proyecto.

A continuación tenemos un ejemplo muy sencillito de pom.xml, en el cual se pueden ver todas las etiquetas enumeradas.


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="
<a href="http://www.w3.org/2001/XMLSchema-instance">http://www.w3.org/2001/XMLSchema-instance
</a>"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
<a href="http://maven.apache.org/maven-v4_0_0.xsd">http://maven.apache.org/maven-v4_0_0.xsd
</a>">



       
<modelVersion>4.0.0</modelVersion>
       
<groupId>com.autentia</groupId>

       
<artifactId>autentiaNegocio</artifactId>

       
<packaging>jar</packaging>
       
<version>1.0-SNAPSHOT</version>
       
<name>Maven Quick Start Archetype</name>
       
<dependencies>
               
<dependency>

                       
<groupId>junit</groupId>
                       
<artifactId>junit</artifactId>
                       
<version>4.12</version>
                       
<scope>test</scope>
               
</dependency>
               
<dependency>
                       
<groupId>log4j</groupId>
                       
<artifactId>log4j</artifactId>

                       
<version>1.2.17</version>
               
</dependency>
       
</dependencies>

</project>

3.2. Transitividad de las dependencias

Dentro del ámbito de cada dependencia podemos indicar su transitividad mediante la etiqueta scope, dando información sobre dónde se encuentra disponible la dependencia y sobre cuándo será necesaria. Este scope podrá ser de 6 tipos distintos:

  • compile: Es el valor por defecto, el que se aplica si no especificamos ningún scope. Con este modo, las dependencias se propagarán a todos los proyectos dependientes, y estarán disponibles en todos los classpath.
  • runtime: La dependencia solo es necesaria en tiempo de ejecución, no en tiempo de compilación.
  • test: La dependencia solo es necesaria en tiempo de compilación y en la ejecución de tests.
  • provided: La dependencia será encontrada en la JDK o en un contenedor en tiempo de ejecución.
  • system: Funciona de manera similar al scope provided, pero es necesario proporcionar el JAR.
  • import: Se utiliza para importar dependencias definidas en otros pom.xml.

Es importante saber qué scope debemos asignar a cada una de las dependencias, ya que un scope erróneo puede dar lugar a errores en la aplicación, tanto en tiempo de compilación como en tiempo de ejecución.


3.3. Exclusión de librerías

También podemos excluir ciertas librerías al incluir una dependencia, con la etiqueta exclusions. Aquí tenemos un ejemplo, donde estamos excluyendo la librería jms de la dependencia oscache:

<dependency>
        <groupId>opensymphony</groupId>

        <artifactId>oscache</artifactId>

        <version>2.4</version>


        <exclusions>
                <exclusion>

                        <groupId>javax.jms</groupId>
                        <artifactId>jms</artifactId>

                </exclusion>
        </exclusions>

</dependency>

3.4. ¿Y cuáles son las dependencias que debemos añadir?

Seguramente tengamos en nuestro proyecto una carpeta llamada lib, que contiene un montón de librerías en formato jar. Basta con buscar cada una de esas librerías en la web http://mvnrepository.com/ y copiar la referencia a dicha dependencia (que aparece entre las etiquetas dependency).

Si en algún momento tenéis dudas o problemas sobre las dependencias a importar para una clase en concreto, no dejéis de visitar el tutorial de Iván Zaera sobre Obtención de información de clases Java en aplicaciones web. En él, nos propone una página JSP en la que se vuelca mediante reflexión toda la información disponible sobre cualquier clase.

Tras completar todas las dependencias en el fichero pom.xml podremos prescindir de los ficheros de librerías.

4. El proyecto parent y sus módulos

Dentro de un mismo proyecto pueden existir varios módulos, funcionando cada uno de ellos como proyectos más pequeños. Denominaremos proyecto parent al proyecto que los engloba. En su fichero pom.xml deberá aparecer la referencia a todos esos módulos a través de la etiqueta modules, de la siguiente forma:


<modules>

        
<module>modulo-uno
</module>
        
<module>modulo-dos
</module>
        
<module>modulo-tres
</module>

</modules>

Además, en el pom.xml de cada módulo deberá aparecer la referencia al proyecto padre. 



<parent>

        
<groupId>com.autentia
</groupId>

        
<artifactId>autentiaNegocio
</artifactId>

        
<version>1.0.0-SNAPSHOT
</version>

</parent>



Como podemos ver en el ejemplo, en la referencia al proyecto padre aparece la versión del mismo. No será necesario añadir de nuevo la etiqueta version, pues la versión será la misma.



Cada módulo deberá tener la estructura típica de Maven, mientras que el proyecto parent constará de un directorio que contenga su propio fichero pom.xml y una carpeta por cada módulo.

5. Los repositorios de dependencias

En caso de disponer de un repositorio remoto donde se alojen parte de las dependencias, como por ejemplo un Nexus, debemos añadirlo en la especificación del proyecto. Para ello, añadimos la referencia en el fichero pom.xml de la siguiente forma:


<repositories>

        
<repository>

                
<id>autentia-repository
</id>

                
<name>Autentia Repository
</name>

                
<url>url-del-repositorio
</url>
        
</repository>

</repositories>

Podemos añadir tantos repositorios como sean necesarios.

6. Configurando el proyecto para liberar versiones

Si queremos configurar el proyecto Maven para que al generar una release se libere una nueva versión, subiendo la etiqueta de la misma, será necesario indicar el path del repositorio remoto. Esto se hace a través de la etiqueta Maven scm, como podemos ver a continuación:


<scm>

        
<connection>
                scm:git:http://localhost/repoAutentia.git

        
</connection>



        
<developerConnection>

                scm:git:http://localhost/repoAutentia.git
        
</developerConnection>

        
<url>
                http://localhost/repoAutentia.git

        
</url>

</scm>

También debemos indicar la referencia al repositorio que almacena cada release del proyecto:


<distributionManagement>

        
<repository>
                
<id>repoAutentia
</id>
                
<name>Nombre del repositorio
</name>

                
<url>http://localhost/repositorio
</url>

        
</repository>

</distributionManagement>

7. Definiendo los plugins para compilar y empaquetar

Ya solo nos queda configurar la compilación y empaquetado del código de nuestro proyecto. Gracias a los plugin de Maven podemos hacer que nuestra configuración sea lo más completa o personalizada posible, definiendo a través de los mismos de qué manera queremos que Maven haga las cosas. Para configurar nuestro proyecto añadiremos las etiquetas build y plugins, incluyendo dentro del ámbito de las mismas todos los plugins que creamos necesarios.



Vamos a ver un ejemplo de configuración, en la que indicamos que se compile el código fuente con la versión de Java 1.7.


<build>
        
<plugins>
                
<plugin>

                        
<groupId>org.apache.maven.plugins
</groupId>

                        
<artifactId>maven-compiler-plugin
</artifactId>

                        
<version>3.5.1
</version
>

                        
<configuration>
                                
<source>1.7
</source>
                                
<target>1.7
</target>

                        
</configuration>

                
</plugin>

        
</plugins>

</build>

Maven tiene muchísimos otros plugins, disponibles a través del enlace https://maven.apache.org/plugins/index.html. Otro ejemplo de configuración es el siguiente: si queremos copiar ciertos recursos al directorio de salida del proyecto, podemos utilizar el plugin maven-resources-plugin, como se muestra a continuación:

<plugin>

        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-resources-plugin</artifactId>

        <version>3.0.1</version>

        <executions>
                <execution>

                        <id>copy-project-resources</id>
                        <phase>generate-resources</phase>

                        <goals>
                                <goal>copy-resources</goal>
                        </goals>

                        <configuration>

                        <outputDirectory>

                                ../MavenProject/target/generated-resources

                        </outputDirectory>

                                <overwrite>true</overwrite>
                                <resources>
                                        <resource>

                                                <directory>

                                                        ../MavenProject/src/main/resources/

                                                </directory>
                                                <includes>

                                                        <include>resource1.txt</include>
                                                        <include>resource2.txt</include>

                                                </includes>

                                        </resource>

                                </resources>
                        </configuration>

                </execution>
        </executions>

</plugin>

En este ejemplo estamos copiando los recursos resource1.txt y resource2.txt del directorio ../MavenProject/src/main/resources/ al directorio ../MavenProject/target/generated-resources.

Por supuesto, en último lugar queda resolver todos los errores de compilación que nos reporte nuestro entorno de desarrollo, al igual que los errores sucedidos en tiempo de ejecución en nuestro servidor.

8. Conclusiones

Mavenizar un proyecto no es algo trivial. Tras haber seguido todos los pasos recogidos en este tutorial nos habremos dado cuenta de que cada proyecto tiene sus propias características y requiere una serie de acciones, pero tras acabar el proceso su estructura será muy similar a la de cualquier otro proyecto basado en Maven.

A partir de ahora, ahorraremos mucho tiempo en la gestión de nuestro proyecto gracias a esta herramienta. Y lo mejor de todo, trabajar en él será muchísimo más cómodo, pues podremos realizar de forma automática tareas que hasta ahora hacíamos manualmente.

Monitorización y análisis de rendimiento de aplicaciones con InspectIT APM

$
0
0

Después de investigar sobre herramientas open source de APM nos encontramos con inspectIt, un proyecto con licencia Apache 2.

Monitorización y análisis de rendimiento de aplicaciones con InspectIT APM.


0. Índice de contenidos.


1. Introducción

Después de trabajar con New Relic, AppDynamics o Dynatrace y haber trasteado también con PinPoint, en este tutorial vamos a probar otra herramienta open source llamada inspectIt.

Se basa en los mismos principios que el resto, tiene un agente, un server (CMR) y un cliente basado, esta vez, en la plataforma Eclipse.

2. Entorno.

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 15′ (2.5 GHz Intel Core i7, 16GB DDR3).
  • Sistema Operativo: Mac OS El Capitan 10.11
  • InspectIt 1.7.3

3. Instalación.

Como casi todas, podemos realizar una descarga del producto dockerizado o realizar una instalación, accediendo a la página de descarga http://www.inspectit.rocks; para el objetivo de este tutorial vamos a realizar una instalación, seleccionando primero el sistema operativo.

inspectit-01

y después la versión

inspectit-02

una vez descargado, podemos acceder al paquete de instalación ejecutando el siguiente comando:

macbook-jmsanchez:Downloads jmsanchez$ java -jar inspectit.installer-all.macosx.x64-1.6.9.83.jar

Que lanzará un wizard de instalación

inspectit-03

Aceptamos la licencia

inspectit-04

Seleccionamos los componentes a instalar

inspectit-05

Y el directorio de instalación

inspectit-06

Que, como no existe, se creará

inspectit-07

Una vez instalados los paquetes

inspectit-08

La instalación ha finalizado

inspectit-09

Sólo nos queda arrancar el servidor (CMR):

macbook-jmsanchez:CMR jmsanchez$ ./startup.sh
2016-09-16 16:07:25,216: 214    [           main] INFO      rocks.inspectit.server.CMR - Central Measurement Repository is starting up!
2016-09-16 16:07:25,218: 216    [           main] INFO      rocks.inspectit.server.CMR - ==============================================

...

2016-09-16 16:07:32,251: 7249   [           main] INFO      rocks.inspectit.server.CMR - Spring successfully initialized
2016-09-16 16:07:32,252: 7250   [           main] INFO      rocks.inspectit.server.CMR - Starting CMR in version 1.6.9.83. Please note that inspectIT does not provide any guarantee on backwards compatibility. Only if the version match exactly we ensure that the components are compatible.
2016-09-16 16:07:32,252: 7250   [           main] INFO      rocks.inspectit.server.CMR - CMR started in 7036.505 ms

Y configurar el agente en un servidor para que comience a enviar información añadiendo en los parámetros de arranque los siguientes argumentos:

-javaagent:/Applications/development/inspectIT/agent/inspectit-agent.jar  -Dinspectit.repository=localhost:9070 -Dinspectit.agent.name=TOMCAT_9

Debería quedar como sigue:

inspectit-10

Tras navegar por la aplicación comenzará a enviar información al servidor. Podemos comprobar en los logs de arranque del propio servidor información sobre la conectividad.

sep 16, 2016 11:27:31 PM rocks.inspectit.agent.java.javaagent.JavaAgent premain
INFO: inspectIT Agent: Starting initialization...
23:27:33.149 [main] INFO rocks.inspectit.agent.java.SpringAgent - Location of inspectit-agent.jar set to: /Applications/development/inspectIT.1.7/agent/inspectit-agent.jar
2016-09-16 16:27:33,569: [inspectIT] 446    [           main] INFO  spectit.agent.java.SpringAgent - Initializing Spring on inspectIT Agent...
2016-09-16 16:27:34,296: [inspectIT] 1173   [           main] INFO  nfig.impl.ConfigurationStorage - Repository information found in the JVM parameters: IP=localhost Port=9070
2016-09-16 16:27:34,296: [inspectIT] 1173   [           main] INFO  nfig.impl.ConfigurationStorage - Repository definition added. Host: localhost Port: 9070
2016-09-16 16:27:34,296: [inspectIT] 1173   [           main] INFO  nfig.impl.ConfigurationStorage - Agent name found in the JVM parameters: AgentName=TOMCAT_8
2016-09-16 16:27:34,319: [inspectIT] 1196   [           main] INFO  nfig.impl.ConfigurationStorage - Agent name set to: TOMCAT_8
2016-09-16 16:27:34,409: [inspectIT] 1286   [           main] INFO  izer.schema.ClassSchemaManager - ||-Class Schema Manager started..
2016-09-16 16:27:34,751: [inspectIT] 1628   [           main] INFO  nection.impl.KryoNetConnection - KryoNet: Connecting to localhost:9070
2016-09-16 16:27:36,598: [inspectIT] 3475   [           main] INFO  nection.impl.KryoNetConnection - KryoNet: Connection established!
2016-09-16 16:27:36,863: [inspectIT] 3740   [           main] INFO  nfig.impl.ConfigurationStorage - Agent configuration added with following configuration interface properties:
2016-09-16 16:27:36,863: [inspectIT] 3740   [           main] INFO  nfig.impl.ConfigurationStorage - Assigned environment: Default Environment
2016-09-16 16:27:36,863: [inspectIT] 3740   [           main] INFO  nfig.impl.ConfigurationStorage - Active profiles:
2016-09-16 16:27:36,864: [inspectIT] 3741   [           main] INFO  nfig.impl.ConfigurationStorage - |-[Common] SQL
2016-09-16 16:27:36,864: [inspectIT] 3741   [           main] INFO  nfig.impl.ConfigurationStorage - |-[Common] HTTP
2016-09-16 16:27:36,864: [inspectIT] 3741   [           main] INFO  nfig.impl.ConfigurationStorage - |-[Common] Exclude Classes
2016-09-16 16:27:36,864: [inspectIT] 3741   [           main] INFO  nfig.impl.ConfigurationStorage - Options:
2016-09-16 16:27:36,864: [inspectIT] 3741   [           main] INFO  nfig.impl.ConfigurationStorage - |-class loading delegation: true
2016-09-16 16:27:36,864: [inspectIT] 3741   [           main] INFO  nfig.impl.ConfigurationStorage - |-enhanced exception sensor: false
2016-09-16 16:27:36,864: [inspectIT] 3741   [           main] INFO  nfig.impl.ConfigurationStorage - Class-cache exists on the server: true
2016-09-16 16:27:36,865: [inspectIT] 3742   [           main] INFO  nfig.impl.ConfigurationStorage - Number of initially instrumented classes: 26
2016-09-16 16:27:37,354: [inspectIT] 4231   [           main] INFO  spectit.agent.java.SpringAgent - Spring successfully initialized
2016-09-16 16:27:37,355: [inspectIT] 4232   [           main] INFO  spectit.agent.java.SpringAgent - Using agent version 1.7.3.86.

4. Un vistazo rápido.

Dentro de la carpeta inspectit del directorio de instalación encontraremos un ejecutable para arrancar el cliente de monitorización que debería mostrar una interfaz como la siguiente.

inspectit-11

Accediendo a local CMR podremos comprobar como se muestra la información remitida por el agente.

inspectit-12

Pulsando sobre una transacción podemos acceder a su drill down

inspectit-13

La información se puede almacenar

inspectit-14

Creando un almacén

inspectit-15

Seleccionado un agente para almacenar esa información

inspectit-16

La información a almacenar

inspectit-17

E indicando o no un límite para la monitorización

inspectit-18

Podemos filtrar la información sobre las transacciones de entre un rango horario

inspectit-19

Pulsando sobre botón derecho > detalles podemos acceder a la siguiente información

inspectit-20

La información a monitorizar se puede configurar añadiendo más perfiles y modificándolos

inspectit-21

Si pulsamos sobre Http timer data, podemos acceder a la infomación agregada por URLs

inspectit-22

En una aplicación con más tráfico se mostraría una información como la siguiente

inspectit-23

Desde esa vista podemos acceder a la información individual sobre cada una de las transacciones realizadas desde esa misma URL

inspectit-24

En una transacción con acceso a base de datos el drill down se mostrará como sigue:

inspectit-25

Y pulsando sobre la pestaña inferior de SQL podremos acceder a la siguiente información:

inspectit-26

Si accedemos al detalle de la SQL ejecutada podemos ver una información como la siguiente:

inspectit-27

Por último podemos visualizar la información en forma de gráfico si seleccionamos las transacciones botón derecho > “Navigate to” > “Display in Chart”

inspectit-28

Que mostraría un gráfico como el siguiente:

inspectit-29

5. Referencias.


6. Conclusiones.

Le falta un componente muy interesante que sí tiene el resto de APMs con los que hemos trabajado hasta ahora: el dashboard de componentes, aquél que muestra todos los servidores y motores de bases de datos monitorizados.

Del mismo modo que con PinPoint, le haremos un seguimiento.

Un saludo.

Jose

Colecciones y Vistas en Backbone.js

$
0
0

Índice de contenidos.


1. Introducción.

En los pasados tutoriales de Backbone que os he ido posteando (Introducción a Backbone.js, Eventos y Modelos en Backbone.js ), hicimos una breve introducción sobre cuales eran las bases que componen este potentísimo framework y después extendimos contenido sobre cómo manejar los modelos de una aplicación y cómo hacer para que los distintos componentes de la misma “hablen” entre sí.

Pues bien, en este tutorial hablaremos sobre colecciones de modelos y vistas. Veremos cómo podemos gestionar los modelos a través de colecciones ordenadas y cómo podremos mostrar los datos de nuestra aplicación al usuario a través de las vistas.


2. Colecciones.

Para poder desarrollar nuestras aplicaciones, Backbone nos proporciona un potente componente llamado ‘Collection’, el cual nos permitirá gestionar de una manera muy sencilla colecciones ordenadas de modelos.

Al igual que los modelos, las colecciones lanzarán eventos en nuestra aplicación cada vez que éstas, o los modelos que contienen, cambien de estado. Después veremos la lista de eventos que las colecciones lanzarán por defecto a lo largo del ciclo de vida de las mismas.

Para poder crear una colección en nuestra aplicación, al igual que con los modelos, haremos uso de la función ‘extend’:

var Aula = Backbone.Collection.extend({});

model

Para indicar a la colección el tipo de modelo que va a gestionar se debe de sobrescribir el atributo ‘model’ indicando el tipo de modelo que va a gestionar:

var Alumno = Backbone.Model.extend({});
var Aula = Backbone.Collection.extend({
	model: Alumno
});

Creación de colecciones

Cuando creamos una colección tenemos varias opciones. Podemos crear la colección vacía y después añadir los modelos:

var Alumno = Backbone.Model.extend({}),
    alumno1 = new Alumno(),
    alumno2 = new Alumno();

var Aula = Backbone.Collection.extend({
		model: Alumno
	}),
	aula = new Aula();

aula.add(alumno1);
aula.add(alumno2);

O bien pasarle a la colección el conjunto de modelos que va a gestionar a través de aun array de modelos:

aula.add([alumno1, alumno2]);

models

Una vez creada la colección podemos recuperar los modelos que contiene la misma recuperando el atributo ‘models’, que nos devolverá un array con los modelos que contiene.

aula.models;

add

Una vez creada la colección usaremos la función ‘add’ para añadir modelos a la misma. Esta función puede recibir por parámetro el modelo en cuestión o un array con el conjunto de modelos a añadir a la colección.

remove

Para eliminar modelos de la colección, usaremos la función ‘remove’, la cual elimina el modelo o array de modelos que le pasemos por parámetro:

var aula = new Aula();
aula.add(alumno1);
aula.add(alumno2);
console.log('Numero de alumnos en la colección: ' + aula.size());

aula.remove(alumno1);
console.log('Numero de alumnos en la colección tras eliminar modelo: ' + aula.size());

aula.add(alumno1);
aula.remove([alumno1, alumno2]);
console.log('Numero de alumnos en la colección tras eliminar modelo: ' + aula.size());

Si lo que queremos es vaciar la colección, directamente usaremos la función ‘reset’.

set

Para actualizar modelos de la colección usaremos la función ‘set’. Esta función recibe por parámetro el modelo que vayamos a modificar. De igual manera que con la función ‘add’, puede recibir un array de modelos a actualizar. Si el modelo o modelos que la colección recibe no existe en su interior, la acción con ese modelo pasa automáticamente a ser una creación.

Debemos de tener cuidado con esta función ya que si la colección contiene modelos que no están presentes en los modelos que se pasan por parámetro, éstos, automáticamente serán eliminados de la colección.

var Alumno = Backbone.Model.extend({idAttribute: 'nombre'}),
    alumno1 = new Alumno({nombre: 'Isma'}),
    alumno2 = new Alumno({nombre: 'Alfonso'});

var Aula = Backbone.Collection.extend({
		model: Alumno
	}),
	aula = new Aula();

aula.add(alumno1);
aula.add(alumno2);
console.log('Numero de alumnos en la colección: ' + aula.size());

alumno1.set({apellidos: 'Fernandez Molina'});
aula.set(alumno1);
console.log('Numero de alumnos en la colección: ' + aula.size());

alumno1.set({apellidos: 'Lopez Garcia'});
alumno2.set({apellidos: 'Garcia Montes'});
aula.set([alumno1, alumno2]);
console.log('Numero de alumnos en la colección: ' + aula.size());

get

Podemos recuperar modelos de la colección usando la función ‘get’, la cual recibe por parámetro del valor del ID del modelo que queremos buscar:

var Alumno = Backbone.Model.extend({
		idAttribute: 'nombre'
	}),
    alumno1 = new Alumno({nombre: 'Isma'}),
    alumno2 = new Alumno({nombre: 'Alfonso'});

var Aula = Backbone.Collection.extend({
		model: Alumno
	}),
	aula = new Aula();

aula.add(alumno1);
aula.add(alumno2);

aula.get('Alfonso');
aula.get('Rodrigo');

at

Cuando tenemos una colección ordenada de modelos es interesante poder recuperar modelos que se encuentren en alguna posición determinada. Para ello, tenemos la función ‘at’ la cual recibe la posición en la cual se debe de encontrar el modelo en cuestión.

push/pop

Podemos hacer uso de la colección como si de una pila se tratase. Backbone nos proporciona dos funciones que nos permiten meter y sacar modelo de la ‘pila’:

  • Con la función ‘push(model)’ meteremos un modelo al final de la colección. Recibe por parámetro el modelo que se va a meter en la colección.
  • Con la función ‘pop()’ sacamos y eliminamos el último modelo de la colección

unsifht/shift

Existen otras dos funciones que hacen exactamente lo mismo que ‘push’ y ‘pop’. La única diferencia es que estas funciones trabajan sobre la cabecera de la colección en vez de trabajar sobre la cola.

Ordenación de las colecciones

Hemos hablado anteriormente de que la colecciones son conjuntos ordenados de modelos. Pues bien, Backbone, por defecto, no ordena nuestra colección a no ser que le digamos cómo hacerlo. Para ello debemos de indicarle, a través de la función ‘comparator’, el campo, o campos, del modelo por el cual se va a ordenar.

var Alumno = Backbone.Model.extend({
        idAttribute: 'nombre'
    }),
    alumno1 = new Alumno({nombre: 'Alfonso', apellidos: 'Garcia Montes'}),
    alumno2 = new Alumno({nombre: 'Isma', apellidos: 'Fernandez Molina'}),
    alumno3 = new Alumno({nombre: 'Rodrigo', apellidos: 'Garcia Alvarez'});

var Aula = Backbone.Collection.extend({
        model: Alumno
    }),
    aula = new Aula();

aula.comparator = function(model) {
    return model.get('apellidos');
};

aula.add(alumno1);
aula.add(alumno2);
aula.add(alumno3);

console.log(aula.models);

Usaremos la función ‘sort’ para forzar a una colección a que se re-ordene. Aunque si hemos definido un ‘comparator’ no será necesario llamar a la función ‘sort’ ya que se ordenará de forma automática.

Búsqueda en colecciones

Para realizar búsqueda en colecciones, Backbone nos proporciona varias funciones.

var Alumno = Backbone.Model.extend({
    idAttribute: 'idAlumo'
    }),
    alumno1 = new Alumno({idAlumno: 1, nombre: 'Alfonso', apellidos: 'Garcia Montes'}),
    alumno2 = new Alumno({idAlumno: 2, nombre: 'Ismael', apellidos: 'Fernandez Molina'}),
    alumno3 = new Alumno({idAlumno: 3, nombre: 'Ismael', apellidos: 'Garcia Alvarez'});

var Aula = Backbone.Collection.extend({
        model: Alumno
    }),
    aula = new Aula();

aula.add([alumno1, alumno2, alumno3]);

La función where devuelve un array con todos los modelos que tengan un ‘match’ con la búsqueda en cuestión. Recibe por parámetro un hash con los parámetros de búsqueda:

aula.where({nombre: 'Ismael'});
aula.where({nombre: 'Ismael', apellidos: 'Fernandez Molina'});

La función findWhere realiza el mismo tipo de búsqueda que ‘where’, sólo que en vez de devolver todos los ‘match’ sólo devuelve el primer modelo que se encuentre el match’. Al igual que la función ‘where’, ésta recibe un hash con los parámetros de búsqueda.

Eventos

En el anterior tutorial, eventos y modelos en backbone, os comenté que los modelos de Backbone lanzan eventos, por defecto, cada vez que van cambiando a lo largo de su ciclo de vida. Pues bien, con las colecciones ocurre exactamente lo mismo. Cada vez que una colección o un modelo, que ésta contiene, cambia su estado, hay una serie de eventos que son lanzados al contexto de la aplicación:

  • add: cuando un modelo es añadido a una colección.ó
  • remove: cuando un modelo es eliminado de una colección.ó
  • update: cuando añadimos o eliminamos modelos de una colección.ó
  • reset: cuando ‘reseteamos’ el contenido de una colección.ó
  • sort: cuando una colección se ha reordenado (probad a añadir un modelo a una colección que ya está ordenada).ó
  • destroy: cuando la instancia de un modelo, que contiene la colección, es eliminada.ó
  • request: cuando se lanza una petición hacia el servidor.ó
  • sync: cuando la colección se ha sincronizado correctamente con el servidor.
  • error: cuando falla una petición contra el servidor.

Como habéis podido ver, Backbone nos proporciona un montón de funciones que podemos usar para gestionar las colecciones de nuestra aplicación. Que duda cabe, que hay mchas más funciones. Aquí os he dejado las básicas y por las que podéis empezar a hacer vuestros pinitos.


3. Vistas.

Hasta este punto hemos aprendido cómo gestionar los modelos y colecciones de nuestra aplicación y cómo podemos comunicarnos con los diferentes componentes de nuestra aplicación a través de eventos. Nos quedaría ver qué nos proporciona Backbone para mostrar al usuario toda esa información. Pues bien, para eso, están las vistas.

Para usarlas nos apoyaremos sobre librerías que nos permitan usar plantillas HTML (Underscore.js), para así, mostrar al usuario todos los datos que nuestra aplicación gestionará por detrás.

Para crear nuestra primera vista, al igual que con los demás componentes, usaremos la palabra reservada ‘extend’:

var AlumnosContainer = Backbone.View.extend({});

tagName

Con la sentencia anterior ya habremos creado nuestra primera vista. Por defecto, si no le indicamos nada, Backbone traducirá esa vista como un elemento HTML de tipo ‘div’ (<div></div>).

Para crear cualquier otro tipo de elemento HTML, debemos de sobrescribir el atributo ‘tagName’. En el siguiente ejemplo vamos a crear una lista (<ul></ul>):

var AlumnosContainer = Backbone.View.extend({
    tagName: 'ul'
});

className

Si sobrescribimos el atributo ‘className’, estaremos asignando asociando estilos (definidos en las hojas de estilos), al elemento que representa la vista en cuestión:

var AlumnosContainer = Backbone.View.extend({
    className: 'container color-dark'
});

El resultado HTML de la anterior vista sería la siguiente: <div class=’container color-dark’></div>

el’ y ‘$el

Toda vista contendrá las propiedades ‘el’ y ‘$el’, mediante las cuales podemos hacer referencia al DOM de la propia vista. A través de ‘el’ accederemos al DOM de la vista, mientras que con ‘$el’ nos recupera un objeto jQuery que encapsulará el propio DOM.

var AlumnosContainer = Backbone.View.extend({
    className: 'container color-dark'
});

var alumnosContainer = new AlumnosContainer();

alumnosContainer.el;
alumnosContainer.$el;

Cuando creamos una vista, el DOM de ésta se genera fuera del DOM del navegador. Por lo que será necesario asociar al DOM del navegador las vistas que queramos renderizar en cada momento (esto lo veremos de forma más detallada en tutoriales futuros).

template

Con este atributo indicamos a Backbone cual será la plantilla HTML que usará la vista para mostrar los datos al usuario. Como hemos comentado varias veces, Backbone no da soporte a plantillas por lo que usaremos librerías de terceros para usarlas. En este caso Underscore.js (Cuando programemos la aplicación completa ya veremos, en detalle, cómo usarla).

Mediante la función ‘template’ de underscore (representado por ‘_’), indicaremos cual es la plantilla a asociar a la vista. Como primer parámetro recibe la plantilla en cuestión y como segundo los datos que incrustaremos en la plantilla HTML.

var AlumnosContainer = Backbone.View.extend({

    className: 'container color-dark',

    template: _.template('Hola <%=nombre%>', {nombre: 'Ismael'})

});

Si os dais cuenta, la función ‘template’ está recibiendo como primer parámetro la plantilla en cuestión y, como segundo, los parámetros que serán usados dentro de la plantilla, en este caso, un hash con el atributo ‘nombre’, cuyo valor será incrustado en la plantilla.

Underscore.js nos permite que definamos plantillas HTML identificadas por ‘id’ de manera que podamos hacer referencia a ellas mediante dicho ‘id’. Con esto nos ahorraríamos tener que estar escribiendo un string con todo el contenido de la misma. Por ahora prefiero que os quedéis con la clave de su uso y dejemos para el siguiente tutorial el profundizar sobre este tema.

render

Backbone llama a la función ‘render’ cada vez que la vista es renderizada. Sobrescribiremos esta función para renderizar la plantilla que hemos definido previamente. (Por convención se deberá de lanzar un ‘return this;’ al final de esta función:)

var AlumnosContainer = Backbone.View.extend({

    className: 'container color-dark',

    template: _.template('Hola ', {nombre: 'Ismael'}),

    render: function() {
        this.$el.html(this.template());
        return this;
    }
});

En la función anterior lo que se está haciendo es sustituir el DOM actual de la vista (por defecto es vacío), por el DOM que nos generará la plantilla creada previamente.

events

Como hemos aprendido en tutoriales previos, la comunicación entre los distintos componentes de nuestra aplicación será a través de eventos. Cada uno de los componentes de la aplicación lanzará eventos que serán escuchados por otros, que cambiarán su estado según proceda.

La suscripción a los eventos se hace a través de la función ‘on’ y el lanzamiento con la función ‘trigger’. Pero que pasa si queremos que nuestra vista reaccione a determinados eventos que se produzcan dentro del DOM que ésta gestiona?.

Pues bien, para eso tenemos el atributo ‘events’. Este atributo contendrá un hash que definirá qué eventos, del DOM de la misma, serán los que escuche la vista (un click en un botón, onblur de un campo, pulsación de una tecla, etc.). El hash contendrá la definición del evento y la función que se ejecutará cuando dicho evento se produzca:

var AlumnosContainer = Backbone.View.extend({

    className: 'container color-dark',

    template: _.template(templateHTML, {nombre: 'Ismael'}),

    render: function() {
        this.$el.html(this.template());
        return this;
    },

    events: {
        'click .icon-add': 'addAlumno',
        'click .icon-remove': 'removeAlumno',
    },

    addAlumno: function() {
        console.log('se va a añadir un alumno');
    },

    removeAlumno: function() {
        console.log('se va a eliminar un alumno');
    }
});

En este ejemplo se lanzará la función ‘addAlumno’ cuando el usuario haga click sobre un elemento que contenga un class del tipo ‘icon-add’. Del mismo modo lanzará la función ‘removeAlumno’ cuando se haga click sobre un elemento que contenga un class del tipo ‘icon-remove’.


4. Conclusiones.

Tras este tutorial habéis podido comprobar cómo, de manera sencilla, somos capaces de gestionar colecciones de modelos, las cuales nos serán muy útiles a la hora de desarrollar nuestras aplicaciones.

Como hemos comentado en varias ocasiones, Backbone no nos da soporte para plantillas HTML. Debido a ello necesitaremos usar librerías de terceros. Como Backbone depende de Underscore.js nos aprovecharemos de la misma para usar su sistema de plantillas.

Cierto es que no he profundizado mucho sobre el sistema de plantillas de Underscore.js o el desarrollo de las vistas con Backbone. En realidad lo que quiero es que os vayan sonando los conceptos, funciones, atributos y demás elementos de cada componente ya que en el próximo tutorial desarrollaremos una aplicación completa con Backbone y será ahí donde consolidaremos cada uno de los conceptos que hemos ido viendo a lo largo de estos tutoriales.

Primeros pasos con JUnit 5

$
0
0

Índice de contenidos

Logo de Junit 5

1. Introducción

Junit 5 es la nueva versión del conocidísimo framework para la automatización de pruebas en Java. Ahora mismo se encuentra en su Milestone 2 (publicada el 23 julio de 2016), con lo que no esperamos que haya cambios significativos de aquí a su lanzamiento definitivo, simplemente corrección de bugs y cosas menores.

Esta versión 5 se apoya mucho en las novedades de Java 8, cómo son las lambdas, y en este tutorial vamos a hacer un repaso por sus principales características y empezar a descubrir las novedades que trae.

2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 15” (2.5 GHz Intel i7, 16GB 1600 Mhz DDR3, 500GB Flash Storage).
  • AMD Radeon R9 M370X
  • Sistema Operativo: Mac OS X El Capitan 10.11.6
  • Java v1.8.0_102
  • Maven v3.3.9
  • Gradle v3.0
  • JUnit v5-M2

3. Nombrado de los tests

@DisplayName("Aserciones soportadas")
class AssertionsTest {

    @Test
    @DisplayName("Verdadero o falso 😱")
    void true_or_false() {
        ...
    }

    @Test
    @DisplayName("Existencia de un objeto")
    void object_existence() {
        ...
    }
}

Igual que en JUnit 4, las clases de test acaban con el sufijo Test. Y también marcamos los métodos de test con la anotación @Test. Ojo aquí porque Junit 5 tiene su propia anotación org.junit.jupiter.api.Test así que ojo con no poner la de JUnit 4 porque en tal caso el runner de JUnit 5 no ejecutará estos tests.

También llama la atención la posibilidad de usar la anotación @DisplayName para indicar el nombre del test mediante un texto libre (como cosa curiosa vemos que podemos usar emoticonos dentro del @DisplayName).

Esto nos sirve para que las herramientas presenten este texto en lugar del nombre del método.  

JUnit 5 @DisplayName in IntelliJ
 

Para mi la verdad es que esta anotación no aporta demasiado valor, ya que con el nombre del método debería ser más que suficiente. Si además del nombre del método ponemos la anotación, en cierto modo estamos duplicando información, rompiendo así el principio DRY. Otra opción sería poner el @DisplayName y como nombre del tests poner sólo t1(), t2(), …​ pero esto tampoco parece que tenga sentido ya que en muchos sitios lo que se va a mostrar es sólo el nombre del método por lo que sería muy poco descriptivo.

Para mi lo mejor es no usar la anotación @DisplayName y poner nombres que indiquen la intención del tests, cómo hacerReserva(), tramitarPedido(), …​ Si queremos que los nombres sean un poco más legibles podemos usar guiones bajos: hacer_reserva(), tramitar_pedido(), …​

Para mi los nombres de los tests han de ser lo más genéricos posibles para que podamos refactorizar el test y no tengamos que cambiar el nombre. Es decir, siempre intento evitar poner detalles de, por ejemplo, en qué consiste la fixture, o qué valores concretos voy comprobar en las aserciones. Aunque esto en principio puede parecer buena idea, a la larga lo que conseguimos es que estas cosas cambien y que el nombre del test, como buena documentación qué es, acabe mintiendo. Además no nos engañemos, cuando un test falla, el nombre te debe dar una idea de lo que está ocurriendo, pero siempre vamos a ir al código del test a ver exactamente qu´ es lo que se está haciendo.

4. Mejora en el diseño de las clases de test

Una cosa que puede pasar desapercibida del código anterior es que ni la clase y ni los métodos de test necesitan ser públicos (public).

Esto no tiene un impacto directo en la forma en la que escribimos los tests, pero sí es una mejora desde el punto de vista del diseño y de la semántica de los propios tests, ya que estas clases de tests no están concebidas para ser instanciada o para llamar a sus métodos, si no es por el propio framework de JUnit.

5. Ciclo de vida

class LyfecicleTest {

    @BeforeAll
    static void initAll() {
    }

    @BeforeEach
    void init() {
    }

    @Test
    void regular_testi_method() {
        ...
    }

    @Test
    @Disabled("este tests no se ejecuta")
    void skippedTest() {
        // not executed
    }

    @AfterEach
    void tearDown() {
    }

    @AfterAll
    static void tearDownAll() {
    }
}

Vemos que tenemos la mismas anotaciones que con JUnit 4 para determinar qué métodos se tienen que ejecutar antes y después de todos o cada uno de los test.

La única diferencia con la versión anterior es la forma de hacer que un test no se ejecute, mientras en la versión 4 era con @Ignore, ahora en la versión 5 lo haremos con @Disabled, como podemos ver en la línea 17.

6. Aserciones

class AssertionsTest {

    @Test
    void standardAssertions() {
        assertEquals(2, 2);
        assertEquals(4, 4, "Ahora el mensaje opcional de la aserción es el último parámetro.");
        assertTrue(2 == 2, () -> "Al usar una lambda para indicar el mensaje, "
                + "esta se evalúa cuando se va a mostrar (no cuando se ejecuta el assert), "
                + "de esta manera se evita el tiempo de construir mensajes complejos innecesariamente.");
    }

    @Test
    void groupedAssertions() {
        // En un grupo de aserciones se ejecutan todas ellas
        // y ser reportan todas los fallos juntos
        assertAll("user",
            () -> assertEquals("Francisco", user.getFirstName()),
            () -> assertEquals("Pérez", user.getLastName())
        );
    }

    @Test
    void exceptionTesting() {
        Throwable exception = expectThrows(IllegalArgumentException.class, () -> {
            throw new IllegalArgumentException("a message");
        });
        assertEquals("a message", exception.getMessage());
    }
}

Disponemos de los mismos Assert de JUnit 4, con una algunas peculiaridades, como que ahora tanto la expresión como el mensaje del Assert, se pueden indicar mediante una lambda de Java 8. Se puede ver un ejemplo en la línea 7 o 17.

Otra novedad es que podemos indicar grupos de Asserts mediante assertAll. La gracia de estos grupos de Asserts es que siempre se ejecutan todos, y que luego los fallos que se hayan producido también se van a reportar de forma conjunta.

Otro cambio es la forma hacer aserciones sobre excepciones. Como podemos ver en la línea 23, ahora en vez de usar @Expected usaremos el método expectThrows().

La ventaja de este método es que es más flexible ya que tenemos el control total de la excepción dentro del método de test, por lo que podemos hacer más comprobaciones que antes (por ejemplo antes con el @Expected no podíamos validar el mensaje de la excepción).

Además este estilo de capturar las excepciones está más acorde con las buenas prácticas de testing donde lo que se recomienda es que la última línea de un tests siempre sea un Assert (esto no ocurría al usar la anotación @Expected).

¡Ojo, ya no hay soporte directo para Hamcrest!

Efectivamente si inspeccionamos el API ya no encontraremos rastro del método assertThat. La principal motivación de quitar esto ha sido para eliminar la dependencia que JUnit tenía con esta librería, que en algunas ocasiones podía incluso provocar algún conflicto en la resolución de clases (en particular cuando teníamos otras librerías que incluían una versión diferente de Hamcrest).

Si ahora queremos seguir usando Hamcrest tendremos que usar el método MatcherAssert.assertThat() que proporciona el propio Hamcrest.

7. Asunciones

Las asunciones nos permiten decidir si un test se debe de ejecutar o no en función de alguna condición, por ejemplo si estamos en el entorno de desarrollo o de integración, o si estamos en una máquina Windows o Unix. Esto ya lo teníamos en JUnit 4, pero ahora podemos usar lambdas de Java 8 para las expresiones.

class AssumptionsDemo {

    @Test
    void testOnlyOnCiServer() {
        assumeTrue("CI".equals(System.getenv("ENV")));

        // Resto del tets, sólo se ejecutará si estamos en el entorno de "CI"
    }

    @Test
    void testOnlyOnDeveloperWorkstation() {
        assumeTrue("DEV".equals(System.getenv("ENV")),
            () -> "Abortando test: no estamos en una máquina de desarrollo");

        // Vemos como podemos usar una lambda para indicar el mensaje.
    }

    @Test
    void testInAllEnvironments() {
        assumingThat("CI".equals(System.getenv("ENV")),
            () -> {
                // Esta aserción sólo se ejecuta en el entorno de "CI"
                assertEquals(2, 2);
            });

        // Esta aserción se ejecuta en todos los entornos
        assertEquals("a string", "a string");
    }
}

8. Etiquetas

@Tag("fast")
@Tag("model")
class TaggingDemo {

    @Test
    @Tag("taxes")
    void testingTaxCalculation() {
    }
}

En JUnit 4 el equivalente a las etiquetas son las Categories. Esto nos va a permitir lanzar conjuntos específicos de tests en función de las etiquetas que especifiquemos.

9. Clases anidadas

@DisplayName("A stack")
class TestingAStackDemo {

    Stack<Object> stack;

    @Test
    void is_instantiated_with_new() {
        new Stack<>();
    }

    @Nested
    class WhenNew {

        @BeforeEach
        void create_new_stack() {
            stack = new Stack<>();
        }

        @Test
        void is_empty() {
            assertTrue(stack.isEmpty());
        }

        @Test
        void throws_exception_when_popped() {
            assertThrows(EmptyStackException.class, () -> stack.pop());
        }

        @Test
        void throws_exception_when_peeked() {
            assertThrows(EmptyStackException.class, () -> stack.peek());
        }

        @Nested
        class AfterPushingAnElement {

            String anElement = "an element";

            @BeforeEach
            void pushAnElement() {
                stack.push(anElement);
            }

            @Test
            void is_is_no_longer_empty() {
                assertFalse(stack.isEmpty());
            }

            @Test
            void return_element_when_popped_and_is_empty() {
                assertEquals(anElement, stack.pop());
                assertTrue(stack.isEmpty());
            }

            @Test
            void return_element_when_peeked_but_remains_not_empty() {
                assertEquals(anElement, stack.peek());
                assertFalse(stack.isEmpty());
            }
        }
    }
}

Las clases de test anidadas nos permiten crear una estructura jerárquica para la ejecución de nuestros tests, de forma que podemos compartir fixtures y precondiciones (en JUnit 4 podíamos hacer algo parecido con runner Enclosed.class). El ejemplo anterior se leería de la siguiente manera:

TestingAStackDemo
    is_instantiated_with_new

    WhenNew
        is_empty
        throws_exception_when_popped
        throws_exception_when_peeked

        AfterPushingAnElement
            is_is_no_longer_empty
            return_element_when_popped_and_is_empty
            return_element_when_peeked_but_remains_not_empty

Nótese como podemos no tenemos límite en el nivel de anidación de las clases. También cabe destacar que las clases anidadas no pueden ser estáticas.

10. Conclusiones

Hemos hecho un repaso por las principales novedades que trae JUnit 5, y si bien los cambios no son radicales, sí que encontramos una serie de cosas que nos pueden facilitar una poco nuestro trabajo diario.

Os animo a que, después de esta introducción a las principales novedades, le echéis un vistazo a:

  • Escribir tests para interfaces usando los métodos por defecto que se pueden implementar en las interfaces de Java 8
  • Generación de tests dinámicamente con @TestFactory
  • @ExtensWith para indicar puntos de extensión del propio framework (por ejemplo para enganchar Mockito)

Después de esto sólo nos queda ir practicado y esperar a que publiquen la versión final para poder incorporarla en nuestros proyectos. Por ahora los planes son que esté lista para finales del 2016, así que, si toda va bien, la publicación de la GA (General Availability) debería ser inminente.

11. Sobre el autor

Alejandro Pérez García (@alejandropgarci) Ingeniero en Informática (especialidad de Ingeniería del Software) y Certified ScrumMaster

Socio fundador de Autentia Real Business Solutions S.L. – “Soporte a Desarrollo”

Socio fundador de ThE Audience Megaphone System, S.L. – TEAMS – “Todo el potencial de tus grupos de influencia a tu alcance”

Prueba del sharding en MongoDB

$
0
0
En este tutorial, explicaremos los fundamentos del particionado de datos en MongoDB y configuraremos, a través de un sencillo script, una pequeña prueba de Sharding en nuestra propia máquina.

Índice de contenidos

1. Introducción y Objetivo

Junto con la réplica de datos, de la que ya hablamos en Réplica de datos en MongoDB, el particionado de datos, también conocido como Sharding, es otra de las funcionalidades que nos brinda MongoDB para gestionar la escalabilidad del almacenamiento de nuestros datos. Con el Sharding conseguiremos distribuir la carga de datos a almacenar entre varios nodos de MongoDB, de forma que no todos los datos se almacenan en todos los nodos. Sin embargo, es importante conocer cómo funciona en detalle el sistema de particionado de datos y cómo las consultas se gestionan entre los distintos nodos, porque ello puede afectar en gran medida al rendimiento que obtengamos de nuestro sistema. En este artículo explicaremos cómo configurar la réplica de datos (a través de ShardingTest) en MongoDB. A lo largo del tutorial iremos viendo los pasos para montar un servicio de MongoDB configurado con el sharding activo usando tres instancias, en una misma máquina, entre las que particionaremos los datos. Para ello se utilizará el propio API de MongoDB y la facilidad que nos proporciona para la configuración por defecto de la funcionalidad de sharding a través del objeto ShardingTest. Los pasos que se presentan en el presente artículo NO constituyen la forma recomendada de configurar el particionado de datos, sino que pretenden mostrar cómo probar la funcionalidad de particionado de MongoDB de una forma sencilla. Por este motivo, este documento no debe utilizarse como guía para la configuración de entornos de producción. Para configurar correctamente el particionado de datos en MongoDB, se debe configurar los mismos elementos que se levantan en este ejemplo de forma automática a través de ShardingTest, pero de forma específica y manual.

2. Requisitos

Para poder seguir las instrucciones de esta guía y probar el funcionamiento del Sharding en MongoDB, vamos a utilizar un servidor de MongoDB, que levantará varios procesos mongod en una misma máquina y sobre el que probaremos a insertar datos y comprobar cómo estos se van repartiendo a través de los distintos nodos que forman parte del grupo de Sharding. Para realizar el proceso, utilizaremos la consola de Mongo y el API JavaScript que ofrece esta consola. Por tanto, el único requisito es tener una versión de MongoDB instalada. Se requiere que sea al menos la versión 2.2, dado que es la primera versión donde el API incluye el objeto ShardingTest, con la estructura de la que nos valdremos para configurar las pruebas de particionado de datos.

3. Arranque y acceso a la consola con Mongo Shell

Para iniciar la prueba de Sharding necesitamos arrancar una primera consola de Mongo, desde la que iremos levantando los nodos que configuraremos para el grupo de particionado. Por este motivo, necesitamos arrancar la consola de mongo sin conectar contra ningún servidor en concreto. Esto lo conseguimos con el parámetro --nodb
$ mongo --nodb
MongoDB shell version: 3.2.0
>
Es preferible que no tengamos una instancia de MongoDB previamente arrancada, las distintas instancias que formarán parte del grupo de particionado de prueba se irán levantando durante el proceso del script.

4. Creación del grupo de particionado para pruebas

Para probar el mecanismo de réplica de MongoDB, necesitaremos crear varias instancias de mongod que actúen como servidores y relacionarlas de forma que sepan repartirse los datos entre ellas. El Sharding permite dividir la carga de almacenamiento de datos entre varios nodos, de forma que no todos los datos se almacenan en todos los nodos. Es decir, cada nodo, guarda un subconjunto total de los datos, de forma que el escalado horizontal de la BD es más sencillo de realizar. MongoDB, con el Sharding, tratará de que, en la medida de lo posible, los datos se repartan de la forma más homogénea posible entre todos los nodos. El objetivo es conseguir que todos los nodos tengan más o menos el mismo volumen de datos, sin que se descompense ninguno de ellos. Aunque para conseguirlo, es necesario que tengamos en cuenta cómo funciona este particionado y lo configuremos en base a atributos que permitan hacer más homogénea dicha división de datos. Para el grupo de Sharding, MongoDB necesita tener almacenada, en otra BD, la configuración del grupo de particionado, para saber cómo se han repartido los datos (lo que permitirá optimizar las consultas), qué nodos forman parte del grupo de particionado (lo que permitirá optimizar las inserciones) y cuál es la política de particionado, entre otra información. Esta información sobre la “geometría de cómo se particionan los datos”, se almacena habitualmente en una instancia específica de MongoDB, que sirve para guardar esta configuración. Esta BD de configuración del particionado de datos es crítica para el funcionamiento y rendimiento de todo el sistema de sharding, dado que si esta base de datos se para, se pierde o se corrompe, el grupo de particionado no podrá saber dónde tiene que insertar, cómo tiene que dividir o dónde tiene que redirigir las operaciones de consulta. Por este motivo, habitualmente la base de datos de configuración del sharding, se configura en modo de réplica (como ya describimos en el tutorial sobre Prueba de RéplicaSet en MongoDB). En este proceso de levantar el Sharding, el objeto ShardingTest creará y levantará tantas instancias de mongod como nodos indiquemos que queremos utilizar para configurar nuestras pruebas del particionado de datos. Además, el objeto ShardingTest también creará las instancias de mongod necesarias para guardar la configuración en un ReplicaSet. Para levantar el clúster de réplica, desde la consola mongo que tenemos abierta, escribiremos el siguiente comando:
> cluster = new ShardingTest({shards : 3, chunksize : 1})
Con este comando indicamos que se cree un objeto ShardingTest que contiene el conjunto de procesos mongod que formarán parte del particionado de datos, así como las instancias mongod para la base de datos de configuración en modo réplica. Como parámetro a construir el ShardingTest podemos personalizar la configuración de las instancias que formarán parte del particionado de datos. Las opciones de configuración completas se pueden consultar en la documentación de ShardingTest. A diferencia de lo que ocurre con la creación de un grupo de réplica de prueba, a través de ReplSetTest, en el caso de un ShardingTest, al crearlo, directamente se instancian ya todos las instancias del demonio de mongo necesarias. Vemos esta información en algunos de los fragmentos que obtendremos en las trazas (en la consola salen muchas más trazas, pero mostramos sólo las partes más descriptivas):
> cluster = new ShardingTest({shards : 3, chunksize : 1})
	Resetting db path '/data/db/test0'
	2016-02-26T11:08:31.616+0100 I -        [thread1] shell: started program (sh8317):  mongod --dbpath /data/db/test0 --port 20000 --setParameter enableTestCommands=1
	d20000| 2016-02-26T11:08:31.698+0100 I CONTROL  [initandlisten] MongoDB starting : pid=8317 port=20000 dbpath=/data/db/test0 64-bit host=Irensaga-2.local
	d20000| 2016-02-26T11:08:31.699+0100 I CONTROL  [initandlisten] db version v3.2.0
	d20000|
	[...]
	ReplSetTest Starting Set
	ReplSetTest n is : 0
	{
		"useHostName" : true,
		"oplogSize" : 40,
		"keyFile" : undefined,
		"port" : 20003,
		"noprealloc" : "",
		"smallfiles" : "",
		"replSet" : "test-configRS",
		"dbpath" : "$set-$node",
		"pathOpts" : {
			"testName" : "test",
			"node" : 0,
			"set" : "test-configRS"
		},
		"journal" : "",
		"configsvr" : "",
		"noJournalPrealloc" : undefined,
		"storageEngine" : "wiredTiger",
		"restart" : undefined
	}
	ReplSetTest Starting....
	Resetting db path '/data/db/test-configRS-0'
	2016-02-26T11:08:35.250+0100 I -        [thread1] shell: started program (sh8320):  mongod --oplogSize 40 --port 20003 --noprealloc --smallfiles --replSet test-configRS --dbpath /data/db/test-configRS-0 --journal --configsvr --storageEngine wiredTiger --setParameter enableTestCommands=1
	2016-02-26T11:08:35.251+0100 W NETWORK  [thread1] Failed to connect to 127.0.0.1:20003, reason: errno:61 Connection refused
	[...]
Lo primero es que vemos que se levantan la base de datos de configuración y configurada en modo réplica. Al finalizar el arranque de esta base de datos de configuración, podemos ver la estructura de instancias de mongod para sharding que se almacena en la propia base de datos de configuración:
ShardingTest test :
	{
		"config" : "test-configRS/Irensaga-2.local:20003,Irensaga-2.local:20004,Irensaga-2.local:20005",
		"shards" : [
			connection to Irensaga-2.local:20000,
			connection to Irensaga-2.local:20001,
			connection to Irensaga-2.local:20002
		]
	}
En ella podemos ver los elementos que hemos descrito antes:
  • Base de datos config: en modo replicaSet con nombre test-configRS y con tres nodos en el grupo de réplica para el almacenamiento de dicha configuración:
    • mongod en el puerto 20003,
    • mongod en el puesto 20004
    • mongod en el puerto 20005
  • Tres instancias, denominadas “shards”, para los nodos entre los que se realizará el particionado de datos:
    • mongod en el puerto 20000
    • mongod en el puerto 20001
    • mongod en el puerto 20002
También podemos ver en otro fragmento de las trazas, cómo se arrancan los procesos mongod que forman los shards. Por último, se levantará un proceso mongos, que actúa como balanceador y que es el encargado de recibir las peticiones de consulta y las operaciones de escritura/modificación y, tras consultar en la BD de configuración los shards y la división de datos que se ha realizado, es capaz de redirigir la consulta al proceso (o al conjunto de procesos) mongod adecuados:
s20006| 2016-02-26T11:08:55.671+0100 I SHARDING [mongosMain] MongoS version 3.2.0 starting: pid=8323 port=20006 64-bit host=Irensaga-2.local (--help for usage)
	s200
Al final de este proceso, podemos ver la comprobación final, que realiza el balanceador (el proceso mongos) de carga entre los nodos y que se irá repitiendo cada cierto tiempo para comprobar:
  • El número de shards que están definidos en la base de datos de configuración,
  • El tamaño del bloque (chunksize) donde se agrupan los datos particionados en cada shard
  • La necesidad o no de hacer un balanceo de datos (en el caso de que un nodo del shard tenga muchos más datos que otro).
  • Por último, comprueba que el grupo de réplica para la base de datos de configuración está vivo.
s20006| 2016-02-26T11:09:13.055+0100 D SHARDING [Balancer] found 3 shards listed on config server(s)
	s20006| 2016-02-26T11:09:13.055+0100 D SHARDING [Balancer] Refreshing MaxChunkSize: 1MB
	s20006| 2016-02-26T11:09:13.056+0100 D SHARDING [Balancer] skipping balancing round because balancing is disabled
	s20006| 2016-02-26T11:09:15.679+0100 D NETWORK  [ReplicaSetMonitorWatcher] checking replica set: test-configRS
Este chequeo se irá repitiendo cada cierto tiempo, y se volcará en la consola de mongo que hemos abierto para arrancar el objeto ShardingTest. Podemos comprobar las instancias de mongod que se han levantado.
$ ps -ef | grep mongo
	  501  8922  8337   0  4:35PM ttys000    0:30.02 mongod --dbpath /data/db/test0 --port 20000 --setParameter enableTestCommands=1
	  501  8923  8337   0  4:35PM ttys000    0:29.97 mongod --dbpath /data/db/test1 --port 20001 --setParameter enableTestCommands=1
	  501  8924  8337   0  4:35PM ttys000    0:29.78 mongod --dbpath /data/db/test2 --port 20002 --setParameter enableTestCommands=1
	  501  8925  8337   0  4:35PM ttys000    0:40.86 mongod --oplogSize 40 --port 20003 --noprealloc --smallfiles --replSet test-configRS --dbpath /data/db/test-configRS-0 --journal --configsvr --storageEngine wiredTiger --setParameter enableTestCommands=1
	  501  8926  8337   0  4:35PM ttys000    0:39.35 mongod --oplogSize 40 --port 20004 --noprealloc --smallfiles --replSet test-configRS --dbpath /data/db/test-configRS-1 --journal --configsvr --storageEngine wiredTiger --setParameter enableTestCommands=1
	  501  8927  8337   0  4:35PM ttys000    0:39.02 mongod --oplogSize 40 --port 20005 --noprealloc --smallfiles --replSet test-configRS --dbpath /data/db/test-configRS-2 --journal --configsvr --storageEngine wiredTiger --setParameter enableTestCommands=1
	  501  8928  8337   0  4:35PM ttys000    0:11.56 mongos --configdb test-configRS/Irensaga-2.local:20003,Irensaga-2.local:20004,Irensaga-2.local:20005 -v --chunkSize 1 --port 20006 --setParameter enableTestCommands=1
Vemos en esta salida los procesos mongod arrancados para representar los nodos que forman parte del Sharding (los tres primeros), los nodos que actuarán en modo de réplica para almacenar la base de datos de configuración (del 4 al 6) y el proceso mongos (el último) que actúa como balanceador.

5. Prueba del particionado de datos y distribución entre los nodos del Shard

Una vez levantado el Sharding en Mongo DB, realizaremos las pruebas de inserción de datos para comprobar cómo se realiza el particionado.

5.1. Inserción de datos sobre el balanceador

Para actuar sobre el conjunto de Sharding, arrancaremos una nueva consola cliente de mongo contra el balanceador:
$ mongo --port 20006
	MongoDB shell version: 3.2.0
	connecting to: 127.0.0.1:20006/test
	mongos>
Conectamos al balanceador, que actuará a todos los efectos como nuestro servidor MongoDB que representa el conjunto de Shards completo. Ejecutamos una inserción de documentos, en este caso vamos a simular una inserción de entradas en un blog (por ejemplo 100.000 entradas parece un buen número para probar)
mongos> for (i = 0; i < 100000; i++) {
	... db.blog_posts.insert({
	...     author : "author" + i,
	...     post_title : "Blog Post by Author " + i,
	...     date : new Date()
	...     });
	... }
Esta operación tardará un tiempo; pero al cabo de unos segundos obtendremos la confirmación de que se ha realizado la inserción
WriteResult({ "nInserted" : 1 })
	mongos>
Podemos comprobar el número de datos insertados, de forma global entre todos los nodos del Shard, consultando directamente a través de la conexión al balanceador:
mongos> db.blog_posts.count();
	100000
Como salida del comando obtenemos el número total de documentos almacenados en la colección blog_posts entre todos los nodos del Shard.

5.2. Comprobación (fallida) de la distribución de datos entre los nodos

Desde el balanceador, siempre tenemos la visión del conjunto de datos completo, para ver cómo se ha producido de forma efectiva la distribución de datos entre los distintos nodos, podemos conectarnos a los distintos procesos que forman parte del Shard. Para ello, abriremos una nueva consola de Mongo desde la que nos conectaremos a cada uno de los nodos con objetos distintos.:
$ mongo --nodb
	MongoDB shell version: 3.2.0
	>
Desde esta consola, comenzamos por conectarnos al primero de los nodos del shard, el que se ha levantado escuchando en el puerto 20.000, obtenemos el acceso a la BD de test que se mantiene en este primer nodo, y consultamos el número de registros guardados en ella.
> shard1 = new Mongo("localhost:20000")
	connection to localhost:20000
	> shard1DB = shard1.getDB("test")
	test
	> shard1DB.blog_posts.count()
	100000
	>
Repetimos la misma secuencia para comprobar los registros que se han almacenado en la base de datos del segundo nodo del Shard:
> shard2 = new Mongo("localhost:20001")
	connection to localhost:20001
	> shard2DB = shard2.getDB("test")
	test
	> shard2DB.blog_posts.count()
	0
Por último, comprobamos sobre el tercer nodo del Shard.
> shard3 = new Mongo("localhost:20002")
	connection to localhost:20002
	> shard3DB = shard3.getDB("test")
	test
	> shard3DB.blog_posts.count()
	0
Según esta comprobación, todos los documentos que insertamos se han almacenado en el primer nodo del shard, y los nodos 2 y 3 no contienen ningún documento en la colección blog_posts. Según estas pruebas, podríamos concluir que el particionado de datos no funciona porque, de hecho, no se ha producido ningún tipo de reparto de datos entre los distintos nodos y el desequilibrio es obvio. Antes de concluir que el Sharding en MongoDB no funciona, debemos saber que, por defecto, al crear el objeto para las pruebas de Sharding, no se activa el particionado y éste es el motivo de que no se reparta la carga de datos entre todos los nodos. En el siguiente apartado veremos cómo activar el particionado en el Shard, comprobaremos cómo el balanceador detecta el desequilibrio y cómo entra en acción para corregir dicho desequilibrio moviendo datos entre los nodos y repartiendo la carga.

5.3. Activación del Sharding

Para activar la funcionalidad de particionado de datos, debemos actuar sobre el balanceador. Por tanto, volveremos a la consola de mongo que hemos arrancado contra la instancia de mongos (que corría en el puerto 20006). En dicha consola, podremos comprobar, a través de la función status(), el estado del grupo de Sharding:
mongos> sh.status()
	--- Sharding Status ---
	  sharding version: {
		"_id" : 1,
		"minCompatibleVersion" : 5,
		"currentVersion" : 6,
		"clusterId" : ObjectId("56d070bc76ad433b3068863d")
	}
	  shards:
		{  "_id" : "shard0000",  "host" : "Irensaga-2.local:20000" }
		{  "_id" : "shard0001",  "host" : "Irensaga-2.local:20001" }
		{  "_id" : "shard0002",  "host" : "Irensaga-2.local:20002" }
	  active mongoses:
		"3.2.0" : 1
	  balancer:
		Currently enabled:  no
		Currently running:  no
		Failed balancer rounds in last 5 attempts:  0
		Migration Results for the last 24 hours:
			No recent migrations
	  databases:
		{  "_id" : "test",  "primary" : "shard0000",  "partitioned" : false }

	mongos>
En este caso vemos, efectivamente, que el balanceador (el proceso que reparte la carga de datos entre los distintos nodos) no está activo (propiedad balancer: Currently enabled : no y balancer: Currently running: no) y, de hecho el particionado de datos para el shard0000 (el nombre que se le ha dado automáticamente a nuestro shard al crear el ShardingTest) no está particionado (partitioned : false) Para activar el sharding utilizaremos la función enableSharding() sobre la base de datos que queremos que reparta sus datos entre todos los nodos:
mongos> sh.enableSharding("test")
	{ "ok" : 1 }
	mongos>
Pero no es suficiente sólo con activar el particionado de datos. El proceso de reparto, se basa en un atributo (o conjunto de atributos) en función del cual MongoDB creará grupos de datos (chunks) que irá moviendo entre los distintos nodos. Este concepto de chunks o grupos de datos en el shard se verá mucho mejor cuando comprobemos cómo se ha realizado la creación de dichos chunks y su distribución entre los nodos. Para indicar este atributo, debemos crear un índice sobre la colección que vamos a particionar. Esto lo realizamos con la función ensureIndex() indicando como argumento el atributo de los objetos de la colección por el que queremos realizar los grupos de datos antes de repartirlos. Con la creación de este índice lo que hacemos es marcar la restricción de que todos los documentos de la colección contienen ese atributo.
mongos> db.blog_posts.ensureIndex({author : 1})
	{
		"raw" : {
			"Irensaga-2.local:20000" : {
				"createdCollectionAutomatically" : false,
				"numIndexesBefore" : 1,
				"numIndexesAfter" : 2,
				"ok" : 1
			}
		},
		"ok" : 1
	}
	mongos>
Una vez creado el índice, y como último paso para que MongoDB comience a distribuir los documentos de la colección entre los nodos de Shard es, precisamente, realizar el particionado de la colección mencionada. Esto lo conseguimos con la función shardCollection(), donde especificaremos los siguientes atributos:
    • base de datos y colección a particionar
    • atributo (especificado como objeto JSON, el mismo que utilizamos para definir el índice) por el que se agruparán los objetos en chunks y se distribuirán esos chunks entre los nodos.
mongos> sh.shardCollection("test.blog_posts", {author : 1})
	{ "collectionsharded" : "test.blog_posts", "ok" : 1 }
	mongos>
Si volvemos a comprobar el estado del grupo de particionado, otra vez con la función status(), esta vez comprobaremos que:
mongos> sh.status()
	--- Sharding Status ---
	  sharding version: {
		"_id" : 1,
		"minCompatibleVersion" : 5,
		"currentVersion" : 6,
		"clusterId" : ObjectId("56d070bc76ad433b3068863d")
	}
	  shards:
		{  "_id" : "shard0000",  "host" : "Irensaga-2.local:20000" }
		{  "_id" : "shard0001",  "host" : "Irensaga-2.local:20001" }
		{  "_id" : "shard0002",  "host" : "Irensaga-2.local:20002" }
	  active mongoses:
		"3.2.0" : 1
	  balancer:
		Currently enabled:  no
		Currently running:  no
		Failed balancer rounds in last 5 attempts:  0
		Migration Results for the last 24 hours:
			No recent migrations
	  databases:
		{  "_id" : "test",  "primary" : "shard0000",  "partitioned" : true }
			test.blog_posts
				shard key: { "author" : 1 }
				unique: false
				balancing: true
				chunks:
					shard0000	20
				too many chunks to print, use verbose if you want to force print
En este caso el shard0000 está en modo particionado (partitioned : true), sin embargo el balancer (que es quien de forma efectiva moverá los datos) no se ha llegado a ejecutar. De hecho, podemos comprobar su estado de ejecución con getBalancerState()
> sh.getBalancerState()
	false
	mongos>
Ahora sólo nos queda conseguir que el balancer comience a ejecutarse, para ello, utilizamos la función setBalancerState(boolean).
mongos> sh.setBalancerState(true)
	mongos> sh.getBalancerState()
	true
	mongos> sh.isBalancerRunning()
	true
	mongos>
En este momento, si nos movemos a la consola de mongo donde creamos el objeto ShardingTest, veremos que las trazas nos indican que el balanceador ha detectado el desequilibrio y está compensándolo:
[NetworkInterfaceASIO-ShardRegistry-0] Successfully connected to Irensaga-2.local:20002
	s20006| 2016-02-26T18:32:40.178+0100 D SHARDING [Balancer] trying to acquire new distributed lock for balancer ( lock timeout : 900000 ms, ping interval : 30000 ms, process : Irensaga-2.local:20006:1456500921:-1570693962 ) with lockSessionID: 56d08c3876ad433b3068864a, why: doing balance round
	s20006| 2016-02-26T18:32:40.205+0100 I SHARDING [Balancer] distributed lock 'balancer' acquired for 'doing balance round', ts : 56d08c3876ad433b3068864a
	s20006| 2016-02-26T18:32:40.205+0100 D SHARDING [Balancer] *** start balancing round. waitForDelete: 0, secondaryThrottle: default
	s20006| 2016-02-26T18:32:40.211+0100 D SHARDING [Balancer] shard0002 has more chunks me:0 best: shard0001:0
	s20006| 2016-02-26T18:32:40.211+0100 D SHARDING [Balancer] collection : test.blog_posts
	s20006| 2016-02-26T18:32:40.211+0100 D SHARDING [Balancer] donor      : shard0000 chunks on 20
	s20006| 2016-02-26T18:32:40.211+0100 D SHARDING [Balancer] receiver   : shard0001 chunks on 0
	s20006| 2016-02-26T18:32:40.211+0100 D SHARDING [Balancer] threshold  : 4
	s20006| 2016-02-26T18:32:40.211+0100 I SHARDING [Balancer]  ns: test.blog_posts going to move { _id: "test.blog_posts-author_MinKey", ns: "test.blog_posts", min: { author: MinKey }, max: { author: "author14669" }, shard: "shard0000", lastmod: Timestamp 1000|0, lastmodEpoch: ObjectId('56d08a5276ad433b30688648') } from: shard0000 to: shard0001 tag []
	s20006| 2016-02-26T18:32:40.212+0100 I SHARDING [Balancer] moving chunk ns: test.blog_posts moving ( ns: test.blog_posts, shard: shard0000, lastmod: 1|0||56d08a5276ad433b30688648, min: { author: MinKey }, max: { author: "author14669" }) shard0000 -> shard0001
	c20005| 2016-02-26T18:32:40.218+0100 I NETWORK  [initandlisten] connection accepted from 192.168.168.151:55261 #14 (11 connections now open)
Estas trazas las veremos repetirse, según va moviendo más bloques entre los nodos. Podemos ir comprobando cuando acaba de ejecutar su tarea el balancer, invocando al método isBalancerRunning() hasta que nos diga que ha acabado.
mongos> sh.isBalancerRunning()
	true
	mongos>
	[...]
	mongos> sh.isBalancerRunning()
	false
	mongos>

5.4. Comprobación de la réplica de datos (correcto)

Una vez que ha acabado el balancer de equilibrar la carga de datos entre todos los nodos, podemos volver a repetir la consulta del apartado 5.2 y comprobaremos que esta vez el número de datos está más equilibrado. Para ello, volvemos a la tercera consola de MongoDB, donde teníamos las tres conexiones a los tres nodos de mongod:
> shard1DB.blog_posts.count()
	32518
	> shard2DB.blog_posts.count()
	36336
	> shard3DB.blog_posts.count()
	31146
Ahora sí, efectivamente, podemos ver que los datos están distribuidos entre los tres nodos teniendo cada uno de ellos, aproximadamente un tercio del total y que, entre los tres, suman el grupo completo de los mismos (32.518 + 36.336 + 31.146 = 100.000).

5.5. Consulta de la distribución de chunks entre nodos

Utilizando la consola abierta contra el balanceador, podemos comprobar la agrupación de datos en chunks (en base al atributo que se utiliza como clave en el particionado) la ubicación de cada grupo en los nodos. Para obtener esta información, ejecutaremos la función status():
mongos> sh.status(true)
	--- Sharding Status ---
	  sharding version: {
		"_id" : 1,
		"minCompatibleVersion" : 5,
		"currentVersion" : 6,
		"clusterId" : ObjectId("56d070bc76ad433b3068863d")
	}
	  shards:
		{  "_id" : "shard0000",  "host" : "Irensaga-2.local:20000" }
		{  "_id" : "shard0001",  "host" : "Irensaga-2.local:20001" }
		{  "_id" : "shard0002",  "host" : "Irensaga-2.local:20002" }
	  active mongoses:
		{  "_id" : "Irensaga-2.local:20006",  "ping" : ISODate("2016-02-26T19:20:45.956Z"),  "up" : NumberLong(13520),  "waiting" : true,  "mongoVersion" : "3.2.0" }
	  balancer:
		Currently enabled:  yes
		Currently running:  no
		Failed balancer rounds in last 5 attempts:  0
		Migration Results for the last 24 hours:
			13 : Success
	  databases:
		{  "_id" : "test",  "primary" : "shard0000",  "partitioned" : true }
			test.blog_posts
				shard key: { "author" : 1 }
				unique: false
				balancing: true
				chunks:
					shard0000	7
					shard0001	7
					shard0002	6
				{ "author" : { "$minKey" : 1 } } -->> { "author" : "author14669" } on : shard0001 Timestamp(2, 0)
				{ "author" : "author14669" } -->> { "author" : "author19340" } on : shard0002 Timestamp(3, 0)
				{ "author" : "author19340" } -->> { "author" : "author24011" } on : shard0001 Timestamp(4, 0)
				{ "author" : "author24011" } -->> { "author" : "author28685" } on : shard0002 Timestamp(5, 0)
				{ "author" : "author28685" } -->> { "author" : "author33356" } on : shard0001 Timestamp(6, 0)
				{ "author" : "author33356" } -->> { "author" : "author38028" } on : shard0002 Timestamp(7, 0)
				{ "author" : "author38028" } -->> { "author" : "author4270" } on : shard0001 Timestamp(8, 0)
				{ "author" : "author4270" } -->> { "author" : "author47372" } on : shard0002 Timestamp(9, 0)
				{ "author" : "author47372" } -->> { "author" : "author52043" } on : shard0001 Timestamp(10, 0)
				{ "author" : "author52043" } -->> { "author" : "author56716" } on : shard0002 Timestamp(11, 0)
				{ "author" : "author56716" } -->> { "author" : "author61388" } on : shard0001 Timestamp(12, 0)
				{ "author" : "author61388" } -->> { "author" : "author6606" } on : shard0002 Timestamp(13, 0)
				{ "author" : "author6606" } -->> { "author" : "author70731" } on : shard0001 Timestamp(14, 0)
				{ "author" : "author70731" } -->> { "author" : "author75403" } on : shard0000 Timestamp(14, 1)
				{ "author" : "author75403" } -->> { "author" : "author80075" } on : shard0000 Timestamp(1, 14)
				{ "author" : "author80075" } -->> { "author" : "author84748" } on : shard0000 Timestamp(1, 15)
				{ "author" : "author84748" } -->> { "author" : "author8942" } on : shard0000 Timestamp(1, 16)
				{ "author" : "author8942" } -->> { "author" : "author94091" } on : shard0000 Timestamp(1, 17)
				{ "author" : "author94091" } -->> { "author" : "author98764" } on : shard0000 Timestamp(1, 18)
				{ "author" : "author98764" } -->> { "author" : { "$maxKey" : 1 } } on : shard0000 Timestamp(1, 19)
	mongos>
En esta traza de consola podemos ver que esta vez el sharding activo y los números de grupos que contiene cada servidor (bajo la propiedad databases:chunks). Si especificamos el nivel de detalle (pasando true como argumento a la función status()), obtendremos además la ubicación de cada chunk de datos(la lista de datos final bajo databases:chunks). Por ejemplo, todos los documentos cuyo atributo author va desde el primero por orden natural (representado por "author" : { "$minKey" : 1 }) hasta el documento con valor del atributo author author14669, están ubicados en el nodo shard0001. Todos los documentos con clave del author desde el author14669 al author19340 están ubicados en el nodo shard0002. Y así sucesivamente hasta los documentos con valor para el atributo author entre el author98764 y el valor máximo (representado por maxKey), que estarán ubicados en el nodo shard0000 del Shard.

6. Comprobación del mecanismo de distribución de consultas.

Toda la información sobre la agrupación de datos en chunks y la ubicación de estos sobre los distintos nodos del shard se almacenan en la base de datos de configuración (test-ConfigRS) que está, además, configurada en modo réplica dada su importancia. Si se pierde o se corrompe esta información, mongoDB no podría, a priori, saber el estado del shard ni dónde tiene ubicados los distintos datos.

6.1. Consulta de un documento ubicado en un nodo concreto.

Internamente, MongoDB utiliza esta información de configuración para optimizar, en la medida de lo posible, las consultas sobre los distintos nodos del shard. Así, una consulta o modificación sobre un documento buscandolo por el propio atributo que se ha definido para la división de datos (en nuestro caso por el atributo author) el balanceador puede saber a qué nodos dirigirla exclusivamente. Podemos ver el plan de ejecución de la consulta de MongoDB, utilizando la función explain() sobre la propia consulta a realizar:
mongos> db.blog_posts.find({author : "author 999"}).explain()
	{
		"queryPlanner" : {
			"mongosPlannerVersion" : 1,
			"winningPlan" : {
				"stage" : "SINGLE_SHARD",
				"shards" : [
					{
						"shardName" : "shard0000",
						"connectionString" : "Irensaga-2.local:20000",
						"serverInfo" : {
							"host" : "Irensaga-2.local",
							"port" : 20000,
							"version" : "3.2.0",
							"gitVersion" : "45d947729a0315accb6d4f15a6b06be6d9c19fe7"
						},
						"plannerVersion" : 1,
						"namespace" : "test.blog_posts",
						"indexFilterSet" : false,
						"parsedQuery" : {
							"author" : {
								"$eq" : "author 999"
							}
						},
						"winningPlan" : {
							"stage" : "FETCH",
							"inputStage" : {
								"stage" : "SHARDING_FILTER",
								"inputStage" : {
									"stage" : "IXSCAN",
									"keyPattern" : {
										"author" : 1
									},
									"indexName" : "author_1",
									"isMultiKey" : false,
									"isUnique" : false,
									"isSparse" : false,
									"isPartial" : false,
									"indexVersion" : 1,
									"direction" : "forward",
									"indexBounds" : {
										"author" : [
											"[\"author 999\", \"author 999\"]"
										]
									}
								}
							}
						},
						"rejectedPlans" : [ ]
					}
				]
			}
		},
		"ok" : 1
	}
	mongos>
En este caso, vemos que el proceso mongos, tras consultar la distribución de chunks en test-ConfigRS, sabe que la consulta afecta sólo a información contenida en el nodo shard0000.

7. Parada del cluster de particionado de datos

Por último, para parar nuestro ShardingTest, volveremos a la consola inicial, donde creamos el objeto que levantó todos los nodos y ejecutaremos la función stop() sobre el cluster que tenemos:
> cluster.stop()
Veremos como se comienza a enviar la secuencia de finalización de todos los procesos mongod y mongos y se van parando los distintos servicios.
>cluster.stop()
	s20006| 2016-02-26T20:22:26.997+0100 I CONTROL  [signalProcessingThread] got signal 15 (Terminated: 15), will terminate after current cmd ends
	s20006| 2016-02-26T20:22:26.997+0100 D SHARDING [signalProcessingThread] CatalogManagerReplicaSet::shutDown() called.
	s20006| 2016-02-26T20:22:27.010+0100 D NETWORK  [ReplicaSetMonitorWatcher] checking replica set: test-configRS
	s20006| 2016-02-26T20:22:27.028+0100 I SHARDING [signalProcessingThread] dbexit:  rc:0
	d20002| 2016-02-26T20:22:27.029+0100 I NETWORK  [conn5] end connection 192.168.168.151:55256 (5 connections now open)
	d20001| 2016-02-26T20:22:27.029+0100 I NETWORK  [conn5] end connection 192.168.168.151:55251 (6 connections now open)
	d20000| 2016-02-26T20:22:27.029+0100 I NETWORK  [conn8] end connection 192.168.168.151:55246 (7 connections now open)
	c20003| 2016-02-26T20:22:27.029+0100 I NETWORK  [conn57] end connection 192.168.168.151:62693 (26 connec2016-02-26T20:22:27.029+0100 I NETWORK  [conn58] end connection 192.168.168.151:62698 (26 connections now open)
	d20001| 2016-02-26T20:22:27.029+0100 I NETWORK  [conn9] end connection 192.168.168.151:55320 (5 connections now open)
	d20000| 2016-02-26T20:22:27.029+0100 I NETWORK  [conn6] end connection 192.168.168.151:54606 (6 connections now open)
	c20003| 2016-02-26T20:22:27.029+0100 I NETWORK  [conn56] end connection 192.168.168.151:62688 (26 connections now open)
	d20000| 2016-02-26T20:22:27.029+0100 I NETWORK  [conn11] end connection 192.168.168.151:62500 (6 connections now open)
	c20003| 2016-02-26T20:22:27.029+0100 I NETWORK  [conn55] end connection 192.168.168.151:62683 (26 connections now open)
	d20000| 2016-02-26T20:22:27.029+0100 I NETWORK  [conn4] end connection 192.168.168.151:51425 (5 connections now open)
	c20003| 2016-02-26T20:22:27.029+0100 I NETWORK  [conn54] end connection 192.168.168.151:62678 (26 connections now open)


	[...]

	20005| 2016-02-26T20:22:39.409+0100 I STORAGE  [signalProcessingThread] shutdown: removing fs lock...
	c20005| 2016-02-26T20:22:39.409+0100 I CONTROL  [signalProcessingThread] dbexit:  rc: 0
	2016-02-26T20:22:40.039+0100 I -        [thread1] shell: stopped mongo program on port 20005
	ReplSetTest stop *** Mongod in port 20005 shutdown with code (0) ***
	ReplSetTest stopSet deleting all dbpaths
	ReplSetTest stopSet *** Shut down repl set - test worked ****
	*** ShardingTest test completed successfully in 13657.467 seconds ***

Anexo A. Resolución de problemas

A.1. Todos los datos siguen en el mismo nodo, pese a haber activado el sharding sobre la colección y activar el balancer.

El balanceador sólo activa la división de un chunk de datos en al menos otros dos más pequeños susceptibles de ser repartidos entre los nodos del shard, cuando se ha alcanzado un tamaño máximo en dicho chunk. Este tamaño está expresado en el atributo chunksize que se pasa como argumento al construir el ShardingTest. El valor se expresa en Mb. Para las pruebas, conviene asegurarse de que se ha arrancado el ShardingTest con un valor de 1Mb como tamaño máximo. De otro modo, tendríamos que insertar muchísimos más valores para alcanzar el tamaño máximo del chunk y provocar su división y movimiento a otro nodo.
cluster = new ShardingTest({shards : 3, chunksize : 1})

Introducción a ToroDB

$
0
0
Introducción a ToroDB Stampede
torodb-logo

Índice de contenidos


1. Introducción a ToroDB Stampede

ToroDB Stampede es una tecnología que permite la replicación de documentos NoSQL de MongoDB en una estructura relacional de PostgreSQL, de forma totalmente transparente al usuario. Para lograrlo lo que hace es monitorizar un replica set de MongoDB y hacer uso del Oplog.

toro_stampede_structure

A diferencia de otras soluciones en el mercado, la estructura relacional resultante no es un documento de tipo JSON almacenado en PostgreSQL. Se trata de una descomposición en tablas del documento original, resultando en una estructura que es realmente relacional.


2. Requisitos previos

Para hacer uso de ToroDB Stampede hay una serie de requisitos previos que deben estar correctamente instalados y configurados:

  • Oracle Java 8

  • MongoDB 3.2

  • PostgreSQL

Los pasos descritos a continuación están basados en el uso de Ubuntu 16.04 aunque se puede hacer uso de otros sistemas operativos. Para poder completar el tutorial sin mayores problemas, se recomienda utilizar la siguiente receta de Vagrant, que permite tener un entorno inicial de forma automática.


3. Instalación

Antes de poder utilizar Stampede, es necesario configurar MongoDB y PostgreSQL de forma adecuada.

Configuración de MongoDB como replica set

Para configurar una instancia de MongoDB como un nodo primario del replica set, debemos modificar el fichero /etc/mongod.conf añadiendo las siguientes líneas.

replication:
  replSetName: "rs1"

En algunas ocasiones se producen problemas si el fichero /etc/hosts no contiene una entrada con el nombre de nuestra máquina. Si se está usando la receta Vagrant provista en el tutorial, hay que añadir la línea indicada a continuación.

127.0.0.1	ubuntu-xenial.localdomain	ubuntu-xenial

Hecho esto, reiniciamos el servicio.

$ sudo service mongod restart

Ahora podemos acceder haciendo uso de la consola y completar la configuración del replica set.

$ mongo

> rs.initiate()

Para comprobar que la configuración del replica set es correcta podemos ejecutar el comando rs.conf().


4. Configuración de PostgreSQL

ToroDB Stampede espera que exista el usuario torodb y la base de datos torod propiedad de dicho usuario, por lo que tenemos que realizar los pasos necesarios para crearlos.

$ sudo -u postgres createuser -S -R -D -P --interactive torodb

$ sudo -u postgres createdb -O torodb torod

$ sudo adduser torodb

Para comprobar que todo funciona correctamente podemos conectarnos usando el siguiente comando.

$ sudo -u torodb psql torod

5. Replicación con ToroDB Stampede

Llegados a este punto hemos completado los requisitos previos y podemos pasar a ejecutar ToroDB Stampede. Antes de continuar debemos crear el fichero .toropass en nuestra home con los datos de acceso a la base de datos PostgreSQL.

localhost:5432:torod:torodb:torodb

Ahora ya se puede descargar ToroDB Stampede y ejecutarlo con la configuración por defecto.

$ wget "https://www.torodb.com/stampede/downloads/torodb-stampede-1.0-beta.tar.bz2" -O torodb-stampede-1.0-beta.tar.bz2

$ tar xjf torodb-stampede-1.0-beta.tar.bz2

$ torodb-stampede-1.0-beta/bin/torodb-stampede > toro.log &

Para completar el tutorial importaremos un dataset en MongoDB y veremos como este es replicado en PostgreSQL.

$ wget "https://www.torodb.com/stampede/downloads/datasets/primer-dataset.json" -O primer-dataset.json

$ mongoimport -d stampede -c primer primer-dataset.json

La importación en MongoDB se realizará en la base de datos stampede y la colección primer, lo que implica que en PostgreSQL la replicación estará localizada en la base de datos torod, esquema stampede y una serie de tablas que comenzarán por el prefijo primer.


6. La estructura relacional

Durante la realización de la replicación, Stampede creará (y actualizará) una serie de tablas en función de la estructura de los documentos de entrada. En este caso la estructura de los documentos coincide con la siguiente.

{
  "address": {
     "building": "1007",
     "coord": [ -73.856077, 40.848447 ],
     "street": "Morris Park Ave",
     "zipcode": "10462"
  },
  "borough": "Bronx",
  "cuisine": "Bakery",
  "grades": [
     { "date": { "$date": 1393804800000 }, "grade": "A", "score": 2 },
     { "date": { "$date": 1378857600000 }, "grade": "A", "score": 6 },
     { "date": { "$date": 1358985600000 }, "grade": "A", "score": 10 },
     { "date": { "$date": 1322006400000 }, "grade": "A", "score": 9 },
     { "date": { "$date": 1299715200000 }, "grade": "B", "score": 14 }
  ],
  "name": "Morris Park Bake Shop",
  "restaurant_id": "30075445"
}

Por lo tanto se deberían crear cuatro tablas en la réplica relacional. Para comprobarlo podemos acceder a la base de datos mediante el cliente psql e inspeccionar la estructura de tablas creadas.

$ sudo -u torodb psql torod

torod=# set schema 'stampede'

torod=# \d
                List of relations
┌──────────┬──────────────────────┬───────┬────────┐
│  Schema  │         Name         │ Type  │ Owner  │
├──────────┼──────────────────────┼───────┼────────┤
│ stampede │ primer               │ table │ torodb │
│ stampede │ primer_address       │ table │ torodb │
│ stampede │ primer_address_coord │ table │ torodb │
│ stampede │ primer_grades        │ table │ torodb │
└──────────┴──────────────────────┴───────┴────────┘

A su vez, cada una de esas tablas tendrá una distribución de columna equivalente a los campos que contienen cada nivel del documento original. En el siguiente diagrama se puede observar la estructura creada de forma más clara.

tables_distribution

La estructura de tablas no sólo contiene los campos que componen los documentos originales, también poseen columnas de metadatos que permiten gestionar la estructura relacional. Con estos metadatos no sólo se permite la capacidad de hacer queries sino que también se permite el proceso de recomposición del documento original, lo cual permitiría a ToroDB funcionar como un sistema MongoDB.

Columna ¿Qué hace?
did Es el identificador único de documento, posee el mismo valor para todas las filas que forman parte de un mismo documento.
rid Es el identificador único de fila. Por ejemplo, si se mapea un array, es necesario este campo porque el did es siempre el mismo y se necesita otro valor para formar una clave única.
pid Es la referencia a rid del padre. Por ejemplo, primer_address_coord posee filas con rid 0 y 1 y pid 0, eso significa que esas filas son hijas de la fila con rid 0 en la tabla primer_address.
seq Representa la posición de un elemento dentro del array original.

Una vez que los datos están en una estructura relacional en PostgreSQL, se puede realizar analítica avanzada de los mismos de una forma más eficiente y sencilla que en su equivalente MongoDB.


¿Por qué lo que estás haciendo con las redes sociales de tu empresa no funciona?

$
0
0

El pasado 10 de octubre, Macu y yo acudimos al Campus Madrid para asistir a una charla impartida por María Redondo y organizada por el Instituto Internacional del Marketing. El tema fundamental era el beneficio monetario de las redes sociales corporativas. Estrategias a realizar, recomendaciones, cómo gestionar el uso de redes sociales para obtener resultados monetarios.

Antes de empezar María Redondo hizo hincapié en que la gestión de las redes sociales, no se mide ni por kilos ni por litros, sino en eficiencia del tiempo invertido y añadió que ya no vendemos, el cliente sabe lo que quiere y Google también. Por este motivo, lo más importante es posicionar tu web lo mejor posible en buscadores y conseguir con las redes sociales que los usuarios acudan a nuestra web.

Cada vez son más las personas que se interesan por el SEO y cómo pueden posicionar su web en Google y entre las diferentes estrategias está la de generar un correcto contenido. Una página web genera un contenido estático, por eso Maria Redondo hizo mucho hincapié en la integración de un blog en nuestro site con el objetivo de generar contenido dinámico. María Redondo en la charla de rrss

También es importante saber quién habla de tu marca en RRSS y qué se comenta de ella. Para esto es fundamental monitorizar y conocer la reputación online de la marca. Existen muchas herramientas para monitorizar, como Social Mention, entre otras.

En cuanto a crear estrategias, debemos tener en cuenta que los usuarios de RRSS se dividen en tres: Creators 1%, influencers 9% y lurkers 90% (miran y consumen) . Generalmente las campañas se dirigen a lurkers, pero es fundamental hacer estrategias específicas para captar influencers puesto que son éstos los que atraerán mayor número de usuarios finales.

María Redondo también hizo énfasis en la gamificación ; crear contenido simplemente para entretenimiento del usuario, sin venderle nada. Aunque parezca una pérdida de tiempo, supondrá un keep in mind, es decir, el usuario tendrá en mente esa marca que le hace pasar un rato agradable, y lo hará con un pensamiento positivo.

Pero todavía no hemos resuelto la pregunta del millón, ¿qué estrategia debo seguir?, ¿cómo se miden los resultados?. Cuando se analizan resultados en medios digitales, puedes verificar si la estrategia funciona. Con las RRSS no se obtienen beneficios de forma tradicional, el resultado se debe medir con los siguientes conceptos:

  • Conversión: conviertes el tráfico en ventas, ¿qué red social convierte más?
  • Fidelidad: nuestro contenido es tan interesante como para que nuestros seguidores acuden directamente a nuestra página web.
  • Reputación: buenos o malos contenidos.
  • Posicionamiento: El SEO deja de ser una cosa aislada del social media.
  • BBDD: una buena estrategia es el email marketing.

En la charla, María proyectó esta diapositiva con 29 puntos a tener en cuenta a la hora de medir:

Charla de María Redondo en el Campus Madrid

Los resultados obtenidos, lógicamente vendrán condicionados por las estrategias que hayas llevado a cabo. María Redondo hizo mucho hincapié en que antes de empezar con una estrategia clara debes conocer, primero la empresa, fijarte en lo que hacen tus competidores porque ellos serán tus mejores consultores, la reputación digital y recursos con los que partes.

Uno de los puntos más importantes es ver dónde se encuentra tu audiencia o más bien qué redes utilizan y en qué proporción, identificar los contenidos de valor y segmentarlos para ser más productivos. Las tácticas o cuestiones a plantearnos deberían ser:

  • ¿Qué puedes ofrecer a tu audiencia que no le ofrezcan los demás?
  • ¿Por qué tendrían que hacer “me gusta” o “follow”?
  • ¿Qué les motivaría a comentar o participar en tus redes sociales?
  • Intenta ponerte en los zapatos de quien verá el contenido y piensa si le será de utilidad.
  • ¿Qué puede crear un verdadero engagement?
  • ¿Qué acciones concretas puedes realizar para llegar a los influyentes y líderes de opinión?

María Redondo nos recomendó que las tácticas en las diferentes redes sociales, Twitter y Facebook, no son las mismas: Acciones tácticas que funcionan (Twitter):

  • Utilizar cada forma de mención de forma estratégica.
  • Tamaño de las imágenes/GIF ajustado a la red: 1240x512px.
  • Mencionar al autor original del contenido.
  • Acortar enlaces.
  • Identificar nuestras mejores horas de publicación.
  • Usar las tweetCards como recurso para incrementar el tráfico.

Acciones tácticas que funcionan (Facebook):

  • Suficiente texto en la publicación como para evitar el rebote.
  • Enlace con previsualización (con información editada).
  • Respetar medidas de imágenes: 1200x630px, 940x788px o 1200x628px.
  • Imágenes adecuadas a las expectativas de la audiencia.
  • Controlar la hora de máxima audiencia y publicar en función de eso.

Aunque cada empresa tendrá unos objetivos diferentes, todos deben de ser SMART. Sin olvidarnos de que las redes sociales no venden, los objetivos se pueden definir en torno a la VISIBILIDAD:

  • Incrementar del número de seguidores en XXXXX en un 10% en RED_1 mensual.
  • Incrementar número de seguidores en el tramo de edad de XX a YY años en un 10% en Miami mensual.
  • Aumentar número de suscripciones en un 10% mensual en Youtube DEFINIR EL SEGUIMIENTO.
  • Aumentar el número de fans de Facebook en la audiencia entre XX y YY alis eb yb 15% mensual.
  • Incrementar en un 15% mensual el número de comentarios de fans españoles en Facebook.

Una vez que la estrategia se ha puesto en movimiento, se debe monitorizar atendiendo a los siguientes puntos:

  • ¿Dónde hablan de nosotros?
  • ¿Qué dicen?
  • ¿Qué es lo que más y menos les gusta?
  • ¿Qué necesidades hay?
  • ¿Quién protagoniza las conversaciones?

Lo más importante es usar las herramientas de monitorización más adecuadas para poder extraer datos sobre la reputación online. Existen numerosas herramientas pero abarcarlas todas es imposible, por eso deben ser aquellas que proporcionen información clave para:

  • Detectar y resolver incidencias.
  • Medir la reputación online.
  • Establecer acciones de marca.

¿Qué pasa con la medición de los resultados? Durante toda la charla nos quedó claro que con las redes sociales no vendes, entonces ¿cómo se miden esos resultados? Medidores de redes sociales Algunas de las fórmulas para obtener datos fiables fueron las que María Redondo nos indicó en esta presentación.

KPIs negocio

Y planteó una pregunta al aire, ¿Medimos lo adecuado o nos autoengañamos? Para acabar esta charla y comenzar la ronda de preguntas, quiso dejarnos esta diapositiva para aclarar a todos los que allí estuvimos el objetivo de las redes sociales.

Sincronizando la entrega de valor con SAFe: El Nivel de Programa

$
0
0

Índice de contenidos

1. Introducción

Para darle continuidad a los artículos en los que he venido documentando los conceptos básicos sobre SAFe, en el presente apartado explicaré los detalles del Nivel de Programa como marco de trabajo para la sincronización de equipos ágiles en el desarrollo de software. SAFE Program Level

Figura 1. Nivel de programa. Recuperado de www.scaledagileframework.com

Según lo visto en el artículo sobre los Equipos de desarrollo con SAFe, en el Nivel de Equipo se sientan las bases sobre las que se organizan las tareas de desarrollo para entregar en funcionamiento características probadas de un componente del dominio de negocio. Según este modelo, los equipos y otros recursos se organizan en torno a la metáfora del Agile Release Train (ART) para fomentar el flujo continuo e incremental de versiones del producto. Según se explica en las guías de SAFe, cada ART se divide en los llamados Incrementos del Programa (PI, por sus siglas en inglés) y estos a su vez en ciclos cortos y de longitud fija en los que participan de 5 a 12 equipos de desarrollo.

2. El Nivel de Programa

En el Nivel de Programa es donde se descubren, definen y desarrollan las características y habilitadores necesarios para la consecución de los objetivos del negocio. En él, se provee de un Sistema Kanban para gestionar y hacer visible el flujo del trabajo para todos los usuarios. El objetivo principal de este nivel es asegurarse de que las características sean analizadas, definidas y priorizadas antes de su inclusión en el ciclo de un PI. Como parte de la definición, cada una de las características debe incluir sus criterios de aceptación como guía para lograr una implementación apegada a los requisitos del negocio. Cada una de estas características son mantenidas y priorizadas dentro del Program Backlog.

3. Roles en el Nivel de Programa

Los equipos en SAFe están concebidos para ser auto-gestionados y auto-organizados en torno al Agile Release Train. Sin embargo, este ART no se crea ni gestiona por sí mismo; se necesita orientación y dirección para que los equipos se organicen bajo una misión y arquitectura común. Para solventar estas necesidades de gestión, SAFe incluye los siguientes roles como garantía de la ejecución de las iniciativas a Nivel de Programa: SAFE Program Level Roles

Figura 2. Gestión del Programa, Gestión de Contenido y Arquitectura.

Recuperado de www.scaledagileframework.com
  • El Release Train Engineer (RTE, por sus siglas en inglés), es el Scrum Master o Jefe del ART. Su responsabilidad es la de optimizar el flujo de valor a través del programa usando varios mecanismos como la Program Kanban, sesiones de Inspección & Adaptación y las PI plannings, entre otras.
  • El Product Manager es el representante del negocio y trabaja con los Product Owners para entender sus necesidades, definir las características del sistema y participa en su validación. Es el responsable del Program Backlog.
  • El System Architect/Engineer es una persona o departamento que en la práctica es quien aplica un pensamiento sistémico. Define la arquitectura del sistema, los requisitos no funcionales y ayuda a la creación de interfaces de comunicación.

4. La Spanning Pallette

En la Big Picture se definen algunos artefactos adicionales que contribuyen en la relación entre el Nivel de Programa y el flujo de valor de SAFe. Cada uno de estos artefactos contribuyen con la Agile Release Train definiendo los siguientes aspectos clave: SAFE Spanning Palette

Figura 3. Spanning Pallette. Recuperado de www.scaledagileframework.com

  • La Visión, que describe el contexto y solución propuesta para solventar las necesidades de las partes interesadas.
  • El RoadMap, que representa una visión con los compromisos para los Incrementos en el Programa.
  • Las Métricas utilizadas para medir los sistemas y evaluar su rendimiento. En el Nivel de Programa suelen utilizarse las siguientes métricas:
    • El Tablón de progresos.
    • El Tablón Kanban del programa.
    • La predictibilidad del programa.
    • Las Métricas de rendimiento.
    • El BurnDown chart del PI.
    • El Diagrama de flujo acumulativo.
    • La Autoevaluación del ART.
  • Los Milestones, definen los puntos claves que se utilizan para medir los progresos y riesgos dentro del Roadmap.
  • Las Releases, que tienen el objetivo de ofrecer valor al cliente en ciclos de liberación frecuentes.
  • Las DevOps, que representan los mecanismos de integración necesarios para que las operaciones de desarrollo y despliegue puedan integrar a los equipos de operaciones con los equipos ágiles dentro del ART.
  • El System Team, es un equipo ágil dedicado a la asistencia en la construcción y uso de la infraestructura necesaria para entregar soluciones probadas e integradas.
  • La Gestión de la Release, consiste típicamente en la planificación, gestión y gobierno de la solución, teniendo la autoridad y responsabilidad de ayudar a guiar el flujo de valor hacia los objetivos de negocio.
  • Los Servicios Compartidos, que representan los roles necesarios para apoyar al ART y el flujo de valor pero sin una dedicación exclusiva.
  • La Experiencia del Usuario (UX, por sus siglas en inglés), que contempla las prácticas que dirigen la inclusión de aspectos como la facilidad de uso, utilidad y eficiencia de las interfaces de los sistemas.

5. Conclusiones

En el Nivel de Programa se proporciona un modelo de gestión como guía para solventar las necesidades del negocio y partes involucradas. En tal sentido, este nivel tiene como objetivo principal proporcionar mecanismos de gobernabilidad para que el Agile Release Train pueda orquestar las actividades de los equipos de desarrollo. Una de las características más relevantes de este modelo de gobernanza es que está concebido para operar en convivencia con modelos tradicionales de desarrollo de software. En Autentia proporcionamos soporte a la implantación corporativa de metodologías ágiles ayudando a la transformación digital de grandes organizaciones. Te invito a que te informes sobre los servicios profesionales de Autentia y el soporte que podemos proporcionar a tu empresa en en la implantación de frameworks de escalado de agile como SAFe y LESS.

6. Referencias

Test end-to-end con NightwatchJS

$
0
0
NightwatchJS nos provee de una forma sencilla de preparar nuestros test funcionales contra una interfaz web a través de Selenium o contra un webdriver concreto.

Índice de contenidos

1. Introducción

Enmarcado en el proceso de desarrollo mediante TDD, la parte backend suele estar cubierta mediante test unitarios y de integración. Y en la parte frontend, cada vez son más los proyectos que usan Jasmine, Karma o Mocha para cubrir su JavaScript con test. Pero a veces, nuestros proyectos adolecen de test funcionales que prueben una funcionalidad de principio a fin. NightwatchJS nos ayuda a solucionar ese problema.

En el ATDD incluso puede ser un punto de partida, pues en los diseños dirigidos por test de aceptación, precisamente hay que hacer un test que cubra el criterio de aceptación definido por el usuario, y lo normal es que el test implique una prueba de principio a fin.

NightwatchJS nos permite hacer test end-to-end en los ciclos de integración continua y es un gran aliado en los procesos de diseño dirigido por test de aceptación (ATDD).

Pero como me recuerda siempre un compañero, este tipo de test son los más frágiles, pues en cuanto cambia algo de todo el proceso, el test se rompe. Basta con que cambie el DOM de la página que queremos probar para que el test se rompa, aunque la funcionalidad se siga manteniendo. En este sentido, hay que saber lo que se tiene entre manos. En los equipos de desarrollo en los que está muy marcado el rol de quien desarrolla backend y quien desarrolla frontend, quizás la responsabilidad de mantener este tipo de test debería recaer sobre el equipo de front, pues un cambio en el DOM debería implicar una adaptación del test al cambio. Pero primero veamos un ejemplo.

1. Primer test con NightwatchJS

Vamos a preparar un test que compruebe que cuando buscamos “autentia” en nuestros buscadores favoritos, aparece en la primera página y como primer resultado. Vamos a probarlo con Google y con DuckDuckGo.

module.exports = {

  'Busqueda en Google' : function (browser) {
    browser.url('https://www.google.es')
      .waitForElementVisible('body', 2000)
      .setValue('#lst-ib', 'autentia')
      .click('input[type=submit]')
      .pause(2000)
      .assert.containsText('#rso > div', 'Autentia | Soporte a desarrollo informático')
      .end()
  },

  'Busqueda en DuckDuckGo' : function (browser) {
    browser.url('https://www.duckduckgo.com')
      .waitForElementVisible('body', 2000)
      .setValue('#search_form_input_homepage', 'autentia')
      .click('input[type=submit]')
      .pause(2000)
      .assert.containsText('#r1-0 > div', 'Autentia | Soporte a desarrollo informático')
      .end()
  }
};

Y lanzamos el test…

Efectivamente, en el test se comprueba que entre los resultados de búsqueda de ambos buscadores, aparece en primer lugar el título de la página web que estábamos buscando. Autentia aparece la primera en los dos.

2. Instalación y configuración

NightwatchJS se instala mediante npm de una forma muy sencilla. En este caso, y como yo lo voy a usar desde distintos proyectos, prefiero hacerlo de forma global. De ahí el parámetro “-g”

npm install -g nightwatch

Luego hay que descargarse la version standalone del servidor de selenium. En el momento de escribir este artículo es la 3.0.1. Así que me creo un directorio llamado bin dentro de mi proyecto, donde guardaré el servidor de selenium y los webdrivers de los distintos navegadores. Me descargo también el webdriver de Chrome y de firefox (Gecko) para mi sistema operativo.

Para no hacer muy largo esta primera toma de contacto, voy a configurar sólo el servidor de Selenium y el WebDriver de Chrome. Creo el fichero nightwatch.json

{
  "src_folders" : ["tests"],
  "output_folder" : "reports",
  "custom_commands_path" : "",
  "custom_assertions_path" : "",
  "page_objects_path" : "",
  "globals_path" : "",

  "selenium" : {
    "start_process" : true,
    "start_session" : true,
    "server_path" : "./bin/selenium-server-standalone-3.0.1.jar",
    "log_path" : "",
    "port" : 4444,
    "cli_args" : {
      "webdriver.chrome.driver" : "./bin/chromedriver"
    }
  },

  "test_settings" : {
    "default" : {
      "launch_url" : "",
      "selenium_port"  : 4444,
      "selenium_host"  : "localhost",
      "silent": true,
      "screenshots" : {
        "enabled" : false,
        "path" : ""
      },
      "desiredCapabilities": {
        "browserName": "chrome",
        "javascriptEnabled": true,
        "acceptSslCerts": true,
        "cssSelectorsEnabled": true,
        "start-maximized": true,
        "chromeOptions": {
           "args": ["start-fullscreen" , "start-maximized"]
        }
      }
    }
  }
}

Con esto, indicamos cual es la carpeta donde van a estar escritos los test, donde se van a generar los informes, donde está el JAR de Selenium y el WebDriver de Chrome, y sus parámetros de configuración

NOTA:

En MacOS X me he topado con dos problemas. El primero es que el Terminal, hay que cambiar un check por defecto en “preferencias > avanzado” y desmarcar “Ajustar variables del entorno local al arrancar”, y el segundo es por el ChromeDriver para MacOS X, que se arranca con una ventana con un ancho determinado, y puede que no sea válido para probar cuestiones relacionadas con responsive design. Para que arranque maximizado hay que añadir en chromeOptions la opción start-fullscreen y start-maximized, como aparece en el código anterior del nightwatch.json.

3. Variables globales y entornos

Este tipo de pruebas se pueden querer enmarcar dentro de procesos de integración continua, por ejemplo con Jenkins, o simplemente queremos probar una batería de test contra distintos entornos. Lo primero de todo es definir dichos entornos en el nightwatch.json y cuales son sus URLs de acceso.

{
  [...],

  "test_settings" : {
    "default" : {
      "launch_url" : "http://localhost",
      [...]
    },
    "desarrollo" : {
      "launch_url" : "http://desarrollo.midominio.com"
    },
    "integracion" : {
      "launch_url" : "http://integracion.midominio.com"
    },
    "produccion" : {
      "launch_url" : "http://www.midominio.com"
    }

  }
}

De esta forma, nuestra batería de test se lanzarán contra uno u otro entorno usando el siguiente comando

nightwatch
nightwatch --env desarrollo
nightwatch --env integracion
nightwatch --env produccion

El problema es que nuestros test no están preparados para coger la URL propia del entorno. Además, los datos son independientes del entorno que se prueba. Imaginemos que lo que estamos probando es una operativa bancaria propia de una entidad financiera. El usuario, password y número de cuenta serán distintos dependiendo del entorno. Y esto deberíamos poderlo independizar de nuestros test. Para esto podemos definir variables globales por entorno.

{
  [...],

  "test_settings" : {
    "default" : {
      "launch_url" : "http://localhost",
      [...]
    },
    "desarrollo" : {
      "launch_url" : "http://desarrollo.midominio.com",
      "globals" : {
        "username" : "jjimenezt",
        "password" : "jjimenezt",
        "numeroCuenta" : "20380100220506078090"
      }
    },
    "integracion" : {
      "launch_url" : "http://integracion.midominio.com",
      "globals" : {
        "username" : "aramirez",
        "password" : "12345678",
        "numeroCuenta" : "20140100336566676094"
      }
    },
    "produccion" : {
      "launch_url" : "http://www.midominio.com",
      "globals" : {
        "username" : "azpilicueta",
        "password" : "_1zP3l3C52t1+",
        "numeroCuenta" : "15640100988392019283"
      }
    }

  }
}

Y nuestros test tendríamos que adaptarlos para que usen estas variables globales y así independizarlos del entorno en que se está probando

module.exports = {

  'Autenticacion en el Banco' : function (browser) {
    browser.url(browser.launchUrl)
      .waitForElementVisible('body', 2000)
      .setValue('#username', browser.globals.username)
      .setValue('#password', browser.globals.password)
      .click('input[type=submit]')
  },

  'Selección de cuenta bancaria' : function (browser) {
    browser
      .waitForElementVisible('h1',2000)
      .assert.containsText('h1', 'Bienvenido')
      .setValue('#numeroCuenta', browser.globals.numeroCuenta)
      .click('#shop-send')
  },

  'Se comprueba que estamos en la cuenta seleccionada' : function(browser) {
    browser
      .pause(2000)
      .assert.containsText('.selectedAccount', browser.globals.numeroCuenta)
      .assert.containsText('#saldo', 'Su saldo es ')
      .end();
  }
};

4. Objetos de página

El tratamiento con el browser se hace mediante selectores CSS del DOM, y con él podemos realizar ciertas acciones (comandos) o afirmaciones (asserts). El caso, es que esos selectores, se pueden repetir mucho a lo largo de los test, y además son muy cercanos al DOM, mientras que probablemente los test de aceptación estén redactados en un lenguaje mucho más cercano al usuario o dueño del producto. Es por eso, que debemos emplear page objects. Martin Fowler explica muy bien porque es conveniente usar Page Objects

Los Page Objects se comportan como alias de selectores. En lugar de referirme a un elemento del DOM por su XPath o por selectores CSS, me refiero a él por un alias, de forma que

  • es más fácil de leer funcionalmente el test
  • si cambia el selector, sólo lo tenemos que cambiar en u sitio

Para usarlo sería suficiente indicar el directorio de los page objects en el nightwatch.json en el atributo page_objects_path

"page_objects_path" : "dirPageOptions"

En nuestro siguiente ejemplo, vamos a testear un cliente web de correo. Para ello vamos al directorio que hemos creado, en nuestro caso, dirPageOptions, y creamos el fichero correo.js

module.exports = {
  url: function() {
    return this.api.launchUrl;
  },
  elements: {
    username: {
      selector: 'div.login > input[type=text]'
    },
    password: {
      selector: 'div.login > input[type=password]'
    },
    botonValidarUsuario: {
      selector: 'div.login > input[type=submit]'
    },
    tituloPagina: {
      selector: '.col-md-12 > h2.heading-page'
    },
    botonRedactar: {
      selector: 'nav > .navbar-collapse > input[type=button]'
    },
    destinatario: {
      selector: '.mail > input[type=text].to'
    },
    asunto: {
      selector: '.mail > input[type=text].subject'
    },
    cuerpo: {
      selector: '.mail > textarea'
    },
    botonEnviar: {
      selector: '.mail > input[type=submit]'
    },
    correosEnviados: {
      selector: 'nav > .navbar-collapse > div.sent'
    },
    listadoCorreosEnviados: {
      selector: '.col-md-12 > .content > ul'
    }
  }
};

Luego en nuestro test vamos a probar que nos podemos autenticar bien, que podemos redactar un correo y enviarlo, y que éste aparece en la lista de correos enviados. El test quedaría como sigue:

module.exports = {
  before : function(browser) {
    ms = browser.globals.time;
  },

  'Login en el cliente de correo' : function (browser) {
    var correo = browser.page.correo();
    correo.navigate()
      .waitForElementVisible('body', ms)
      .setValue('@username', browser.globals.username)
      .setValue('@password', browser.globals.password)
      .click('@botonLogin')
      .waitForElementVisible('@tituloPagina',ms)
      .assert.containsText('@tituloPagina', 'Hola, Juan Antonio');
  },

  'Redactar un correo nuevo' : function (browser) {
    var correo = browser.page.correo();
    correo
      .click('@botonRedactar')
      .waitForElementVisible('@destinatario',ms)
      .waitForElementVisible('@asunto',ms)
      .waitForElementVisible('@cuerpo',ms)
      .setValue('@destinatario', browser.globals.correo.destinatario)
      .setValue('@asunto', 'Esto es un correo de prueba')
      .setValue('@cuerpo', 'Vamos a ver si esto llega')
      .click('@botonEnviar')
      .waitForElementVisible('body','Su correo ha sido enviado correctamente');
  },

  'Comprobar que el correo se ha enviado' : function(browser) {
    var correo = browser.page.correo();
    correo
      .click('@correosEnviados')
      .waitForElementVisible('@listadoCorreosEnviados',ms)
      .assert.containsText('@listadoCorreosEnviados','Esto es un correo de prueba');
  },

  after : function(browser) {
    browser.end();
  }
};

No hay selectores y el test es mucho más legible. Con muy pocas líneas, y de forma clara, hemos probado como autenticarnos, enviar un correo y comprobar que se ha enviado.

NightwatchJS nos permite usar Chai para aumentar la verbosidad de las aserciones. Es algo así como hamcrest pero para JavaScript. De esta forma, nos acerca un poco más al estilo de aserciones propio del BDD.

Probar aplicaciones web móviles con Appium y NightwatchJS

NightwatchJS está pensado para interactuar con navegadores. Y Appium para probar aplicaciones móviles, ya sean nativas, híbridas o web. Es posible configurar el nightwatch.json para que ambos programas colaboren, y poder probar nuestras aplicaciones web en móviles ya sean iOS o Android, e integrar las pruebas dentro del ciclo de integración continua. Este tema daría por sí sólo para otro artículo.

Enlaces y referencias

5 claves fundamentales para equipos exitosos

$
0
0
En 2012, Google se embarcó en uno de sus proyectos más ambiciosos: Crear el equipo perfecto. Tras varios años de estudio se sintetizaron las 5 claves fundamentales para poder a llegar a crear un equipo exitoso. En este artículo os desgrano la esencia de cada una de las claves.

Índice de contenidos

1. Motivación

A menudo formamos equipos basándonos en el expertise de cada uno de los miembros y confiamos en que, de alguna forma, éstos lleguen a formar un equipo. Si los individuos de un equipo tienen una alta capacidad analítica, por lo tanto el equipo debería ser inteligente, ¿no? No siempre es así. Juntar a gente muy inteligente no implica necesariamente que el CI del equipo aumente. En la mayoría de organizaciones, montar equipos altamente productivos no es producto del azar.

2. Top 5 claves para un equipo exitoso

A continuación os expongo el top 5 de las claves para un equipo exitoso. 5claves_equipos_productivos

2.1. Seguridad psicológica

  • Los miembros del equipo se sienten seguros al tomar riesgos y siendo vulnerables frente a terceros.
  • ¿Podemos tomar riesgos en el equipo sin llegar a sentirnos inseguros o avergonzados?
La seguridad psicológica es la percepción que los miembros del equipo tienen acerca de que tomar riesgos dentro del mismo no va a suponer dar una imagen de ser ignorante, incompetente, negativo o que pueda provocar un rechazo brusco. Es tener la certeza de que ningún miembro del equipo será avergonzado o castigado por admitir haber cometido un error, preguntar dudas u ofrecer nuevas ideas.

2.2. Confiabilidad

  • Todos los miembros del equipo entregan el trabajo a tiempo y con calidad homogénea.
  • ¿Podemos contar unos con otros para hacer el trabajo de alta calidad y a tiempo?
Todos los miembros del equipo confían unos en otros para hacer que todo el trabajo relacionado con la aportación de valor se complete a tiempo y de una calidad homogénea (es decir, alta calidad). Este concepto entra en contraposición de los equipos cuyos miembros eluden responsabilidades (cuando algunos de los miembros hacen mucho, y otros hacen poco, ya que si las tareas las realizan aquellos que hacen poco… nunca saldría el trabajo)

2.3. Estructura y claridad

  • Los miembros del equipo tienen bien definidos sus roles, sus objetivos y el plan para llevarlos a cabo.
  • ¿Los objetivos, roles y planes de ejecución de nuestro equipo son claros?
Cada miembro del equipo conoce y entiende las expectativas que tiene que cubrir el puesto y además conoce el proceso para cumplir dichas expectativas, siendo consciente de que el rendimiento del miembro del equipo es muy importante para el rendimiento colectivo de dicho equipo. Los objetivos pueden ser establecidos a nivel individual o a nivel de equipo, pero siempre deben ser específicos, acometibles y que imprima cierto carácter retador.

2.4. Sentido del trabajo

  • El trabajo que se está realizando es personalmente importante para los miembros del equipo.
  • ¿Estamos trabajando en algo que es personalmente importante para cada uno de nosotros?
Encontrar un sentido al propósito del propio trabajo o del resultado del mismo es importante para el rendimiento y la efectividad del equipo. Este sentido o significado referido al trabajo realizado puede ser muy diverso: desde la seguridad financiera, pasando por el apoyo familiar, el sentimiento de ayuda a conseguir objetivos en el equipo, o el poder expresarse uno mismo son algunos ejemplos.

2.5. Impacto del trabajo

  • Los miembros del equipo piensan que su trabajo importa y que están aportando valor con el cambio.
  • ¿Creemos o pensamos fundamentalmente que el trabajo que estamos realizando importa?
La percepción subjetiva de que el trabajo que estamos realizando está marcando la diferencia es importante para los equipos. El ver que el esfuerzo del equipo contribuye a los objetivos de la organización puede ser importante para revelar el impacto de dicho trabajo. teamwork

3. Conclusiones

Según este estudio, la seguridad psicológica es la clave fundamental para poder llegar a tener un equipo altamente productivo. En cierto sentido, si tenemos seguridad psicológica, podemos llegar a obtener el resto de claves si el equipo puede expresarse libremente. Como podemos comprobar en alguna ocasión, juntar a gente muy inteligente y esperar que el equipo sea altamente inteligente es, en la mayoría de los casos, una utopía. ¿Estás de acuerdo con este ranking? Déjame un comentario en el blog o escríbeme en Twitter a @drodriguezhdez y cuéntame tu experiencia.

4. Referencias

Promesas en JavaScript

$
0
0
En este tutorial veremos cómo utilizar promesas en JavaScript y algunas funcionalidades de la librería de promesas Bluebird.js para manejar operaciones asíncronas.

Índice de contenidos


1. Introducción

JavaScript utiliza un estándar para ejecutar operaciones asíncronas que se basa en el llamado continuation-passing style, accediendo al resultado de nuestras llamadas asíncronas por medio de callbacks. Este estilo de programación es difícil de manejar y modificar cuando se apilan muchas llamadas asíncronas, y también dificulta el testing de las aplicaciones. Una promesa es un objeto que queda a la escucha del resultado de una operación asíncrona y permite acceder a él del mismo modo que lo haríamos ejecutando código síncrono, sin depender de callbacks.

2. Entorno

Cualquier editor de texto y un navegador te valdrá para ejecutar los snippets de código que encontrarás en este tutorial.

3. Promesas en JavaScript

Tal vez ya hayáis leído este tutorial de Samuel Martín sobre promesas en Angular. Bien, Angular no es el único framework del lenguaje que utiliza promesas, lo cierto es que es un patrón de diseño prácticamente ubiquo en JavaScript, tanto de lado cliente como servidor, y hay multitud de frameworks que lo utilizan como respuesta estándar para operaciones asíncronas, como jQuery. De hecho, ES6 añade un objeto Promise nativo del lenguaje. Así que es muy probable que si estamos usando jQuery o Angular para lanzar peticiones asíncronas, simplemente podamos usar sus promesas sin tener que preocuparnos por nada más. No obstante, hay muchas librerías en JavaScript -especialmente en nodeJs- que hacen llamadas a API’s de third parties que no trabajan directamente con promesas, sino con callbacks. Todas las implementaciones de promesas se adhieren –o deberían– a una interfaz definida como Promises/A+.

4. ¿Qué es una promesa?

Una promesa es un objeto que envuelve una operación asíncrona y queda pendiente de su estado. Cuando realizamos operaciones asíncronas con promesas, la función que usemos devolverá una promesa. En cambio, cuando ejecutamos funciones que realizan llamadas asíncronas y ejecutan un callback cuando llega la respuesta, estas funciones no devuelven ningún valor: la única forma que tendremos de utilizar los resultados de estas llamadas es ejecutar código dentro de esos callbacks. Por ejemplo, usando el método readFile del módulo fs de nodeJs, para leer de un fichero tendríamos que llamar a fs.readFile y trabajar con ese fichero dentro del callback que le pasamos a la función:
var fs = require(‘fs’);
fs.readFile(‘file.txt’, 'utf8', function manageFile(err, data) {
    if (err) throw err;
    console.log(data);
});

Aquí, simplemente, cuando fs.readFile acceda al fichero, se ejecutará el callback pasando como primer parámetro algún error en caso de que haya habido alguno, y el fichero mismo como string como segundo parámetro -data en nuestro ejemplo-. Una promesa, en cambio, encapsula un estado que corresponde con el resultado de un callback, y tiene una api para comprobar y acceder a esos resultados cuando sea posible, fundamentalmente mediante su método then. Una promesa recibe como parámetro una función, que a su vez recibe dos funciones como parámetro: resolve y reject. Debemos ejecutar código asíncrono dentro del primer callback, e invocar resolve() sobre el resultado del callback -o reject() en caso de que haya habido un error-, para que la promesa recién creada quede a la escuche de ese valor cuando se ejecute el callback:
var readFilePromise = function(filename) {
    return new Promise(function(resolve, reject) {
        fs.readFile(filename, function(err, success) {
            if (err)
                reject(err);
            resolve(success);
        });
    });
}

readFilePromise.then(function(file) {...
});

El método then debe recibir como parámetro una función que será el resultado del callback de readFile. La promesa se asegura de que sólo sea llamado cuando el valor está disponible, y no antes. Una de las cosas buenas que tiene Bluebird es su mecanismo para promisificar funciones que realizan operaciones asíncronas. En vez de crear manualmente una promesa y llamar a resolve sobre el resultado de un callback, llamamos a Promise.promisify sobre un método asíncrono y el resultado será una función equivalente que devolverá una promesa:
var fs = require(‘fs’);
var Promise = require(‘bluebird’);
var readFileAsync = Promise.promisify(fs.readFile);
readFileAsync(‘file.txt’, ‘utf8’)
    .then(console.log)
    .catch(console.log);

La función readFileAsync devuelve una promesa. Una promesa es un objeto que está enlazado con el resultado de una operación asíncrona, y que tiene tres estados posibles: pendiente, satisfecha o rechazada (pending, fullfilled, rejected). Una promesa está pendiente mientras se espera el resultado de nuestra llamada, satisfecha cuando el resultado es exitoso, y rechazada cuando la respuesta nos indica que ha habido algún error. Dentro de la API de las promesas, las dos funciones básicas son then y catch. Ambas toman funciones como parámetros. Cuando llamamos a then sobre una promesa, la función que éste toma como parámetro sólo se llegará a ejecutar cuando el estado de la promesa pase a ser satisfecha, y recibirá por parámetro el resultado de la llamada asíncrona. Exactamente lo mismo sucede con catch, pero solamente en caso de error, y lo que recibirá su función como argumento es el error. Cuando utilizamos una promesa, el resultado de la operación, si ha sido exitosa, se pasa como parámetro automáticamente a cualquier función que pasemos a su vez como parámetro de la llamada a then, y lo mismo sucede con los errores que puedan resultar de la llamada asíncrona cuando usamos catch. En el ejemplo anterior -que no es muy sofisticado- usamos console.log en ambos casos, simplemente para ver los datos en el terminal. Una de las grandes ventajas de todo esto, es que dentro de un then, podemos utilizar una función que a su vez haga una petición asíncrona y devuelva una promesa. Bien, esta función no se ejecutará hasta que la promesa sobre la que se ha invocado su then no esté satisfecha. En un caso tan simple como el anterior, no hay mucha diferencia entre usar callbacks o usar promesas, pero las promesas se vuelven muy útiles cuando llamamos a callbacks dentro de callbacks o queremos crear piezas de código asíncrono reusables. Y cuantas más llamadas anidadas a callbacks haya dentro de callbacks, más nos beneficiaremos de usar promesas. Usando promesas, en vez de anidamiento de callbacks, tendremos trenes de métodos then, que se irán llamando subsiguientemente según se vayan resolviendo sus promesas. Las Promesas son un patrón general de desarrollo. Bluebird es solamente una implementación. Como ya dije, hay una especificación de EcmaScript6 para soportar promesas como una función nativa del lenguaje. Muchos frameworks envuelven sus llamadas asíncronas con su propia implementación de las promesas, y hay otras implementaciones como q. Este tutorial está basado en Bluebird porque es la implementación de Promesas que yo he manejado más a menudo y se ha erigido casi como un estándar en node, pero en principio, cualquier implementación que se adhiera a la interfaz A+ debería poder usarse exactamente igual. Algunas utilidades como Promise.all() no están en el spec A+, pero igualmente son implementadas por la mayoría de frameworks para trabajar con promesas.

5. Promisificación de funciones asíncronas

Nuestra función readFileAsync es el resultado de aplicar promisify sobre la función readFile del módulo fs de node. Bluebird puede convertir automáticamente funciones asíncronas en Promesas,pero solo si se cumplen ciertas condiciones: los callbacks de las funciones asíncronas que queramos promisificar deben adherirse a una determinada interfaz: la convención de nodeJs para funciones asíncronas, lo que supone que:
  • El último argumento de la función que queremos promisificar debe ser una función, que es la que se ejecutará como callback cuando la llamada asíncrona finalice:
  • El primer parámetro de este callback debe ser un objeto que, en caso de no ser nulo, indica que ha habido un error en la llamada y contiene información sobre el error.
  • El resto de parámetros constituyen la respuesta de la llamada asíncrona en caso de que no haya habido errores.
Entonces, si una función que hace una operación asíncrona se adhiere a esta convención, podemos usar el método promisify de bluebirdjs para envolver la llamada en una promesa. Envolver una única llamada asíncrona que ejecuta un solo callback tal vez no sea muy útil. No obstante, cuando tenemos varios callbacks anidados dentro de otros, generando lo que se llama el callback hell, es cuando usar promesas se vuelve especialmente útil. Por ejemplo, supongamos que nuestro fichero movies.json es un listado de nombres de películas, tal que así:
{
    "movies": [{
        "name": "The Toxic Avenger"
    }, {
        "name": "The matrix"
    }, {
        "name": "Willow"
    }]
}

y que queremos listar la información de esas películas usando la api de IMDB y el paquete imdb-api. Imdb-api es un paquete de node para hacer peticiones a la api de imdb, que es un servicio con información sobre películas. Imdb-api nos permite obtener la información de una película en concreto por su nombre, tal que: Simplemente imprimirá en nuestra consola datos de ‘The toxic Avenger’, como su fecha de estreno, el nombre del director etc. etc. Entonces, supongamos que queremos hacer lo siguiente:
  • Leer el fichero movies.json.
  • Elegir la primera película del json.
  • Llamar a imdb para obtener los datos de esa película.
  • Utilizar esta información en algún callback.
El código para hacer eso, utilizando las funciones nativas de fs e imdb-api, sería como el que sigue:
var fs = require('fs');
var imdb = require('imdb-api');
var useMovieData = function(filename, operationCallback) {
    fs.readFile(filename, function(err, file) {
        if (err)
            throw err;
        var movie = JSON.parse(file).movies[0];
        imdb.getReq(movie, function(err, data) {
            if (err)
                throw err;
            operationCallback(data);
        })
    })
}
useMovieData('movies.json', console.log);

En cambio, si promisificamos las dos llamadas asíncronas, podríamos hacerlo así:
var fs = require('fs');
var imdb = require('imdb-api');
var Promise = require('bluebird');
var getMovieAsync = Promise.promisify(imdb.getReq);
var readFileAsync = Promise.promisify(fs.readFile);
var getMovieData = function(filename) {
    return readFileAsync(filename)
        .catch(console.log)
        .then(function(file) {
            var movie = JSON.parse(file).movies[0];
            return getMovieAsync(movie);
        });
}
getMovieData('movies.json')
    .catch(console.log)
    .then(console.log);

Aquí, getMovieData lanza una petición asíncrona con readFileAsync, que como ya hemos visto devuelve una promesa. No podemos saber cuándo terminará de leer el fichero, pero la promesa que hemos asociado a readFile quedará a la escucha del resultado de la operación. En el then siguiente a readFileAsync leemos la primera película del fichero y llamamos a getMovieAsync, que a su vez hace una petición asíncrona a imdb y devuelve la promesa resultante. Las sentencias de return que hay en getMovieData son muy importantes. Si os fijáis, no estamos simplemente llamando a readFileAsync: lo estamos retornando. Y readFileAsync devuelve una promesa. Como quiera que tiene un .then asociado, lo que estamos devolviendo en realidad es la promesa que creamos dentro de la lambda que pasamos a ese then: la promesa que creamos con la llamada a getMovieAsync. Por eso el return que hay en la última línea de la función getMovieData también es muy importante, porque de otro modo no devolveríamos una promesa, sino undefined. Al devolver una promesa, podemos utilizar el resultado de llamar a getMovieData inmediatamente después de llamarla. Esto es distinto a lo que sucede si usamos callbacks, por ejemplo:
var fileContents = null;
fs.readFile('text.txt', 'utf8', function(err, file) {
    fileContents = file;
});
console.log(fileContents);
/*Será null. La llamada asíncrona de readFile todavía
se estará ejecutando en este punto del script. En cambio, este log se lanza a la consola
En cuanto el flujo del programa llega a este punto*/

Por otro lado:
var filePromise = readFileAsync('text.txt');
filePromise.then(console.log);
/* Esto sí hará log del contenido del fichero.
No obstante, el log solamente será ejecutado cuando readFileAsync termine de hacer
su trabajo, así que aunque esta línea ya haya sido ejecutada en este punto, es imposible
controlar a priori cuándo exáctamente se hará el log.*/

Sin embargo, solamente el código que esté promisificado se ejecutará en orden. El código que no vaya dentro de promesas no esperará a las llamadas asíncronas:
var fileContents = null;
filePromise.then(function(result) {
    fileContents = result;
});
console.log(fileContents); //null

Volviendo a nuestro ejemplo con imdb, las diferencias entre el código promisificado y el basado en callbacks pueden no parecer muy grandes, pero son notables: getMovieData -la versión con promesas- toma un solo parámetro en lugar de dos, lo que simplifica su uso. Además, si quisiéramos realizar varias operaciones sobre el resultado de la llamada, no habría ningún problema, mientras que hacerlo en el caso de los callbacks requeriría modificar ese código. Los niveles de indentación en useMovieData son mayores. Si hubiera más llamadas asíncronas anidadas, sería aún peor.

6. Promise.all() , some() y any()

Si esto no te ha convencido, vamos a ver un ejemplo un poco más complejo y más realista: dado que movies.txt contiene, al fin y al cabo, varias películas, ahora vamos a definir una función que llame a imdb para cada una de las películas que hay en el fichero. En el caso de querer usar las funciones nativas de fs e imdb, el código podría ser como el que sigue:
var fs = require('fs');
var imdb = require('imdb-api');
var useMoviesDataFromFile = function(file, operationCallback) {
    fs.readFile(file, 'utf8', function getFile(err, data) {
        if (err)
            throw err;
        var movies = JSON.parse(data).movies;
        var imdbMoviesData = [];
        movies.forEach(function eachMovieImdbRequest(movie, index) {
            imdb.getReq({
                name: movie.name
            }, function getMovie(err, data) {
                if (err)
                    throw (err);
                else
                    imdbMoviesData.push(data);
                if (imdbMoviesData.length == movies.length)
                    operationCallback(imdbMoviesData);
            })
        });
    });
}
useMoviesDataFromFile('movies.json', console.log);

La idea es que una vez que leemos el fichero ‘movies.json’ del filesystem, iteramos sobre el resultado y lanzamos una petición a imdb por cada película, y vamos guardando el resultado de esta llamada en el array imdbMoviesData. Al final de cada llamada, si ya tenemos los datos de todas las películas, invocamos a operationCallback sobre este array que tiene todos los datos. En el ejemplo, simplemente usamos console.log como operationCallback. Este código tiene bastantes problemas. Para empezar, al contener un callback dentro de otro, los niveles de indentación son mayores, y puede ser complicado saber exactamente en qué punto estamos cuando leemos el código. Por otro lado, dado que dentro de movies.forEach estamos lanzando peticiones asíncronas, su código no se habrá ejecutado cuando termine el foreach. Es por ese motivo que tenemos que preguntar al final de cada iteración si imdbMoviesData -el array donde estamos guardando los resultados de las peticiones a imdb- tiene el mismo número de elementos que nuestro array movies donde habíamos almacenado cada título de película. Sólo cuando son iguales podemos operar sobre imdbMoviesData con la seguridad de que tendrá los datos de todas las películas -si no ha habido errores-. Esta solución complica el código y dificulta tanto su lectura como su edición. ¿No sería mejor poder ejecutar código después del forEach con la seguridad de que podemos acceder a lo que se haya fijado dentro del bucle? La función Promise.all de bluebird nos permite iterar sobre un array de promesas y operar sobre un array equivalente de sus resultados cuando éstas son resueltas satisfactoriamente. Con esto en mente, podríamos escribir nuestro código como sigue:
var Promise = require('bluebird');
var imdb = require('imdb-api');
var fs = require('fs');
var readFileAsync = Promise.promisify(fs.readFile);
var imdbGetAsync = Promise.promisify(imdb.getReq);
var getAllMoviesFromFile = function(file) {
    return readFileAsync('movies.json')
        .then(function(moviesfile) {
            var movies = JSON.parse(moviesfile).movies;
            var promises = [];
            movies.forEach(function(movie) {
                promises.push(imdbGetAsync(movie))
            });
            return Promise.all(promises);
        });
}
getAllMoviesFromFile('movies.json').catch(console.log).then(console.log);

Aquí, promises es un array de promesas. Cuando termine el forEach, tendremos una promesa -que envuelve una petición asíncrona a imdb- por cada película. No obstante, no tenemos la seguridad de que, cuando termine el foreach, estas peticiones hayan obtenido un resultado -de hecho, es absolutamente seguro que no les habrá dado tiempo a obtener una respuesta-. Pero, como nuestro array es de promesas, y no de los resultados de las llamadas asíncronas, podemos usar Promise.all para devolver, a su vez, una promesa que sólo quedará satisfecha cuando todas las promesas del array estén satisfechas. El último console.log que se pasa como parámetro a getAllMoviesFromFile... ...then() está actuando sobre el resultado de esa promesa, devuelta a su vez por Promise.all, que devolverá un array cuando su estado quede satisfecho. El código del último ejemplo, aunque promisificado, es un poco feo, en el siguiente sentido: dado que then toma funciones como argumento, es mucho más adecuado definir nuestras funciones al margen de la cadena de promesas y luego pasarlas a los then por su nombre, sin incluír bloques enteros de código dentro de funciones anónimas. Además,en vez de pasar funciones directamente a los then, podemos invocar funciones que devuelvan a su vez… funciones, lo que nos permite agilizar algo el código con algunas técnicas de programación funcional. Un ejemplo de código más conciso y legible podría ser el siguiente:
var Promise = require('bluebird');
var imdb = require('imdb-api');
var fs = require('fs');
var readFileAsync = Promise.promisify(fs.readFile);
var imdbGetAsync = Promise.promisify(imdb.getReq);
var getMovie = function(movie) {
    return imdbGetAsync(movie);
}
var parseMoviesFile = function(file) {
        return JSON.parse(file).movies;
    }
    //nota que esto devuelve una función
var mapToMovies = function(func) {
    return function(movies) {
        return movies.map(func);
    }
}
var getAllMoviesFromFile = function(file) {
    return readFileAsync('movies.json')
        .catch(console.log)
        .then(parseMoviesFile)
        .then(mapToMovies(getMovie))
        .then(Promise.all);
}
getAllMoviesFromFile('movies.json').catch(console.log).then(console.log);

Bluebird expone métodos como some y any que son similares a all, pero que devuelven una promesa que se satisface solamente cuando un número determinado de las promesas del array quedan satisfechas -en some– o cuando hay al menos una -en any-. Puedes comprobar la api de bluebird aquí.

7. Creación de nuevas promesas

Un problema que nos puede surgir trabajando con promesas e intentando promisificar nuestro código, es que no todas las funciones asíncronas se adhieren a la interfaz error-success de node, lo que provocará que no podamos promisificarlas con Promise.promisify. En ese caso, como en nuestro primer ejemplo, tendremos que crear una nueva promesa y llamar a las funciones resolve y reject que recibe como parámetros. Por ejemplo, una función asíncrona muy conocida que no se adhiere a la interfaz de callbacks de node es setTimeout. La función setTimeout toma el callback que va a ejecutar como primer parámetro, y el tiempo que debe esperar para ejecutarla como segundo parámetro. Un ejemplo de cómo promisificar setTimeout es este pequeño programa (que destruirá el mundo la mitad de las veces, tal vez no queráis ejecutarlo en vuestra máquina 0_0):
var Promise = require('bluebird');
var doomMechanism = function() {
    var randomNumber = Math.floor(Math.random() * 2);
    if (randomNumber == 0)
        throw new Error("Doom!!!");
    return "World is safe";
}
new Promise(function(resolve, reject) {
        setTimeout(function() {
            try {
                var isArmaggeddonSkipped = doomMechanism();
                resolve(isArmaggeddonSkipped);
            } catch (err) {
                reject(err);
            }
        }, 1000);
    })
    .catch(console.log)
    .then(console.log);

La idea es ejecutar la llamada asíncrona en la función que se pasa al constructor de la nueva promesa, y llamar a resolve siempre al final de la ejecución del callback que se ejecuta en la respuesta, pasando por parámetro el resultado de la llamada.

8. Conclusiones

Las promesas permiten escribir código asíncrono que se comporta como su contrapartida síncrona: los métodos devuelven valores en vez de ejecutar callbacks, el orden cronológico del programa sigue el orden del código y podemos utilizar el resultado de nuestras operaciones asíncronas sin tener que pasar operaciones a través de cadenas de callbacks.

9. Referencias

Algunos de los mejores enlaces que podéis leer sobre promesas en JavaScript son los siguientes:
Viewing all 996 articles
Browse latest View live