Simulación de latencia con SQL/JDBC – Java, SQL y jOOQ.

Encontré un pequeño truco divertido para simular la latencia en sus entornos de desarrollo al probar ciertas consultas SQL. Posibles casos de uso, incluida la validación de que la latencia del backend no derribará su frontend, o que su UX aún es soportable, etc.

La solución es específica para PostgreSQL e Hibernate, aunque no es obligatoria. Además, utiliza una función almacenada para sortear las limitaciones de un VOID en PostgreSQL, pero esto también se puede eludir de otra manera, sin almacenar nada auxiliar en el catálogo.

Para eliminar la dependencia de Hibernate, simplemente puede usar el pg_sleep operar directamente usando un NULL predicado, ¡pero no lo intentes así!

select 1
from t_book
-- Don't do this!
where pg_sleep(1) is not null;

Esto dormirá 1 segundo Por fila (!). Como se puede ver en el plan de explicación. Limitémonos a 3 líneas para ver:

explain analyze
select 1
from t_book
where pg_sleep(1) is not null
limit 3;

Y el resultado es:

Limit  (cost=0.00..1.54 rows=3 width=4) (actual time=1002.142..3005.374 rows=3 loops=1)
   ->  Seq Scan on t_book  (cost=0.00..2.05 rows=4 width=4) (actual time=1002.140..3005.366 rows=3 loops=1)
         Filter: (pg_sleep('1'::double precision) IS NOT NULL)
 Planning Time: 2.036 ms
 Execution Time: 3005.401 ms

Como puede ver, toda la consulta tomó alrededor de 3 segundos para 3 filas. De hecho, eso también es lo que sucede en el ejemplo de Gunnar del tweet, excepto que estaba filtrando por ID, lo que "ayuda" a ocultar este efecto.

Podemos usar lo que Oracle llama almacenamiento en caché de subconsultas escalares, el hecho de que una subconsulta escalar puede considerarse razonablemente libre de efectos secundarios (a pesar del efecto secundario obvio de pg_sleep), lo que significa que algunos RDBMS almacenan en caché su resultado por ejecución de consulta.

explain analyze
select 1
from t_book
where (select pg_sleep(1)) is not null
limit 3;

El resultado es ahora:

Limit  (cost=0.01..1.54 rows=3 width=4) (actual time=1001.177..1001.178 rows=3 loops=1)
   InitPlan 1 (returns $0)
     ->  Result  (cost=0.00..0.01 rows=1 width=4) (actual time=1001.148..1001.148 rows=1 loops=1)
   ->  Result  (cost=0.00..2.04 rows=4 width=4) (actual time=1001.175..1001.176 rows=3 loops=1)
         One-Time Filter: ($0 IS NOT NULL)
         ->  Seq Scan on t_book  (cost=0.00..2.04 rows=4 width=0) (actual time=0.020..0.021 rows=3 loops=1)
 Planning Time: 0.094 ms
 Execution Time: 1001.223 ms

Ahora obtenemos el filtro único deseado. Sin embargo, no me gusta mucho este truco, porque depende de una optimización, que es opcional, y no una garantía formal. Esto puede ser suficiente para una simulación de latencia rápida, pero no cuente con este tipo de optimización en producción a la ligera.

Otro enfoque que parece garantizar este comportamiento sería utilizar un MATERIALIZED ETC:

explain
with s (x) as materialized (select pg_sleep(1))
select *
from t_book
where (select x from s) is not null;

Estoy usando una subconsulta escalar nuevamente, porque necesito acceder al CTE y no quiero ponerlo en el FROM cláusula, donde impactaría mi proyección.

Siendo el plan:

Result  (cost=0.03..2.07 rows=4 width=943) (actual time=1001.289..1001.292 rows=4 loops=1)
   One-Time Filter: ($1 IS NOT NULL)
   CTE s
     ->  Result  (...) (actual time=1001.262..1001.263 rows=1 loops=1)
   InitPlan 2 (returns $1)
     ->  CTE Scan on s  (cost=0.00..0.02 rows=1 width=4) (actual time=1001.267..1001.268 rows=1 loops=1)
   ->  Seq Scan on t_book  (cost=0.03..2.07 rows=4 width=943) (actual time=0.015..0.016 rows=4 loops=1)
 Planning Time: 0.049 ms
 Execution Time: 1001.308 ms

Nuevamente, contiene un solo filtro, que es lo que queremos aquí.

Uso de un enfoque basado en JDBC

Si su aplicación está basada en JDBC, no necesita simular la latencia ajustando la consulta. Puede simplemente representar JDBC de alguna manera. Veamos este pequeño programa:

try (Connection c1 = db.getConnection()) {

    // A Connection proxy that intercepts preparedStatement() calls
    Connection c2 = new DefaultConnection(c1) {
        @Override
        public PreparedStatement prepareStatement(String sql) 
        throws SQLException {
            sleep(1000L);
            return super.prepareStatement(sql);
        }
    };

    long time = System.nanoTime();
    String sql = "SELECT id FROM book";

    // This call now has a 1 second "latency"
    try (PreparedStatement s = c2.prepareStatement(sql);
        ResultSet rs = s.executeQuery()) {
        while (rs.next())
            System.out.println(rs.getInt(1));
    }

    System.out.println("Time taken: " + 
       (System.nanoTime() - time) / 1_000_000L + "ms");
}

O:

public static void sleep(long time) {
    try {
        Thread.sleep(time);
    }
    catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
}

Para simplificar, esto usa jOOQ's DefaultConnection que actúa como un proxy, delegando convenientemente todos los métodos a una conexión de delegado, permitiendo que solo se anulen métodos específicos. La salida del programa es:

1
2
3
4
Time taken: 1021ms

Esto simula la latencia en el prepareStatement() un evento. Obviamente, extraerá el proxy a una utilidad para no saturar su código. Incluso puede ordenar todos sus consultas de desarrollo y habilite la llamada de suspensión solo en función de una propiedad del sistema.

Alternativamente, también podríamos simularlo en el executeQuery() un evento:

try (Connection c = db.getConnection()) {
    long time = System.nanoTime();

    // A PreparedStatement proxy intercepting executeQuery() calls
    try (PreparedStatement s = new DefaultPreparedStatement(
        c.prepareStatement("SELECT id FROM t_book")
    ) {
        @Override
        public ResultSet executeQuery() throws SQLException {
            sleep(1000L);
            return super.executeQuery();
        };
    };

        // This call now has a 1 second "latency"
        ResultSet rs = s.executeQuery()) {
        while (rs.next())
            System.out.println(rs.getInt(1));
    }

    System.out.println("Time taken: " +
        (System.nanoTime() - time) / 1_000_000L + "ms");
}

Esto ahora usa la clase de conveniencia jOOQ DefaultPreparedStatement. Si lo necesita, simplemente agregue la dependencia jOOQ Open Source Edition (no hay nada específico de RDBMS en estas clases), con cualquier aplicación basada en JDBC, incluido Hibernate:

<dependency>
  <groupId>org.jooq</groupId>
  <artifactId>jooq</artifactId>
</dependency>

Alternativamente, simplemente copie las fuentes de las clases. DefaultConnection Donde DefaultPreparedStatement si no necesita toda la dependencia, o simplemente proxy de la API de JDBC usted mismo.

Una solución basada en jOOQ

Si ya está utilizando jOOQ (¡y debería hacerlo!), puede hacerlo aún más fácilmente implementando un ExecuteListener. Nuestro programa ahora se vería así:

try (Connection c = db.getConnection()) {
    DSLContext ctx = DSL.using(new DefaultConfiguration()
        .set(c)
        .set(new CallbackExecuteListener()
            .onExecuteStart(x -> sleep(1000L))
        )
    );

    long time = System.nanoTime();
    System.out.println(ctx.fetch("SELECT id FROM t_book"));
    System.out.println("Time taken: " +
        (System.nanoTime() - time) / 1_000_000L + "ms");
}

Siempre el mismo resultado:

+----+
|id  |
+----+
|1   |
|2   |
|3   |
|4   |
+----+
Time taken: 1025ms

La diferencia es que con una única devolución de llamada de intercepción, ahora podemos agregar esta suspensión a todos los tipos de declaraciones, incluidas las declaraciones preparadas, las declaraciones estáticas, las declaraciones que devuelven conjuntos de resultados o los recuentos de actualización del día, o ambos.

Si quieres conocer otros artículos parecidos a Simulación de latencia con SQL/JDBC – Java, SQL y jOOQ. puedes visitar la categoría Código.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Subir

Esta página web utiliza cookies para analizar de forma anónima y estadística el uso que haces de la web, mejorar los contenidos y tu experiencia de navegación. Para más información accede a la Política de Cookies . Ver mas