API Async Datastore

A API Async Datastore permite realizar chamadas paralelas e sem bloqueio ao armazenamento de dados e recuperar os resultados dessas chamadas em um ponto posterior no gerenciamento da solicitação. Esta documentação descreve os seguintes aspectos da API Async Datastore:

Como trabalhar com o serviço de armazenamento de dados assíncrono

Com a Async Datastore API, você faz chamadas de armazenamento de dados usando métodos da interface AsyncDatastoreService. Esse objeto é recebido ao chamar o método de classe getAsyncDatastoreService() da classe DatastoreServiceFactory.

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

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

A AsyncDatastoreService aceita as mesmas operações que DatastoreService, exceto que a maioria dos métodos retorna imediatamente uma classe Future (em inglês). É possível bloquear o resultado dela posteriormente. Por exemplo, DatastoreService.get() retorna uma classe Entity, mas AsyncDatastoreService.get() retorna 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();

Observação: as exceções não são geradas até que você chame o método get(). A chamada a esse método permite confirmar o sucesso da operação assíncrona.

Se você tem um AsyncDatastoreService, mas precisa executar uma operação de maneira síncrona, invoque o método AsyncDatastoreService apropriado e, em seguida, bloqueie imediatamente o resultado:

// ...

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

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

Como trabalhar com transações assíncronas

Chamadas da API assíncrona do armazenamento de dados podem participar de transações da mesma forma que chamadas síncronas. Esta é uma função que ajusta o salário de um Employee e grava uma outra entidade SalaryAdjustment no mesmo grupo de entidades que o Employee, tudo dentro de uma única transação.

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
}

Nesse exemplo, ilustramos uma diferença importante entre as chamadas assíncronas sem transações e aquelas com transações. Quando você não está usando uma transação, a única maneira de confirmar se uma chamada assíncrona individual foi concluída é buscar o valor de Future retornado quando a chamada foi feita. Quando você está usando uma transação, chamar Transaction.commit() bloqueia o resultado de todas as chamadas assíncronas feitas desde que a transação foi iniciada antes de confirmá-la.

Então, no exemplo acima, mesmo que nossa chamada assíncrona para inserir a entidade SalaryAdjustment ainda possa estar pendente quando chamamos commit(), a execução ocorrerá somente depois que a inserção for concluída. Da mesma forma, se você optar por chamar commitAsync() em vez de commit(), invocar get() na classe Future retornada por commitAsync() gera bloqueio até que todas as chamadas assíncronas pendentes tenham sido concluídas.

Observação: transações são associadas a uma sequência específica e não a uma instância específica de DatastoreService ou AsyncDatastoreService. Isso significa que, se você iniciar uma transação com um DatastoreService e realizar uma chamada assíncrona com um AsyncDatastoreService, a chamada assíncrona participará da transação. Em resumo, DatastoreService.getCurrentTransaction() e AsyncDatastoreService.getCurrentTransaction() sempre retornam a mesma Transaction.

Como trabalhar com classes Future

O documento sobre a classe Future do Java (em inglês) explica grande parte do que você precisa saber para trabalhar com uma classe Future retornada pela API Async Datastore, mas é necessário observar alguns fatores específicos do App Engine:

Consultas assíncronas

Atualmente, não oferecemos uma API explicitamente assíncrona para consultas. No entanto, quando você invoca PreparedQuery.asIterable(), PreparedQuery.asIterator() ou PreparedQuery.asList(FetchOptions fetchOptions), tanto DatastoreService quanto AsyncDatastoreService imediatamente retornam resultados de busca prévia de forma assíncrona. Isso permite que o aplicativo realize trabalho paralelamente enquanto os resultados da consulta são buscados.

// ...

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

Quando usar chamadas assíncronas ao armazenamento de dados

As operações expostas pela interface DatastoreService são síncronas. Por exemplo, quando você chama DatastoreService.get(), seu código bloqueia até que a chamada ao armazenamento de dados seja concluída. Se a única coisa que seu aplicativo precisa fazer é exibir o resultado do get() em HTML, o bloqueio até que a chamada seja concluída é uma medida perfeitamente razoável a se tomar. No entanto, caso seu aplicativo precise do resultado get(), além do resultado de uma consulta Query para renderizar a resposta, e o get() e a consulta Query não tenham dependências de dados, será perda de tempo aguardar até que o get() seja concluído para iniciar a consulta Query. Veja um exemplo de código que pode ser melhorado com o uso da Async API:

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

Em vez de aguardar a conclusão de get(), use uma instância de AsyncDatastoreService para executar a chamada de forma assíncrona:

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

As versões síncrona e assíncrona desse código usam quantidades de CPU semelhantes. Afinal, as duas realizam a mesma quantidade de trabalho. Porém, por permitir que as duas operações do armazenamento de dados sejam executadas paralelamente, a versão assíncrona tem latência mais baixa. De modo geral, se você precisa realizar várias operações do armazenamento de dados que não têm dependências de dados, o AsyncDatastoreService pode melhorar a latência significativamente.