Como armazenar em cache do NDB

O NDB gerencia caches para você. Há dois níveis de armazenamento em cache: um cache em contexto e um gateway para o serviço de armazenamento em cache padrão do App Engine, Memcache. Ambos os caches são ativados por padrão para todos os tipos de entidade, mas podem ser configurados para atender a necessidades avançadas. Além disso, o NDB implementa um recurso denominado envio em lote automático, que tenta agrupar as operações para minimizar as idas e vindas do servidor.

Introdução

O armazenamento em cache ajuda a maioria dos tipos de aplicativos. O NDB armazena em cache automaticamente os dados que ele grava ou lê, a menos que um aplicativo o configure para não fazê-lo. A leitura do cache é mais rápida do que a leitura do Datastore.

Você pode alterar o comportamento de armazenamento em cache de muitas funções do NDB, passando argumentos de Opções de contexto. Por exemplo, você pode chamar key.get(use_cache=False, use_memcache=False) para ignorar o armazenamento em cache. É possível também alterar a política de armazenamento em cache padrão em um contexto do NDB, conforme descrito abaixo.

Cuidado: quando você usa o leitor do Datastore do console de administração para modificar o conteúdo do Datastore, os valores armazenados em cache não são atualizados. Assim, o cache pode ficar inconsistente. Para o cache no contexto, isso geralmente não é um problema. Para o Memcache, recomendamos o uso do console de administração para limpar o cache.

Objetos de contexto

O gerenciamento de cache usa uma classe denominada Context: cada linha de execução e cada transação são executadas em um novo contexto. Como cada solicitação HTTP de entrada inicia uma nova linha de execução, cada solicitação é executada com um novo contexto também. Para acessar o contexto atual, use a função ndb.get_context().

Cuidado: não faz sentido compartilhar objetos Context entre várias linhas de execução ou solicitações. Não salve o contexto como uma variável global. É melhor armazená-lo em uma variável local ou local de linha de execução.

Os objetos de contexto têm métodos para definir políticas de cache e manipular o cache.

O cache no contexto

O cache no contexto dura somente uma única linha de execução. Isso significa que cada solicitação HTTP recebida ganha um novo cache no contexto e é "visível" somente para o código que processa essa solicitação. Se o aplicativo gerar qualquer linha extra de execução ao processar uma solicitação, essas linhas de execução também terão um novo cache no contexto.

Como reside na memória, o cache no contexto é rápido. Quando uma função do NDB é gravada no Datastore, ela também é gravada no cache no contexto. Quando uma função do NDB é lida por uma entidade, ela verifica primeiro o cache no contexto. Se a entidade for encontrada lá, não ocorrerá interação do Datastore.

Quando uma função do NDB consulta o Datastore, a lista de resultados é recuperada do Datastore. No entanto, se algum resultado individual estiver no cache no contexto, ele será usado no lugar do valor recuperado da consulta do Datastore. Os resultados da consulta são gravados de volta no cache no contexto, se a política de cache diz isso. No entanto, nunca são gravados no Memcache.

Com a execução de consultas demoradas em tarefas em segundo plano, é possível que o cache no contexto consuma grandes quantidades de memória. Isso ocorre porque o cache mantém uma cópia de cada entidade que é recuperada ou armazenada no contexto atual. Para evitar exceções de memória em tarefas de longa duração, desative o cache ou defina uma política que exclua qualquer entidade que esteja consumindo mais memória.

Memcache

O Memcache é o serviço de armazenamento em cache padrão do App Engine, muito mais rápido que o Datastore, mas mais lento do que o cache no contexto (milissegundos em comparação com microssegundos).

Por padrão, um contexto não transacional armazena todas as entidades no Memcache. Todos os contextos de um aplicativo usam o mesmo servidor de Memcache e veem um conjunto consistente de valores armazenados em cache.

O Memcache não é compatível com transações. Assim, uma atualização destinada a ser aplicada ao Datastore e ao Memcache pode ser feita apenas para um dos dois. Para manter a consistência em tais casos (possivelmente às custas do desempenho), a entidade atualizada é excluída do Memcache e gravada no Datastore. Uma operação de leitura subsequente encontra a entidade ausente do Memcache, a recupera do Datastore e a atualiza no Memcache como um efeito colateral da leitura. Além disso, leituras do NDB feitas dentro de transações ignoram o Memcache.

Quando as entidades são gravadas em uma transação, o Memcache não é usado. Quando a transação é confirmada, seu contexto tenta excluir todas essas entidades do Memcache. Observe, no entanto, que algumas falhas podem impedir que essas exclusões ocorram.

Funções de política

O armazenamento em cache automático é conveniente para a maioria dos aplicativos, mas talvez seu aplicativo seja incomum e você queira desativar o armazenamento em cache automático para algumas entidades ou todas elas. É possível controlar o comportamento dos caches definindo funções de política. Há uma função de política para o cache no processo, definida com

context = ndb.get_context()
context.set_cache_policy(func)

e outra para Memcache, definida com

context = ndb.get_context()
context.set_memcache_policy(func)

Cada função de política aceita uma chave e retorna um resultado booleano. Se retornar False, a entidade identificada por essa chave não será salva no cache correspondente. Por exemplo, para ignorar o cache em processo de todas as entidades Account, é possível gravar

context = ndb.get_context()
context.set_cache_policy(lambda key: key.kind() != 'Account')

No entanto, continue lendo para ver uma maneira mais fácil de conseguir o mesmo resultado. Por conveniência, é possível passar True ou False em vez de uma função que sempre retorna o mesmo valor. As políticas padrão armazenam em cache todas as entidades.

Há também uma função de política do Datastore que rege quais entidades são gravadas no próprio Datastore:

context = ndb.get_context()
context.set_datastore_policy(func)

Isso funciona como as funções de política de cache no contexto e Memcache: se a função de política do Datastore retornar False para uma determinada chave, a entidade correspondente não será gravada no Datastore. Ela poderá ser gravada no cache no processo ou Memcache se as funções de política permitirem isso. Isso pode ser útil nos casos em que você tem dados semelhantes à entidade que gostaria de armazenar em cache, mas não precisa armazenar no Datastore. Assim como para as políticas de cache, é possível passar True ou False em vez de uma função que sempre retorna o mesmo valor.

O Memcache expira automaticamente itens quando está sob pressão de memória. É possível definir uma função de política de tempo limite do Memcache para determinar o tempo de vida máximo de uma entidade no cache:

context = ndb.get_context()
context.set_memcache_timeout_policy(func)

Essa função é chamada com um argumento key e deve retornar um número inteiro especificando o ciclo de vida máximo em segundos: 0 ou None significa indefinido, contanto que o servidor do Memcache tenha memória suficiente. Para facilitar, simplesmente passe uma constante de número inteiro em vez de uma função que sempre retorna o mesmo valor. Consulte a documentação do Memcache para mais informações sobre os tempos limite.

Observação: não há política de ciclo de vida exclusiva para o cache no contexto: o ciclo de vida do cache é igual ao do contexto dele, uma única solicitação HTTP recebida. No entanto, é possível limpar o cache no processo ao chamar
context = ndb.get_context()
context.clear_cache()

Um novo contexto começa com um cache vazio no processo.

As funções de política são muito flexíveis e, na prática, a maioria das políticas é simples. Por exemplo,

  • Não armazene em cache entidades pertencentes a uma classe de modelo específica.
  • Defina o tempo limite do Memcache para 30 segundos para entidades nesta classe de modelo.
  • Entidades nesta classe de modelo não precisam ser gravadas no Datastore.

Para poupar o trabalho de gravar e atualizar continuamente as funções de política triviais (ou pior, substituir as políticas de cada operação usando opções de contexto), as funções de política padrão recebem a classe de modelo da chave passada a elas e procuram na classe de modelo variáveis de classe específicas:

Variável de classe TipoDescrição
_use_cache bool Especifica se é necessário armazenar entidades no cache no processo e substitui a política de cache no processo padrão.
_use_memcache bool Especifica se é necessário armazenar entidades no Memcache e substitui a política do Memcache padrão.
_use_datastore bool Especifica se é necessário armazenar entidades no Datastore e substitui a política do Datastore padrão.
_memcache_timeout int Tempo de vida máximo para entidades no Memcache. Substitui a política de tempo limite do Memcache padrão.

Observação: este é um recurso da função de política padrão para cada política. Se você especificar sua própria função de política, mas também desejar usar a política padrão, chame explicitamente as funções de política padrão como métodos estáticos da classe Context:

  • default_cache_policy(key)
  • default_memcache_policy(key)
  • default_datastore_policy(key)
  • default_memcache_timeout_policy(key)