lunes, 19 de enero de 2015

Cambiar el estado de una aplicación Web

Introducción

Una aplicación Web generalmente debe disponer de algún mecanismo que permita monitorear su funcionamiento y detectar si se ha presentado un problema para su corrección lo antes posible.

Dependiendo del lenguaje y framework usado se tienen herramientas que permiten una gestión más avanzadas que otras, por ejemplo JMX en el caso de Java o GOD para Ruby on Rails.

Sin embargo una de las prácticas más comunes en las aplicaciones Web, independiente del lenguaje/framework, es tener una URL a la cual se pueda realizar una petición HTTP GET y la respuesta sea texto plano con una palabra como "OK", "UP", etc. si todo está funcionando correctamente. Esta práctica se conoce comúnmente como "Health Check" (o "Health Endpoint Monitoring Pattern" en la literatura de Microsoft) y es por eso que generalmente tienen la forma http://dominio/health.

Es una práctica muy popular no solamente por lo sencilla que puede ser de implementar e integrara en herramientas de monitoreo como New Relic o Pingdom, sino porque también es muy usada para otros propósitos, como por ejemplo en balanceadores de carga para determinar a que nodo se pueden enviar las peticiones (Amazon Route 53 entre otros).

Problema a tratar

No siempre es el caso, pero en muchas ocasiones lo que se hace para estos "Health Check" es que la URL siempre retorne el texto de éxito, con lo cual la herramienta de monitoreo o el balanceador solamente detectaría el problema cuando la aplicación Web deje de responder peticiones HTTP del todo.

Para los casos más básicos de monitoreo esto puede ser suficiente, pero existen situaciones en los cuales se requiere que la aplicación (o el nodo de la aplicación) deje de funcionar, bien sea por una migración, despliegue, etc. Algunos balanceadores de carga permiten indicar que deje se enviar peticiones a dicha instancia, pero ¿Qué tal si el balanceador no lo soporta? ¿Qué tal si la aplicación no solamente recibe peticiones HTTP del balanceador, sino que tiene procesos automáticos internos (cron jobs mediante Quartz por ejemplo)?

En ciertas ocasiones lo que se hace es esperar una ventana de tiempo en la cual no hay tareas automáticas programadas y estar pendientes de los logs de la aplicación, esperando un momento en el cual no se esté procesando nada para bajarla o reiniciarla manualmente, sin embargo esto puede generar errores por cuestiones de milisegundos y en algunos contextos ello puede ser muy perjudicial.

Lo que se desarrollará

En este post se mostrará no solamente cómo desarrollar una aplicación Web con una URL de "Health Check", sino también cómo indicar que está activa o inactiva manualmente. Aunque se usará Java y Jersey la idea es aplicable a cualquier otro lenguaje/framework.
  • http://localhost:8080/health: Un HTTP GET a esta URL retornará el estado actual de la aplicación, siendo:
    • UP: La aplicación está funcionando correctamente.
    • DOWN: La aplicación no debe procesar ninguna acción.
  • http://localhost:8080/toggleStatus: Un HTTP PUT a esta URL (mediante herramientas como cURL o Postman) cambiará el estado de la aplicación, pasando de "UP" a "DOWN" y viceversa.
El código fuente completo está disponible en: https://github.com/guillermo-varela/status-changer

Aplicación Web con Java

Paso 1: Health Check Endpoint
Para empezar se desarrollará la clase que recibirá las peticiones HTTP GET, con las anotaciones de Jersey, retornando siempre el texto "UP", indicando que la aplicación funciona normalmente.

package com.blogspot.nombre_temp.resource;

import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

@Path("/health")
@Produces(MediaType.TEXT_PLAIN)
public class HealthResource {

    /**
     * Gets the current status of this application instance.
     * 
     * @return Current status of this application instance.
     */
    @GET
    public String health() {
        return "UP";
    }
}

La siguiente clase simplemente indica el/los paquetes en los que estarán los recursos RESTful que se expondrán a través de Jersey.
Nota: Esto requiere requiere soporte de Servlets 3

package com.blogspot.nombre_temp.config;
import javax.ws.rs.ApplicationPath;

import org.glassfish.jersey.server.ResourceConfig;

@ApplicationPath("/")
public class WebApplicationConfig extends ResourceConfig {

    public WebApplicationConfig() {
        packages("com.blogspot.nombre_temp.resource");
    }
}
Si se despliega la aplicación en un contenedor de Servlets 3 (Tomcat 7+ por ejemplo), suponiendo que el nombre del proyecto sea "status-changer", al acceder a la URL http://localhost:8080/status-changer/health el resultado sería simplemente el texto "UP", como es de esperarse.

Figura 1 - Navegador con el estado actual

Paso 2: Contenedor de Estado
Como primer paso para hacer que el estado pueda cambiarse se requiere un mecanismo que permita almacenar el estado actual y consultarlo, no solamente desde la clase que expone el servicio "/health" sino también a componentes internos de la aplicación (como los ya mencionados cron jobs).

Para mantener el ejemplo sencillo, el estado se almacenará en memoria, mediante un atributo estático de una clase, cuyo valor podrá ser accedido por cualquier otro componente de la aplicación, en cualquier hilo.

package com.blogspot.nombre_temp.util;

public class StatusHolder {

    private static volatile Status currentStatus = Status.UP;

    public static enum Status {
        UP, DOWN;
    }

    public static Status getCurrentStatus() {
        return currentStatus;
    }

    public static synchronized void setCurrentStatus(Status currentStatus) {
        StatusHolder.currentStatus = currentStatus;
    }

    /**
     * Allows changing the current status of this application instance.
     * 
     * @return Current status after the change.
     */
    public static synchronized Status toggleStatus() {
        if (currentStatus == Status.UP) {
            currentStatus = Status.DOWN;
        } else {
            currentStatus = Status.UP;
        }
        return currentStatus;
    }
}

El atributo "currentStatus" se inicializa con el valor "UP" indicando que por defecto la aplicación estará en capacidad de procesar peticiones, es estática para que su valor sea el mismo para toda la clase y usa el modificador "volatile" para garantizar que su valor es leído de manera consistente en ambientes concurrentes (multi-thread).

El enum "Status" garantiza que el estado de la aplicación solamente podrá tener dos valores: "UP" y "DOWN".

Los métodos que modifican el valor de "currentStatus" son estáticos y synchronized, para garantizar que solamente un hilo a la vez podrá editar este valor.

Paso 3: Estado dinámico externamente
Ahora que se tiene como acceder y modificar el estado actual de la aplicación, se deben exponer estas funcionalidades en el API RESTful que se inició en el paso 1, haciendo uso de la nueva clase "StatusHolder".

package com.blogspot.nombre_temp.resource;

import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import com.blogspot.nombre_temp.util.StatusHolder;

@Path("/health")
@Produces(MediaType.TEXT_PLAIN)
public class HealthResource {

    /**
     * Gets the current status of this application instance.
     * 
     * @return Current status of this application instance.
     */
    @GET
    public String health() {
        return StatusHolder.getCurrentStatus().name();
    }

    /**
     * Allows changing the current status of this application instance.
     * 
     * @return Current status after the change.
     */
    @PUT
    @Path("/toggleStatus")
    public String toggleStatus() {
        return StatusHolder.toggleStatus().name();
      }
}

El nuevo método "toggleStatus" permite acceder a la URL http://localhost:8080/status-changer/health/toggleStatus mediante una petición HTTP PUT lo cual cambiará el estado de la aplicación, como se indicó en la sección "Lo que se desarrollará", permitiendo que cualquiera que invoque el método "StatusHolder.getCurrentStatus()" obtenga el estado actualizado de la aplicación, como ahora es el caso del método "health".
Figura 2 - Cambiando el estado

Para el caso de otros componentes, como las tareas automáticas, dado que el estado se almacena de manera pública y estática, bastaría con poner una condición al inicio que impida su ejecución si "StatusHolder.getCurrentStatus()" retorna "DOWN".

Extra: Autenticación
La URL que permite el cambio de estado de la aplicación no debería ser pública, aún dentro de la misma organización, así que como paso adicional se mostrará cómo puede adicionarse autenticación.

La idea es mantener el ejemplo lo más sencillo posible, así que se usará la autenticación proporcionada por el propio contenedor de Servlets, la cual puede ser integrada fácilmente con Jersey.

Quienes requieran un sistema de autenticación más avanzados, pueden revisar las Recursos Adicionales al final del post.

Para empezar, dado que las anotaciones de Servlets 3 para seguridad solamente cubren el uso de Serlvets, será necesario crear un archivo "web.xml" el cual se indique el acceso a la URL "/health/toggleStatus" solamente estará permitido a los usuarios con rol "admin" y se requerirá autenticación tipo HTTP Basic.

<web-app version="3.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemalocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">

 <display-name>status-changer</display-name>

 <security-constraint>
  <web-resource-collection>
   <web-resource-name>Change Current Status</web-resource-name>
   <url-pattern>/health/toggleStatus</url-pattern>
  </web-resource-collection>
  <auth-constraint>
   <role-name>admin</role-name>
  </auth-constraint>
 </security-constraint>

 <security-role>
  <role-name>admin</role-name>
 </security-role>

 <login-config>
  <auth-method>BASIC</auth-method>
 </login-config>
</web-app>

En caso de usar Tomcat, la información de los usuarios se almacena por defecto en el archivo "$CATALINA_BASE/conf/tomcat-users.xml" y se pueden indicar estos datos a manera de ejemplo:

<role rolename="admin"/>
<role rolename="user"/>
<user username="admin1" password="admin1" roles="admin"/>
<user username="user1" password="user1" roles="user"/>

Al tratar de cambiar el estado como "user1", se obtendrá se rechazará la petición.
Figura 3 - Cambio de estado prohibido

Nota: En caso de usar una instancia de Tomcat a través del IDE Eclipse, no debe editarse directamente el archivo "tomcat-users.xml" directamente en la carpeta de instalación de Tomcat ni en la carpeta del workspace actual (temp0, temp1, dependiendo de cuantos servidores se tenga), sino que se debe editar el archivo que aparece dentro de la vista "Project Explorer" en el proyecto "Servers". Esto debido a que Eclipse sobrescribirá lo que se ponga en la capeta del workspace con lo que se tenga en "Servers":
Figura 4 - Archivo de usuarios de Tomcat en Eclipse

Recursos Adicionales