API Async Datastore

L'API Async Datastore permet d'effectuer des appels parallèles non bloquants vers le datastore et de récupérer les résultats de ces appels à un stade ultérieur du traitement de la requête. Ce document décrit plusieurs aspects de l'API Async Datastore.

Utilisation de l'API Async Datastore

L'API Async Datastore permet d'appeler le datastore à l'aide des méthodes de l'interface AsyncDatastoreService Vous obtenez cet objet en appelant la méthode de classe getAsyncDatastoreService() de la classe DatastoreServiceFactory.

import com.google.appengine.api.datastore.AsyncDatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;

// ...
AsyncDatastoreService datastore = DatastoreServiceFactory.getAsyncDatastoreService();

AsyncDatastoreService est compatible avec les mêmes opérations que DatastoreService, excepté que la plupart des méthodes renvoient immédiatement un objet Future dont vous pouvez bloquer le résultat ultérieurement. Par exemple, DatastoreService.get() renvoie un objet Entity, tandis que AsyncDatastoreService.get() renvoie un objet Future<Entity>.

// ...

Key key = KeyFactory.createKey("Employee", "Max");
// Async call returns immediately
Future<Entity> entityFuture = datastore.get(key);

// Do other stuff while the get operation runs in the background...

// Blocks if the get operation has not finished, otherwise returns instantly
Entity entity = entityFuture.get();

Remarque : Les exceptions ne sont pas levées tant que la méthode get() n'est pas appelée. L'appel de cette méthode vous permet de vérifier que l'opération asynchrone a réussi.

Si vous avez une méthode AsyncDatastoreService et que vous devez exécuter une opération de manière synchrone, appelez la méthode AsyncDatastoreService appropriée, puis bloquez immédiatement le résultat :

// ...

Entity entity = new Employee("Employee", "Alfred");
// ... populate entity properties

// Make a sync call via the async interface
Key key = datastore.put(key).get();

Utiliser des transactions asynchrones

Les appels vers l'API Async Datastore peuvent être effectués dans des transactions, de la même façon que les appels synchrones. Voici une fonction qui ajuste le salaire d'un Employee et écrit une entité supplémentaire SalaryAdjustment dans le même groupe d'entités qu'Employee, le tout dans une même transaction.

void giveRaise(AsyncDatastoreService datastore, Key employeeKey, long raiseAmount)
        throws Exception {
    Future<Transaction> txn = datastore.beginTransaction();

    // Async call to lookup the Employee entity
    Future<Entity> employeeEntityFuture = datastore.get(employeeKey);

    // Create and put a SalaryAdjustment entity in parallel with the lookup
    Entity adjustmentEntity = new Entity("SalaryAdjustment", employeeKey);
    adjustmentEntity.setProperty("adjustment", raiseAmount);
    adjustmentEntity.setProperty("adjustmentDate", new Date());
    datastore.put(adjustmentEntity);

    // Fetch the result of our lookup to make the salary adjustment
    Entity employeeEntity = employeeEntityFuture.get();
    long salary = (Long) employeeEntity.getProperty("salary");
    employeeEntity.setProperty("salary", salary + raiseAmount);

    // Re-put the Employee entity with the adjusted salary.
    datastore.put(employeeEntity);
    txn.get().commit(); // could also call txn.get().commitAsync() here
}

Cet exemple illustre une différence essentielle entre les appels asynchrones avec et sans transactions. Lorsque vous n'utilisez pas de transaction, le seul moyen de vous assurer qu'un appel asynchrone individuel est terminé consiste à extraire la valeur de Future qui a été renvoyée lors de l'appel. Lorsque vous utilisez une transaction, l'appel de Transaction.commit() bloque le résultat de tous les appels asynchrones effectués depuis le début de la transaction avant sa validation.

Ainsi, dans notre exemple ci-dessus, même si notre appel asynchrone visant à insérer l'entité SalaryAdjustment peut toujours être en suspens lorsque nous appelons commit(), la validation ne se produira pas tant que l'insertion n'est pas terminée. De même, le fait d'appeler commitAsync() au lieu de commit() entraîne le blocage de l'appel get() sur l'objet Future renvoyé par commitAsync() jusqu'à ce que tous les appels asynchrones soient terminés.

Remarque : Les transactions sont associées à un thread spécifique et non à une instance spécifique de DatastoreService ou AsyncDatastoreService. En d'autres termes, si vous lancez une transaction avec un service DatastoreService et effectuez un appel asynchrone avec un service AsyncDatastoreService, cet appel asynchrone participe à la transaction. Ou, pour résumer, DatastoreService.getCurrentTransaction() et AsyncDatastoreService.getCurrentTransaction() renvoient toujours la même Transaction.

Utiliser des objets Future

Future Javadoc explique la plupart des choses que vous devez savoir pour utiliser avec succès un objet Future renvoyé par l'API Async Datastore. Toutefois, vous devez prendre en compte quelques éléments spécifiques à App Engine :

Requêtes asynchrones

Pour le moment, nous ne proposons pas d'API explicitement asynchrone pour les requêtes. Toutefois, lorsque vous appelez PreparedQuery.asIterable(), PreparedQuery.asIterator() ou PreparedQuery.asList(FetchOptions fetchOptions), DatastoreService et AsyncDatastoreService renvoient immédiatement et récupèrent de manière asynchrone les résultats. Cela permet à votre application d'exécuter des tâches en parallèle pendant la récupération des résultats de la requête.

// ...

Query q1 = new Query("Salesperson");
q1.setFilter(new FilterPredicate("dateOfHire", FilterOperator.LESS_THAN, oneMonthAgo));

// Returns instantly, query is executing in the background.
Iterable<Entity> recentHires = datastore.prepare(q1).asIterable();

Query q2 = new Query("Customer");
q2.setFilter(new FilterPredicate("lastContact", FilterOperator.GREATER_THAN, oneYearAgo));

// Also returns instantly, query is executing in the background.
Iterable<Entity> needsFollowup = datastore.prepare(q2).asIterable();

schedulePhoneCall(recentHires, needsFollowUp);

Utilisation des appels vers l'API Async Datastore

Les opérations exposées par l'interface DatastoreService sont synchrones. Par exemple, lorsque vous appelez DatastoreService.get(), votre code est bloqué jusqu'à la fin de l'appel du datastore. Si la seule opération requise de la part de votre application consiste à restituer le résultat de la méthode get() au format HTML, le blocage du code jusqu'à l'aboutissement de l'appel se révèle parfaitement judicieux. Cependant, si votre application a besoin du résultat de get() et du résultat d'une Query pour restituer la réponse, et si get() et la Query ne comportent aucune dépendance de données, le fait d'attendre que get() termine pour initier la Query est une perte de temps. Voici un exemple de code susceptible d'être amélioré par l'utilisation de l'API asynchrone :

DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
Key empKey = KeyFactory.createKey("Employee", "Max");

// Read employee data from the Datastore
Entity employee = datastore.get(empKey); // Blocking for no good reason!

// Fetch payment history
Query query = new Query("PaymentHistory");
PreparedQuery pq = datastore.prepare(query);
List<Entity> result = pq.asList(FetchOptions.Builder.withLimit(10));
renderHtml(employee, result);

Au lieu d'attendre la fin de l'opération get(), utilisez l'instance AsyncDatastoreService pour exécuter l'appel de manière asynchrone comme suit :

AsyncDatastoreService datastore = DatastoreServiceFactory.getAsyncDatastoreService();
Key empKey = KeyFactory.createKey("Employee", "Max");

// Read employee data from the Datastore
Future<Entity> employeeFuture = datastore.get(empKey); // Returns immediately!

// Fetch payment history for the employee
Query query = new Query("PaymentHistory", empKey);
PreparedQuery pq = datastore.prepare(query);

// Run the query while the employee is being fetched
List<Entity> result = pq.asList(FetchOptions.Builder.withLimit(10));
// Implicitly performs query asynchronously
Entity employee = employeeFuture.get(); // Blocking!
renderHtml(employee, result); 

Bien que les versions synchrones et asynchrones de ce code exploitent autant le processeur l'une que l'autre (puisqu'elles effectuent le même volume de travail), la version asynchrone se caractérise par un temps de latence inférieur, car elle permet aux deux opérations du datastore de s'exécuter en parallèle. En général, si vous devez exécuter plusieurs opérations de datastore ne dépendant pas de données, AsyncDatastoreService peut considérablement améliorer la latence.