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 😀