Background Image
NUAGE

Test unitaire des transactions Apache TinkerPop : De TinkerGraph à Amazon Neptune

Asset - Ken Hu
Ken Hu
Senior Software Developer

June 4, 2024 | 7 Lecture minute

Un article précédent (Tests automatisés de l'accès aux données d'Amazon Neptune avec Apache TinkerPop Gremlin) décrit les avantages des tests unitaires de votre système Apache TinkerPop Gremlin et montre comment vous pouvez ajouter les tests à votre pipeline CI/CD. Il couvre certains des problèmes auxquels les utilisateurs sont confrontés s'ils tentent d'utiliser Amazon Neptune comme cible de leurs requêtes de tests unitaires, notamment la nécessité d'être connecté à Internet et de se connecter au VPC (actuellement, on ne peut accéder à Neptune que depuis le VPC où il est hébergé). En outre, vous pouvez économiser de l'argent en effectuant des tests unitaires avec un serveur Apache TinkerPop Gremlin local. Cet article a également suggéré et démontré comment utiliser TinkerGraph hébergé dans un serveur Gremlin pour résoudre ces problèmes de test.

Dans ce billet, je m'appuie sur l'approche du billet précédent et je montre comment vous pouvez utiliser TinkerGraph pour tester unitairement vos charges de travail transactionnelles. En outre, je montre comment utiliser TinkerGraph en mode intégré. Le mode intégré nécessite l'utilisation de Java, mais il simplifie considérablement l'environnement de test car il n'est pas nécessaire d'exécuter le serveur en tant que processus distinct.

Les deux diagrammes suivants montrent les différences architecturales entre l'exécution d'une requête sur Neptune et l'exécution d'une requête sur un graphe intégré.

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

Les exemples présentés dans ce billet supposent que vous travaillez avec Java et que vous avez donc accès à la version intégrée de TinkerGraph. Voir Tests automatisés de l'accès aux données d'Amazon Neptune avec Apache TinkerPop Gremlin pour plus d'informations sur l'utilisation de la version distante de TinkerGraph dans un conteneur Docker. Notez que les transactions intégrées ont plus de capacités que les transactions à distance, vous ne devez donc tester que les fonctionnalités qui existent pour les transactions à distance (ce qui est utilisé lors de la connexion à Neptune).

Aperçu des transactions dans TinkerGraph et Neptune

Historiquement, l'un des inconvénients de l'utilisation de TinkerGraph pour les tests était qu'il ne prenait pas en charge les transactions. Les transactions sont un élément important pour garantir l'exactitude des modifications apportées à la base de données sous-jacente, et ce type de comportement ne pouvait pas être testé avec TinkerGraph. Cependant, avec l l'introduction du TinkerGraph transactionneltransactionnel, TinkerTransactionGraph, dans la version 3.7.0, cela a changé et TinkerGraph est une solution appropriée dans la plupart des cas.

Il existe des différences importantes entre la sémantique des transactions de TinkerTransactionGraph et celle de Neptune, et il y a donc des scénarios que vous ne devriez pas tester avec TinkerTransactionGraph. Ces scénarios doivent être couverts par votre suite de tests complète, qui doit être exécutée avec Neptune.

Tout d'abord, TinkerTransactionGraph ne fournit des garanties que contre les lectures sales, il a donc un niveau d'isolation de lecture engagée. Neptune, quant à lui, peut fournir des garanties solides contre les lectures sales, les lectures fantômes et les lectures non répétables. Cela signifie que vos tests unitaires devraient être écrits en pensant que seules les lectures sales ne peuvent pas se produire.

Deuxièmement, TinkerTransactionGraph utilise une forme de verrouillage optimiste, de sorte que si deux transactions tentent de modifier le même élément, la seconde transaction lèvera une exception. Neptune utilise un verrouillage pessimiste (approche wait-lock) et autorise un temps d'attente maximum pour l'acquisition d'une ressource. Il se peut que vous deviez tenir compte de ce comportement de verrouillage optimiste en attrapant des exceptions de transaction et en réessayant.

En outre, il existe des différences dans la prise en charge de Gremlin entre TinkerGraph et Neptune. Pour plus d'informations, voir Tests automatisés de l'accès aux données d'Amazon Neptune avec Apache TinkerPop Gremlin et Conformité aux normes Gremlin dans Amazon Neptune.

Exemples de tests unitaires TinkerGraph

Prenons l'exemple d'un simple service d'aéroport.

Conditions préalables

Pour exécuter ces exemples contre le TinkerGraph transactionnel directement, vous devez inclure l'artefact tinkergraph-gremlin dans votre construction. Par exemple, si vous utilisez Maven, vous devez inclure la dépendance suivante dans votre fichier pom :

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

La version 3.7.0 est utilisée ici à titre d'exemple car il s'agit de la première version de TinkerGraph transactionnel. La version que vous devez utiliser dépend de la version de votre moteur Neptune. Voir ce tableau pour plus d'informations.

Pour exécuter ces exemples avec Neptune, vous devez également avoir accès à un cluster Neptune.

Exemple de service d'aéroport

Le code suivant montre à quoi pourrait ressembler l'interface d'un tel service :

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);
}

Voyons maintenant à quoi pourrait ressembler l'implémentation de la fonction addRoute . Le code suivant montre l'implémentation de la méthode addRoute et quelques champs de la classe :

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;
        }
    }

Nous pourrions vouloir avoir deux tests unitaires pour cette méthode : un pour un aéroport inexistant, qui devrait échouer, et un pour les aéroports valides, qui devrait réussir. Remarquez que la variable d'instance g est utilisée pour passer d'un fournisseur de graphe à l'autre :

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());
    }

Voyons à quoi cela pourrait ressembler dans un scénario un peu plus compliqué, où l'on souhaite interrompre temporairement les itinéraires vers un aéroport spécifique. Le code suivant illustre la mise en œuvre d'une fonction qui arrête le trafic entrant :

/**
     * 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();
        }
    }

Le code suivant illustre un test unitaire pour 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());
    }

Nettoyage

Si vous avez suivi les exemples en utilisant un TinkerGraph intégré, celui-ci sera automatiquement nettoyé à la fin des tests.

Si vous avez suivi les exemples en utilisant un cluster Neptune, vous pouvez éviter les frais en supprimant le cluster Neptune.

Conclusion

Les tests unitaires sont un aspect important de CI/CD. Pour des raisons de coût et de flexibilité, vous pouvez exécuter vos tests unitaires sur TinkerGraph. Le TinkerGraph transactionnel, TinkerTransactionGraph, introduit dans la version 3.7.0, est un bon candidat pour tester les transactions. Pour la partie staging de votre pipeline CI/CD, qui exécute des tests moins fréquents comme la performance ou l'intégration, vous pouvez envisager d'exécuter contre une instance de test de Amazon Neptune Serverlessqui est un moyen rentable d'exécuter des charges de test irrégulières et qui aura la même sémantique de transaction que votre base de données Neptune de production.

"Ce billet est le fruit d'une collaboration entre Improving et AWS et fait l'objet d'une publication croisée sur le blog d'Improving et sur le AWS Database Blog."

Nuage

Dernières réflexions

Explorez nos articles de blog et laissez-vous inspirer par les leaders d'opinion de nos entreprises.
Asset - Image 1 Beyond Cost Savings: How Mexico Talent Drives Innovation Nearshore
LEADERSHIP

La proposition de valeur n'est pas réservée au marketing

Découvrez comment des propositions de valeur bien conçues améliorent l'expérience des clients, rationalisent les opérations et favorisent l'innovation.