My favorites | Português | Sign in

Como usar ganchos no Google App Engine

Jens Scheffler
11 de fevereiro de 2009

Introdução

À medida que o código cresce, pode ficar difícil mantê-lo e modificá-lo. A implementação de recursos como a definição de perfil de uso do armazenamento de dados ou a adição de "dados de auditoria" (quem alterou essa linha?) em toda a base de códigos pode ser difícil. Como podemos ter certeza de que não deixamos passar nenhum local em nossos códigos fonte? Como podemos impedir que outro desenvolvedor cometa erros ao estender o código posteriormente?

Plano de fundo

Normalmente, desejamos implementar uma alteração no comportamento global de um aplicativo que não tem relação com a lógica comercial. A meta é fazer uma grande alteração com pouco risco e pouco código. Gostaríamos de aplicar uma preocupação transversal, como o registro de dados relacionados a desempenho, a um aplicativo inteiro. Gostaríamos de atingir essa meta globalmente, não importa qual tipo de api de nível superior (Modelos versus armazenamento de dados, Django versus webob) um desenvolvedor usa.

O módulo natural a ser usado é o api_proxy_map, um registro de nível inferior dos serviços RPC que delega chamadas de API de nível superior à implementação de serviço apropriada. Tradicionalmente, a forma de modificar o api_proxy_map é aplicar um monkeypatch a esse módulo e substituir o seu ponto de entrada principal apiproxy_stub_map.MakeSyncCall. Embora esse método funcione em alguns casos, não está livre de perigos. Para facilitar o teste da unidade, às vezes outros módulos escolhem injetar o método MakeSyncCall e armazenar uma referência local a ele. Dessa forma, um teste pode facilmente substituir uma simulação.

O módulo de cache de memória é um bom exemplo de como essa técnica é usada. Infelizmente, isso também significa que qualquer memcache-Client criado antes de aplicarmos o nosso patch ignorará todas as nossas modificações. Esse pode ser o caso, por exemplo, se o módulo do cache de memória tiver sido importado por um script antes da aplicação do nosso patch. O rastreamento e correção de todas essas referências é um processo complexo e não há garantia de que uma nova versão de um SDK (ou de qualquer outra biblioteca de ferramentas desenvolvida externamente usada para qualquer outra finalidade) não introduzirá novas dependências estáticas posteriormente.

Felizmente, há uma alternativa para monkeypatch. A versão 1.1.8 do SDK introduz um novo mecanismo ao api_proxy_map: o conceito de ganchos. Agora, qualquer desenvolvedor pode definir um método com a mesma assinatura que MakeSyncCall:

def hook(service, call, request, response):
  ...

e registrá-lo com a execução usando um dos seguintes métodos:

apiproxy_stub_map.apiproxy.GetPreCallHooks().Append('unique_name', hook, 'optional_api_identifier')
apiproxy_stub_map.apiproxy.GetPostCallHooks().Append('unique_name', hook, 'optional_api_identifier')
apiproxy_stub_map.apiproxy.GetPreCallHooks().Push('unique_name', hook, 'optional_api_identifier')
apiproxy_stub_map.apiproxy.GetPostCallHooks().Push('unique_name', hook, 'optional_api_identifier')

Há dois tipos diferentes de ganchos. Um PreCallHook é executado antes da realização de uma chamada de RPC; um PostCallHook é executado após a chamada de RPC. Também é possível especificar um identificador de api opcional para garantir que um gancho seja invocado apenas para um tipo específico de rpc (como armazenamento de dados ou cache de memória). É possível registrar mais de um gancho para o apiproxy: Append anexará um novo gancho ao final da lista de ganchos existentes; Push anexará o gancho no início.

Exemplo

Vamos analisar um exemplo concreto e implementar algumas definições de perfil do armazenamento de dados. A ideia é coletar automaticamente as estatísticas de cada tipo de modelo em nosso banco de dados. Gostaríamos de contadores separados para put/get/query/delete, mas sem ter que modificar as classes de modelo ou manipuladores que acessam os nossos dados. Por causa do desempenho, decidimos não gravar os nossos resultados no armazenamento de dados. Em vez disso, coletamos algumas estatísticas rápidas (não precisas ou de longa duração) no cache de memória, e colocamos informações mais detalhadas nas instruções de registro: cada operação do armazenamento de dados cria uma entrada de registro que pode ser recuperada e analisada no modo off-line.

A primeira etapa é criar um assistente que possa coletar dados no cache de memória e registrar informações mais detalhadas. Vale a pena observar que as contagens no cache de memória são apenas estimativas, já que não podemos bloquear esses dados e realizar incrementos de uma maneira transacional. Mas isso não tem problema, já que servem apenas como uma visão geral, comparadas aos dados mais detalhados nos registros. O db_log a seguir alcança esse objetivo.

def db_log(model, call, details=''):
  """Call this method whenever the database is invoked.
  
  Args:
    model: the model name (aka kind) that the operation is on
    call: the kind of operation (Get/Put/...)
    details: any text that should be added to the detailed log entry.
  """
  
  # First, let's update memcache
  if model:
    stats = memcache.get('DB_TMP_STATS')
    if stats is None: stats = {}
    key = '%s_%s' % (call, model)
    stats[key] = stats.get(key, 0) + 1
    memcache.set('DB_TMP_STATS', stats)
  
  # Next, let's log for some more detailed analysis
  logging.debug('DB_LOG: %s @ %s (%s)', call, model, details)

Observação: O cache de memória não tem recursos transacionais para aumentar as contagens, como incr() e decr(), mas eles funcionam em valores únicos e não em um dicionário de contadores.

Agora, vamos usar esse assistente e criar o nosso gancho. Usamos um PreCallHook que avalia os nossos dados antes da chamada de rpc ser feita (um PostCallHook também funcionaria bem nesse caso específico):

def patch_appengine():
  """Apply a hook to app engine that logs db statistics."""
  def model_name_from_key(key):
    return key.path().element_list()[0].type()
    
  def hook(service, call, request, response):
    assert service == 'datastore_v3'
    if call == 'Put':
      for entity in request.entity_list():
        db_log(model_name_from_key(entity.key()), call)
    elif call in ('Get', 'Delete'):
      for key in request.key_list():
        db_log(model_name_from_key(key), call)
    elif call == 'RunQuery':
      kind = datastore_index.CompositeIndexForQuery(request)[1]
      db_log(kind, call)
    else:
      db_log(None, call)
      
  apiproxy_stub_map.apiproxy.GetPreCallHooks().Append(
      'db_log', hook, 'datastore_v3')

Também podemos criar um script muito simples que exibe as nossas estatísticas em um navegador:

def main():
  """A very simple handler that will print the temporary statistics."""
  print 'Content-Type: text/plain'
  print ''
  print 'Mini stats'
  print '----------'
  stats = memcache.get('DB_TMP_STATS')
  if stats is None: stats = {}
  for name, count in sorted(stats.items(), key=operator.itemgetter(0)):
    print '%s : %s' % (name, count)
  print '----------'

  
if __name__ == "__main__":
  main()

Grande parte do esforço no código é usado para determinar o nome do modelo no qual estamos trabalhando. Se você estiver interessado em saber como obter esses dados, veja os módulos entity_pb.py e datastore_pb.py do SDK. Eles contêm os buffers de protocolo usados para RPCs do armazenamento de dados.

Até agora criamos

  • um assistente que compila as informações do armazenamento de dados no cache de memória (e registro)
  • um gancho que executa o assistente e
  • um manipulador simples que exibe os resultados.

Todos os três componentes podem ser armazenados em um único módulo (vamos chamá-lo de db_log.py). Podemos ativar a gravação rápida de códigos em nosso aplicativo adicionando as duas linhas de código a seguir a um aplicativo:

import db_log
db_log.patch_appengine()

Esse snippet deve ser colocado onde será executado com certeza, sem importar qual manipulador é invocado. Por exemplo, se houver um método principal comum (no caso de um aplicativo django), a adição desse snippet antes do método principal ser definido seria um bom local. Além disso, não se esqueça de adicionar db_log.py ao seu app.yaml para obter acesso às estatísticas rápidas e imprecisas do cache de memória. Você pode encontrar todo o código fonte deste artigo no Cookbook do Google App Engine.