Testcontainers + Localstack +AWS Java SDK 2.x

Al usar localstack en las pruebas y el sdk 2.x de AWS para Java no es tan simple como sacar el endpoint y el credential provider de localstack y pasarlo al cliente de aws, hay que constuir primeros los objetos del sdk 2 para pasarlos al cliente con datos que entrega localstack:

private static LocalStackContainer ls = null;

@BeforeAll
public static void before() {
    ls = new LocalStackContainer(DockerImageName.parse("localstack/localstack"))
    	.withServices(DYNAMODB);
    ls.start();
    final var credentials = ls.getDefaultCredentialsProvider().getCredentials();
    final var ddbc = DynamoDbClient.builder()
            .endpointOverride(ls.getEndpointOverride(DYNAMODB))
            .credentialsProvider(
                    StaticCredentialsProvider.create(AwsBasicCredentials.create(
                            credentials.getAWSAccessKeyId(),
                            credentials.getAWSSecretKey()))).build();
}

ese el código para construir el cliente de dynamo usando el endpoint override (eso fue simple) y el credential provider (eso fue un poco más de trabajo) usando el access key y secret key

también dejo los imports:

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.localstack.LocalStackContainer;
import static org.testcontainers.containers.localstack.LocalStackContainer.Service.DYNAMODB;
import org.testcontainers.utility.DockerImageName;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;

Experimento concurrente

En esta entrada dejaré el resultado de un experimento que surgió por una difícil pregunta que me generó varias dudas...

El experimento es revisar el comportamiento de un programa usando un Map en forma concurrente:

Comenzamos creando la clase con el map, luego agregarle algunos datos y el main:
public class Concurrido {
  private final Map<Integer, String> estado = new HashMap<y>();
  {
     IntStream.rangeClosed(1, 1_000_000)
        .forEach(i -> estado.put(i, String.valueOf(1)));
  }

  public static void main(String[] args) throws InterruptedException {
     final var c = new Concurrido();
     final var t1 = new Thread(() -> {
         c.m1();
         System.out.println("m1 terminado");
     });
     final var t2 = new Thread(() -> {
         c.m2();
         System.out.println("m2 terminado");
     });

     t1.start();
     t2.start();
     t1.join();
     t2.join();
     System.out.println("fin");
  }
el main lanzará 2 hilos (t1 y t2), cada uno tendrá acceso al mismo objeto (c) intentando modificar su estado en distintos métodos (m1 y m2) de la misma referencia.

Prueba 1

la primera prueba es crear m1 y m2 sin ninguna modificación especial, uno quita elementos y otro los muestra:
    private void m1() {
        System.out.println("trabajando en m1");
        for (var j = 0; j < 100_000_000_0L; j++) {
            estado.remove(j);
        }
    }

    private void m2() {
        System.out.println("trabajando en m2");
        estado.forEach((a, b) -> {});
    }
al lanzarlo el resultado es el siguiente:
trabajando en m1
trabajando en m2
Exception in thread "Thread-1" java.util.ConcurrentModificationException
	at java.base/java.util.HashMap.forEach(HashMap.java:1428)
	at com.sebastian.concurrente.Concurrido.m2(Concurrido.java:47)
	at com.sebastian.concurrente.Concurrido.lambda$main$2(Concurrido.java:26)
	at java.base/java.lang.Thread.run(Thread.java:832)
m1 terminado
fin
No se puede ocupar en forma concurrente como m1 y m2 están definidos

Prueba 2

Lo siguiente será usar synchronized solo en un bloque dentro de m1:
    private void m1() {
        synchronized(estado) {
            System.out.println("trabajando en m1");
            for (var j = 0; j < 100_000_000_0L; j++) {
                estado.remove(j);
            }
        }
    }

    private void m2() {
        System.out.println("trabajando en m2");
        estado.forEach((a, b) -> {});
    }
Algo que no será muy útil pues se está sincronizando solo un bloque dentro del método m1:
trabajando en m1
trabajando en m2
Exception in thread "Thread-1" java.util.ConcurrentModificationException
	at java.base/java.util.HashMap.forEach(HashMap.java:1428)
	at com.sebastian.concurrente.Concurrido.m2(Concurrido.java:49)
	at com.sebastian.concurrente.Concurrido.lambda$main$2(Concurrido.java:26)
	at java.base/java.lang.Thread.run(Thread.java:832)

Prueba 3

Ahora será sincronizar ambos métodos sobre la misma variable de instancia:
    private void m1() {
        synchronized(estado) {
            System.out.println("trabajando en m1");
            for (var j = 0; j < 100_000_000_0L; j++) {
                estado.remove(j);
            }
        }
    }

    private void m2() {
        synchronized(estado) {
            System.out.println("trabajando en m2");
            estado.forEach((a, b) -> {});
        }
    }
Genera lo siguiente:
trabajando en m1
m1 terminado
trabajando en m2
m2 terminado
fin
En este caso como el lock se obtiene sobre el estado en m1, m2 no podrá obtenerlo hasta que m1 termine. funciona sin generar error.

Prueba 4

lo siguiente será usar synchronized en m1:
    private synchronized void m1() {
        System.out.println("trabajando en m1");
        for (var j = 0; j < 100_000_000_0L; j++) {
            estado.remove(j);
        }
    }

    private void m2() {
        System.out.println("trabajando en m2");
        estado.forEach((a, b) -> {});
    }
y genera lo siguiente:
trabajando en m1
trabajando en m2
Exception in thread "Thread-1" java.util.ConcurrentModificationException
	at java.base/java.util.HashMap.forEach(HashMap.java:1428)
	at com.sebastian.concurrente.Concurrido.m2(Concurrido.java:48)
	at com.sebastian.concurrente.Concurrido.lambda$main$2(Concurrido.java:27)
	at java.base/java.lang.Thread.run(Thread.java:832)
m1 terminado
fin
en este caso synchronized en el método obtiene el lock sobre la instancia, pero m2 no ocupa lock asi que explota igual

Prueba 5

Ahora dejaré ambos métodos con synchronized, así ambos tienen que competir por el lock sobre la instancia:
    private synchronized void m1() {
        System.out.println("trabajando en m1");
        for (var j = 0; j < 100_000_000_0L; j++) {
            estado.remove(j);
        }
    }

    private synchronized void m2() {
        System.out.println("trabajando en m2");
        estado.forEach((a, b) -> {});
    }
y el resultado es el siguiente:
trabajando en m1
m1 terminado
trabajando en m2
m2 terminado
fin
funciona, como el lock es sobre la instancia es equivalente a:
    private synchronized void m1() {
        System.out.println("trabajando en m1");
        for (var j = 0; j < 100_000_000_0L; j++) {
            estado.remove(j);
        }
    }

    private void m2() {
        synchronized(this) {
            System.out.println("trabajando en m2");
            estado.forEach((a, b) -> {});
        }
    }
y NO equivale a:
    private synchronized void m1() {
        System.out.println("trabajando en m1");
        for (var j = 0; j < 100_000_000_0L; j++) {
            estado.remove(j);
        }
    }

    private void m2() {
        synchronized(estado) {
            System.out.println("trabajando en m2");
            estado.forEach((a, b) -> {});
        }
    }
m1 está obteniendo el lock sobre la instancia y m2 sobre una variable de instancia, el resultado es:
trabajando en m1
trabajando en m2
Exception in thread "Thread-1" java.util.ConcurrentModificationException
	at java.base/java.util.HashMap.forEach(HashMap.java:1428)
	at com.sebastian.concurrente.Concurrido.m2(Concurrido.java:49)
	at com.sebastian.concurrente.Concurrido.lambda$main$2(Concurrido.java:27)
	at java.base/java.lang.Thread.run(Thread.java:832)
m1 terminado
fin


Prueba 6

En esta prueba el map será un ConcurrentHashMap:
private final Map<Integer, String> estado = new ConcurrentHashMap<>();
y los métodos serán sin modificaciones:
    private void m1() {
        System.out.println("trabajando en m1");
        for (var j = 0; j < 100_000_000_0L; j++) {
            estado.remove(j);
        }
    }

    private void m2() {
        System.out.println("trabajando en m2");
        estado.forEach((a, b) -> {});
    }
El resultado es:
trabajando en m2
trabajando en m1
m2 terminado
m1 terminado
fin
En este caso ni t1 ni t2 esperan, ambos hilos pasan a m1 y m2 respectivamente y funciona. 

Así finalizan las pruebas 😀

Stub con Jetty embebido en Test con JUnit 5


En esta entrada mostraré un pequeño ejemplo para utilizar al servidor Jetty embebido durante las pruebas (con JUnit 5) para cuando necesitemos un Stub de algún servicio fuera de nuestro alcance.

1. Las dependencias:

    <dependency>
        <groupId>org.eclipse.jetty</groupId>
        <artifactId>jetty-webapp</artifactId>
        <version>9.4.28.v20200408</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-api</artifactId>
        <version>5.6.0</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-engine</artifactId>
        <version>5.6.0</version>
        <scope>test</scope>
    </dependency>
2. La clase para definir los handlers, lanzar y detener el servidor Jetty en las pruebas:
import java.net.URI;
import java.nio.file.Path;
import java.util.concurrent.ThreadLocalRandom;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.HandlerList;
import org.eclipse.jetty.server.handler.ResourceHandler;
import org.eclipse.jetty.servlet.ServletHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.resource.PathResource;

public class JettyStub {
  private Server server;

  public URI uri() throws Exception {
    crearServer(ThreadLocalRandom.current().nextInt(10000, 20000));
    server.setStopAtShutdown(true);
    server.start();
    return server.getURI();
  }

  public void stop() throws Exception {
    server.stop();
  }

  private void crearServer(int port) throws Exception {   
    server = new Server(port);
    
    // definir recursos desde el sistema de archivos
    final var rh = new ResourceHandler();
    rh.setDirectoriesListed(true);
    rh.setBaseResource(new PathResource(
        Path.of(System.getProperty("user.dir")).resolve("target/test-classes")));
    
    // definir servlets
    final var sh = new ServletHandler();        
    sh.addServletWithMapping(new ServletHolder(new HelloServlet()), "/hello/*");
    
    // agregar los handlers
    server.setHandler(new HandlerList(rh, sh));
  }
}
en esta clase agregué un handler para contenido estático desde el sistema de archivos y un servlet que responde en el path /hello/*:
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class HelloServlet extends HttpServlet {
  public void doGet(HttpServletRequest req, HttpServletResponse res)
      throws ServletException, IOException {
    res.getOutputStream().print("""
        {"hola": "chao"}""");
  }
}
el servlet responderá un JSON y en el contenido estático también existe un archivo llamado demo.json (carpeta resources de test) con el siguiente contenido:
{"a":"b","c":"d"}
3. Las pruebas son las siguientes:
public class StubJettyTest {
  private static final JettyStub JS = new JettyStub();
  /** uri en la que responde el server. */
  private static URI uri;

  @BeforeAll
  public static void beforeAll() throws Exception {
    uri = JS.uri();
  }

  @AfterAll
  public static void afterAll() throws Exception {
    JS.stop();
  }

  @Test
  public void obtengoElContenidoEsperadoDesdeServlet() throws MalformedURLException {
    WebClient client = new WebClient();     
    var json = client.getContent(uri.resolve("/hello").toURL());
    assertEquals("""
        {"hola": "chao"}""", json);
    json = client.getContent(uri.resolve("/hello?a=b").toURL());
    assertEquals("""
        {"hola": "chao"}""", json);
  }
  
  @Test
  public void obtengoElContenidoEsperadoDesdeArchivo() throws MalformedURLException {
    WebClient client = new WebClient();  
    String json = client.getContent(uri.resolve("demo.json").toURL());
    assertEquals("""
            {"a":"b","c":"d"}""", json);
  }
}
y eso es todo, de esta simple manera podemos tener los stubs de los servicios que necesitamos durante nuestras pruebas 😌.

En Github está el ejemplo junto a otros.

Codersrank - TOP DEVELOPERS IN JAVA [UPDATE]

Ahora estoy en el puesto número 1 en codersrank, así que dejaré las capturas para el recuerdo 👻:

  • Chile + Java = 1





  • Chile + Santiago + Java = 1




¿y cual será el motivo si mi puntaje no ha cambiado? 😀


GraalVM + Java 11 + AWS Lambda + PostgreSQL + DynamoDB + Docker

En esta ocasión mostraré la travesía que fue conseguir compilar un jar con GraalVM y que luego corra en AWS Lambda.

¿Por qué hacerlo?
En la actualidad, el cold start de lanzar un jar en un lambda es alto, varios segundos (en node puede ser medio segundo o menos), pero luego de eso cada invocación tiene un tiempo de respuesta bueno, son solo algunos milisegundos. Entonces mi motivación es hacer cada vez el cold start menor y si mejora el tiempo de respuesta de las siguientes invocaciones es aún mejor 😉.

¿Que se necesita?
  1. Java 11
  2. Docker
  3. Maven
  4. Acceso a Lambdas en AWS

¿Cómo hacerlo?
Esta es la parte entretenida, en un principio fue compleja pero luego de entenderla fue divertido.
  1. Para poder lanzar el jar compilado hay que utilizar AWS Lambdas con un custom runtime.
  2. El custom runtime tiene un comportamiento que debe ser seguido y se describe en este link y acá. En resumen: existe una URL para obtener un evento, otra para indicar si hay un error al iniciar, también una para enviar la respuesta del evento y la última para notificar error al procesar el evento. Considerar que la aplicación debe mantenerse en ejecución esperando nuevos eventos.
  3. Antes de poder tener el jar compilado hay que compilarlo... Para eso usé GraalVM con Docker.
Con esas mínimas consideraciones comenzamos...

El siguiente código (en un proyecto usando maven) es lo mínimo necesario para cumplir: 
public class App {

    public static void main(String[] args) throws MalformedURLException, IOException {

        while (true) {
            final var url = new java.net.URL(
                    new StringBuilder("http://")
                            .append(System.getenv("AWS_LAMBDA_RUNTIME_API"))
                            .append("/2018-06-01/runtime/invocation/next")
                            .toString());

            final var conn = url.openConnection();
            final var requestid = conn.getHeaderField("Lambda-Runtime-Aws-Request-Id");

            try (final var is = conn.getInputStream()) {
                int lee;
                while ((lee = is.read()) != -1) {
                    System.out.print((char) lee);
                }
            }

            ((HttpURLConnection) conn).disconnect();
            final var ok = new java.net.URL(
                    new StringBuilder("http://")
                            .append(System.getenv("AWS_LAMBDA_RUNTIME_API"))
                            .append("/2018-06-01/runtime/invocation/")
                            .append(requestid).append("/response")
                            .toString());

            final var responsehttp = (HttpURLConnection) ok.openConnection();
            responsehttp.setRequestMethod("POST");
            responsehttp.setDoOutput(true);
            responsehttp.connect();

            try (final var os = responsehttp.getOutputStream()) {
                os.write("ok".getBytes());
                os.flush();
            }

            try (final var is = responsehttp.getInputStream()) {
                int lee;
                while ((lee = is.read()) != -1) {
                    System.out.print((char) lee);
                }
            }
            responsehttp.disconnect();
        }
    }
}

Explicación del código:
  • Usa el while(true) para quedar corriendo mientras espera nuevos eventos.
  • Crea la URL para acceder a los eventos.
  • Cuando recibe un evento obtiene el Lambda-Runtime-Aws-Request-Id (necesario para luego enviar la respuesta, está descrito en los links de arriba).
  • Lee desde el InputStream los datos del evento recibido y los muestra.
  • Crea la URL para enviar la respuesta.
  • Escribe ok como respuesta al evento recibido (en un POST).
  • Lee el InputStream del POST enviado (necesario para poder recibir un nuevo evento).
  • Vuelve a esperar un evento.
  • Usa AWS_LAMBDA_RUNTIME_API para obtener la ip y puerto al que se le envía la respuesta y se obtienen los eventos.
Luego de eso hay que generar el jar con mvn package y continuar con la compilación usando GraalVM y Docker. El siguiente es el Dockerfile:

FROM oracle/graalvm-ce:19.3.1-java11 as graalvm
COPY . /app
WORKDIR /app
RUN gu install native-image
RUN native-image --no-server -H:FallbackThreshold=0 -H:+ReportExceptionStackTraces -H:+AddAllCharsets -H:EnableURLProtocols=http,https --enable-all-security-services -H:+JNI -H:+TraceServiceLoaderFeature -H:+StackTrace  -jar target/lambda-graal-1.0-SNAPSHOT.jar

FROM frolvlad/alpine-glibc
COPY --from=graalvm /app/lambda-graal-1.0-SNAPSHOT /app/bootstrap

Lanzarlo con:
docker build -t demo .

Ahora que tenemos la imagen hay que extraer el binario generado con GraalVM y dejarlo ejecutable (el nombre tiene que ser bootstrap): 
docker run --rm demo cat /app/bootstrap > bootstrap
chmod +x bootstrap

Luego generamos el zip para subirlo a AWS Lambda:
zip function.zip bootstrap

Y eso es todo!

Para crear una aplicación nativa PERO más compleja usando ServiceLoader, DynamoDB y PostgreSQL Necesité lo siguiente:
  • Hay que tener el archivo libsunec.so y cacerts.
  • Ocupé el plugin Shade para generar un uber-jar.
  • En la dependencia de DynamoDB quité apache-client y netty-nio-client.
  • Usé url-connection-client como dependencia (por eso quité los anteriores).
  • Agregué slf4j-simple para evitar el error UnresolvedElementException con org.slf4j.impl.StaticLoggerBinder.
  • Generé el archivo scripts.sh para realizar todo el proceso.
  • Usé la variable de ambiente _HANDLER que entrega lo que se escribe en la consola de AWS :

  • Creé una interface para servicios (LambdaService) y otra para los handlers (LambdaHandler).
  • La clase que se escribe en el handler de la consola de AWS tiene que ser un LambdaHandler para que sea invocado (es el nombre de la clase, no incluye el método).
  • Luego de configurar la lambda con 128mb de memoria, 5 segundos de timeout y subir el zip, el resultado es el siguiente:

El resultado es excelente 😊 !!!

El código queda disponible en GitHub 😎 y los enlaces que me ayudaron fueron:







Codersrank - TOP DEVELOPERS IN JAVA

Hoy solo quiero dejar las capturas de pantalla de Codersrank en las cuales tengo el agrado de aparecer 😊, no se por cuanto tiempo estaré en el ranking, así que las guardo de recuerdo 😊.


  • Filtro: Chile + Santiago + Java = 1


  • Filtro: Chile + Java = 6





















llegar al 1 en Chile será un poco dificil...



Buenas Practicas creando AWS Lambdas con Java

En esta entrada recopilaré buenas practicas que he aprendido al momento de crear Lambdas en AWS con Java y Maven. Algunas son mis recomendaciones y otras son desde AWS (sobre todo como evitar el cold start). Comenzamos:

  • Todo lo que pueda ser estático tiene que ser estático, de esta manera serán reutilizados en los siguientes request que se realicen en la misma instancia del Lambda.
  • inicializar todo lo que puedan en el handler del lambda en un bloque estático: 
    public class LambdaHandler implements RequestStreamHandler {
     
      static {
         // acá
      }
    
      @Override
      public void handleRequest(InputStream is, OutputStream os, Context ctx) throws IOException {
    
      }
    }
    Hay que hacer esto porque cuando se instancia la clase hay más poder de computo que el configurado como límite para la lambda, si hay conexiones a DynamoDB hay que instanciar la conexión y hacer una Query (cualquiera) lo importante es que se instancien todas las dependencias. Lo mismo ocurre para conexiones con RDS o cualquier otro recurso.
  • Utilizar CloudFormation o Serverless para automatizar nuestra solución.
  • No ocupar el plugin shade de maven para crear el Jar (no me gusta 😊), prefiero dejar dentro del jar una carpeta lib con todas las librerías la cual quedará disponible cuando se ejecute en el entorno de AWS:
    <plugin>
       <groupId>org.apache.maven.plugins</groupId>
       <artifactId>maven-dependency-plugin</artifactId>
       <version>3.1.1</version>
       <executions>
          <execution>
             <id>copy-dependencies</id>
             <phase>prepare-package</phase>
             <goals>
                <goal>copy-dependencies</goal>
             </goals>
             <configuration>
                <outputDirectory>${project.build.directory}/classes/lib</outputDirectory>
                <overWriteReleases>false</overWriteReleases>
                <overWriteSnapshots>false</overWriteSnapshots>
                <overWriteIfNewer>true</overWriteIfNewer>
                <includeScope>runtime</includeScope>
             </configuration>
          </execution>
       </executions>
    </plugin>
  • Utilizar el logger de log4j por su versatilidad:
    <dependencies>
       <dependency>
          <groupId>com.amazonaws</groupId>
          <artifactId>aws-lambda-java-log4j2</artifactId>
          <version>1.1.0</version>
       </dependency>
       <dependency>
          <groupId>org.apache.logging.log4j</groupId>
          <artifactId>log4j-core</artifactId>
          <version>2.12.1</version>
       </dependency>
       <dependency>
          <groupId>org.apache.logging.log4j</groupId>
          <artifactId>log4j-api</artifactId>
          <version>2.12.1</version>
       </dependency>
    </dependencies>
    <?xml version="1.0" encoding="UTF-8"?>
    <Configuration packages="com.amazonaws.services.lambda.runtime.log4j2">
       <Appenders>
          <Lambda name="Lambda">
             <PatternLayout>
                <pattern>%d{yyyy-MM-dd HH:mm:ss} %X{AWSRequestId} %-5p %c{1}:%L - %m%n</pattern>
             </PatternLayout>
          </Lambda>
       </Appenders>
       <Loggers>
          <Root level="info">
             <AppenderRef ref="Lambda" />
          </Root>
       </Loggers>
    </Configuration>
    private static final Logger LOGGER = LogManager.getLogger(LambdaHandler.class);
  • Usar sam para ejecuciones locales del lambda:
    sam local invoke NombreFuncion -t src/main/aws/template.yml
  • Para testing usar testcontainers con localstack, tendremos los servicios de AWS en nuestro ambiente de pruebas (es genial 😍):
       <dependency>
          <groupId>org.testcontainers</groupId>
          <artifactId>testcontainers</artifactId>
          <version>${test.containers}</version>
          <scope>test</scope>
       </dependency>
       <dependency>
          <groupId>org.testcontainers</groupId>
          <artifactId>localstack</artifactId>
          <version>${test.containers}</version>
          <scope>test</scope>
       </dependency>
  • No usar frameworks somo spring, jersey o similares, no es necesario (salvo que quieran utilizar graalvm para compilarlos), las lambdas deberían ser simples (muy personal, nada en contra de los frameworks)
El código de ejemplo está en Github 😇