Background Image
NUBE

Pruebas unitarias de las transacciones de Apache TinkerPop: De TinkerGraph a Amazon Neptune

Asset - Ken Hu
Ken Hu
Senior Software Developer

June 4, 2024 | 7 Minuto(s) de lectura

Un post anterior (Pruebas automatizadas de acceso a datos de Amazon Neptune con Apache TinkerPop Gremlin) se describen las ventajas de realizar pruebas unitarias con Apache TinkerPop Gremlin y muestra cómo puedes añadir las pruebas a tu proceso CI/CD. Cubre algunos de los puntos problemáticos a los que se enfrentan los usuarios que intentan utilizar Amazon Neptune como objetivo de sus consultas de pruebas unitarias, que incluyen la necesidad de estar conectado a Internet y la necesidad de conectarse a la VPC (actualmente sólo se puede acceder a Neptune desde la VPC en la que está alojado). Además, puede ahorrar dinero realizando pruebas unitarias con un servidor local Apache TinkerPop Gremlin. Ese post también sugirió y demostró cómo se puede utilizar TinkerGraph alojado dentro de un servidor Gremlin para hacer frente a estos problemas de pruebas.

En este post, me baso en el enfoque del post anterior y muestro cómo se puede utilizar TinkerGraph para probar la unidad de sus cargas de trabajo transaccionales. Además, muestro cómo utilizar TinkerGraph en modo embebido. El modo incrustado requiere el uso de Java, pero simplifica considerablemente el entorno de pruebas, ya que no hay necesidad de ejecutar el servidor como un proceso separado.

Los dos diagramas siguientes muestran las diferencias arquitectónicas entre ejecutar una consulta contra Neptune y ejecutar una consulta contra un gráfico incrustado.

asset - AWS Blog image 1
Asset - AWS Blog image 2

Los ejemplos en este post asumen que estás trabajando con Java y por lo tanto tienes acceso a la versión embebida de TinkerGraph. Véase Pruebas automatizadas de acceso a datos de Amazon Neptune con Apache TinkerPop Gremlin para obtener más información sobre cómo utilizar la versión remota de TinkerGraph dentro de un contenedor Docker. Ten en cuenta que las transacciones incrustadas tienen más capacidades que las transacciones remotas, por lo que sólo debes probar las características que existen para las transacciones remotas (que es lo que se utiliza cuando se conecta a Neptune).

Visión general de las transacciones en TinkerGraph y Neptune

Históricamente, una desventaja de usar TinkerGraph para pruebas era que no soportaba transacciones. Las transacciones son una parte importante para asegurar la corrección cuando se modifica la base de datos subyacente, y este tipo de comportamiento no podía ser probado con TinkerGraph. Sin embargo, con la introducción del TinkerGraph transaccionalTinkerTransactionGraph, en la versión 3.7.0, esto ha cambiado y TinkerGraph es una solución adecuada en la mayoría de los casos.

Hay algunas diferencias importantes entre la semántica de transacciones de TinkerTransactionGraph y Neptune, por lo que hay algunos escenarios que no deberías probar con TinkerTransactionGraph. Estos escenarios deberían ser cubiertos por el conjunto completo de pruebas, que debería ejecutarse contra Neptune.

En primer lugar, TinkerTransactionGraph sólo proporciona garantías contra lecturas sucias, por lo que tiene un nivel de aislamiento de lectura comprometido. Neptune, por otro lado, puede proporcionar garantías sólidas contra lecturas sucias, lecturas fantasma y lecturas no repetibles.. Esto significa que tus pruebas unitarias deben ser escritas con la expectativa de que sólo las lecturas sucias no pueden ocurrir.

En segundo lugar, TinkerTransactionGraph emplea una forma de bloqueo optimista, de modo que si dos transacciones intentan modificar el mismo elemento, la segunda lanzará una excepción. Neptune usa un bloqueo pesimista (wait-lock approach) y permite un tiempo máximo de espera para adquirir un recurso. Puede que necesite tener en cuenta este comportamiento de bloqueo optimista capturando TransactionExceptions y reintentando.

Además, existen diferencias en el soporte de Gremlin entre TinkerGraph y Neptune. Para obtener más información, consulte Pruebas automatizadas de acceso a datos de Amazon Neptune con Apache TinkerPop Gremlin y Cumplimiento de los estándares de Gremlin en Amazon Neptune.

Ejemplos de pruebas unitarias de TinkerGraph

Veamos un ejemplo de un sencillo servicio de aeropuerto.

Requisitos previos

Para ejecutar estos ejemplos contra el TinkerGraph transaccional directamente, debes incluir el artefacto tinkergraph-gremlin en tu compilación. Por ejemplo, si utilizas Maven, debes incluir la siguiente dependencia en tu archivo pom:

<dependency>
	<groupId>org.apache.tinkerpop</groupId>
	<artifactId>tinkergraph-gremlin</artifactId>
	<version>3.7.0</version>
	<scope>test</scope>
</dependency>

La versión 3.7.0 se utiliza aquí como ejemplo ya que es la primera versión en la que TinkerGraph transaccional está disponible. La versión que debe utilizar depende de la versión de su motor Neptune. Vea esta tabla para más información.

Alternativamente, para ejecutar estos ejemplos contra Neptune, necesitas acceso a un cluster Neptune.

Ejemplo de servicio de aeropuerto

El siguiente código muestra cómo sería la interfaz de un servicio de este tipo:

public interface AirportService {
    public boolean addAirport(Map<String, Object> airportData);
    public boolean addRoute(String fromAirport, String toAirport, int distance);
    public Map<String, Object> getAirportData(String airportCode);
    public int getRouteDistance(String fromAirportCode, String toAirportCode);
    public boolean hasRoute(String fromAirport, String toAirport);
    public boolean removeAirport(String airportCode);
    public boolean removeRoute(String fromAirportCode, String toAirportCode);
}

Veamos ahora cómo sería la implementación de la función addRoute . El siguiente código muestra la implementación de addRoute y algunos campos de la clase:

public class NorthAmericanAirportService implements AirportService {
    private GraphTraversalSource g;

    public NorthAmericanAirportService(GraphTraversalSource g) {
        this.g = g;
    }
    
    /**
     * Adds a route between two airports.
     *
     * @param fromAirportCode   The airport code of airport where the route begins.
     * @param toAirportCode     The airport code of airport where the route ends.
     * @param distance          The distance between the two airports.
     * @return                  True if the route was added; false otherwise.
     */
    public boolean addRoute(String fromAirportCode, String toAirportCode, int distance) {
        Transaction tx = g.tx();
        GraphTraversalSource gtx = tx.begin(); // Explicitly starting the transaction.

        // This try-catch-rollback approach is recommended with TinkerPop transactions.
        try {
            final Vertex fromV = gtx.V().has("code", fromAirportCode).next();
            final Vertex toV = gtx.V().has("code", toAirportCode).next();
            gtx.addE("route").from(fromV).to(toV).next();
            tx.commit();

            return true;
        } catch (Exception e) {
            tx.rollback();
            return false;
        }
    }

Podríamos querer tener dos pruebas unitarias para este método: una para un aeropuerto inexistente, que debería fallar, y otra para aeropuertos válidos, que debería pasar. Observa cómo la variable de instancia g se utiliza para cambiar entre diferentes proveedores de gráficos:

public class AirportServiceTest {

    // In this example, "STAGING_ENV" is used to determine whether to test against TinkerGraph or Amazon Neptune.
    private static boolean STAGING_ENV = (null != System.getProperty("STAGING_ENV"));
    private static Cluster cluster;

    private GraphTraversalSource g;

    @BeforeClass
    public static void setUpServerCluster() {
        if (STAGING_ENV) {
            cluster = Cluster.build().addContactPoint("your-neptune-cluster").enableSsl(true).create();
        }
    }
    
    @Before
    public void setUpGraph() {
        if (STAGING_ENV) { // In this example, STAGING_ENV is a system property used to determine which database to use.
            g = traversal().withRemote(DriverRemoteConnection.using(cluster));
            g.V().drop().iterate();
            // Currently, Neptune only accepts URLs reachable within its VPC as an input to the io step.
            g.io("your-presigned-s3-url").read().iterate();
        } else {
            // Create a default, empty instance of the transactional TinkerGraph.
            g = TinkerTransactionGraph.open().traversal();

            g.io("your-local-data.xml").read().iterate(); // This is how you insert your GraphML test data.
            g.tx().commit(); // By default, transactions are automatically opened, so commit the changes from io().
        }
    }

    @Test
    public void testAddRouteWithIncorrectAirportCode() {
        final NorthAmericanAirportService service = new NorthAmericanAirportService(g);

        // Add route with airport code that doesn't exist.
        final boolean wasAdded = service.addRoute("INCORRECT", "AUS", 500);

        assertFalse(wasAdded);
        // Check to see if any routes exist between those two airports.
        assertEquals(0L,
                     g.E().where(inV().has("code", "INCORRECT"))
                          .where(outV().has("code", "AUS")).count().next().longValue());
    }
	@Test
    public void testAddRouteWithValidAirportCodes() {
        final NorthAmericanAirportService service = new NorthAmericanAirportService(g);

        final boolean wasAdded = service.addRoute("PBI", "ORD", 500);

        assertTrue(wasAdded);
        assertEquals(1L,
                     g.E().where(inV().has("code", "PBI"))
                          .where(outV().has("code", "ORD")).count().next().longValue());
    }

Exploremos cómo se vería esto en un escenario ligeramente más complicado en el que se desea detener temporalmente las rutas a un aeropuerto específico. El siguiente código ilustra la implementación de una función que detiene el tráfico entrante:

/**
     * Removes incoming routes to an airport.
     *
     * @param airportCode   The airport code of the airport to remove incoming routes.
     */
    public void stopIncomingTraffic(String airportCode) {
        Transaction tx = g.tx();

        try {
            g.V().has("code", airportCode).inE().drop().iterate();
            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        }
    }

El código siguiente ilustra una prueba unitaria para stopIncomingTraffic():

@Test
    public void testStoppingTrafficToAus() {
        final NorthAmericanAirportService service = new NorthAmericanAirportService(g);
        final String airport = "AUS";

        service.stopIncomingTraffic(airport);

        // Check that there are no outgoing routes into that airport.
        assertEquals(0, g.V().out().has("code", airport).toList().size());
    }

Limpieza

Si has seguido los ejemplos usando un TinkerGraph embebido, entonces se limpiará automáticamente cuando terminen las pruebas.

Si has seguido los ejemplos usando un cluster Neptune, entonces puedes evitar incurrir en cargos borrando el cluster Neptune.

Conclusión

Las pruebas unitarias son un aspecto importante de CI/CD. Por razones de coste y flexibilidad, es posible que desee ejecutar sus pruebas unitarias contra TinkerGraph. El TinkerGraph transaccional, TinkerTransactionGraph, introducido en 3.7.0, es un buen candidato cuando se necesita probar transacciones. Para la parte de puesta en escena de su canalización de CI/CD, que ejecuta pruebas menos frecuentes como el rendimiento o la integración, puede considerar ejecutar contra una instancia de prueba de Amazon Neptune Serverlessque es una forma rentable de ejecutar cargas de prueba puntuales y tendrá la misma semántica de transacciones que su base de datos Neptune de producción.

"Este post es una colaboración conjunta entre Improving y AWS y se está publicando de forma cruzada tanto en el blog de Improving y en el Blog de bases de datos de AWS."

Nube

Reflexiones más recientes

Explore las entradas de nuestro blog e inspírese con los líderes de opinión de todas nuestras empresas.
Thumbnail - Creating Inclusive Digital Experiences: A Guide to Accessibility - people on computer / laptop smiling UX
ENTREGA DEL PRODUCTO

Creación de experiencias digitales inclusivas: Guía de accesibilidad

Cómo crear experiencias digitales inclusivas garantizando la accesibilidad en el diseño UX.