Documentação e Casos de Teste: Ferramentas Poderosas para Resolver Problemas de Desenvolvimento no OpenResty

API7.ai

October 23, 2022

OpenResty (NGINX + Lua)

Após aprender os princípios e alguns conceitos essenciais do OpenResty, finalmente vamos começar a aprender a API.

Pela minha experiência pessoal, aprender a API do OpenResty é relativamente fácil, então não são necessários muitos artigos para apresentá-la. Você pode se perguntar: a API não é a parte mais comum e essencial? Por que não gastar muito tempo nela? Há duas considerações principais.

Primeiro, o OpenResty fornece uma documentação muito detalhada. Comparado com muitas outras linguagens de programação ou plataformas, o OpenResty não apenas fornece os parâmetros da API e as definições de valores de retorno, mas também exemplos de código completos e executáveis, mostrando claramente como a API lida com várias condições de contorno.

Seguir a definição da API com exemplos de código e advertências é um estilo consistente da documentação do OpenResty. Portanto, após ler a descrição da API, você pode imediatamente executar o código de exemplo em seu ambiente e modificar os parâmetros e a documentação para verificá-los e aprofundar seu entendimento.

Segundo, o OpenResty fornece casos de teste abrangentes. Como mencionei, a documentação do OpenResty mostra exemplos de código das APIs. No entanto, devido a limitações de espaço, o documento não apresenta relatórios de erros e processamento em várias situações anormais e o método de uso de múltiplas APIs.

Mas não se preocupe. Você pode encontrar a maioria desses conteúdos no conjunto de casos de teste.

Para desenvolvedores do OpenResty, os melhores materiais de aprendizado da API são a documentação oficial e os casos de teste, que são profissionais e amigáveis para os leitores.

Dê um peixe a um homem, e você o alimentará por um dia; ensine um homem a pescar, e você o alimentará por toda a vida. Vamos usar um exemplo real para experimentar como exercer o poder da documentação e do conjunto de casos de teste no desenvolvimento do OpenResty.

Tomemos a API get do shdict como exemplo

Baseado na área de memória compartilhada do NGINX, o dicionário compartilhado (shared dict) é um objeto de dicionário Lua, que pode acessar dados entre vários workers e armazenar dados como limitação de taxa, cache, etc. Há mais de 20 APIs relacionadas ao dicionário compartilhado - a API mais comumente usada e crucial no OpenResty.

Vamos tomar a operação mais simples, get, como exemplo; você pode clicar no link da documentação para comparação. O seguinte exemplo de código minimizado é adaptado da documentação oficial.

http {
      lua_shared_dict dogs 10m;
      server {
          location /demo {
              content_by_lua_block {
                  local dogs = ngx.shared.dogs
                  dogs:set("Jim", 8)
                  local v = dogs:get("Jim")
                  ngx.say(v)
              }
          }
      }
  }

Como uma nota rápida, antes de podermos usar o dicionário compartilhado no código Lua, precisamos adicionar um bloco de memória no nginx.conf com a diretiva lua_shared_dict, que é nomeado "dogs" e tem um tamanho de 10M. Após modificar o nginx.conf, você precisa reiniciar o processo e acessá-lo com um navegador ou comando curl para ver os resultados.

Isso não parece um pouco tedioso? Vamos modificá-lo de forma mais direta. Como você pode ver, usar o CLI resty dessa forma tem o mesmo efeito que incorporar o código no nginx.conf.

$ resty --shdict 'dogs 10m' -e 'local dogs = ngx.shared.dogs
 dogs:set("Jim", 8)
 local v = dogs:get("Jim")
 ngx.say(v)
 '

Agora você sabe como o nginx.conf e o código Lua funcionam juntos, e você executou com sucesso os métodos set e get do dicionário compartilhado. Geralmente, a maioria dos desenvolvedores para por aí. Há algumas coisas que valem a pena notar aqui.

  1. Em quais fases não se pode usar as APIs relacionadas à memória compartilhada?
  2. Vemos no código de exemplo que a função get tem apenas um valor de retorno. Então, quando haverá mais de um valor de retorno?
  3. Qual é o tipo de entrada da função get? Há um limite de comprimento?

Não subestime essas perguntas; elas podem nos ajudar a entender melhor o OpenResty, e vou levá-lo através delas individualmente.

Pergunta 1: Em quais fases não se pode usar as APIs relacionadas à memória compartilhada?

Vamos olhar para a primeira pergunta. A resposta é simples; a documentação tem uma seção context (ou seja, seção de contexto) dedicada que lista os ambientes nos quais a API pode ser usada.

context: set_by_lua*, rewrite_by_lua*, access_by_lua*, content_by_lua*, header_filter_by_lua*, body_filter_by_lua*, log_by_lua*, ngx.timer.*, balancer_by_lua*, ssl_certificate_by_lua*, ssl_session_fetch_by_lua*, ssl_session_store_by_lua*

Como você pode ver, as fases init e init_worker não estão incluídas, o que significa que a API get da memória compartilhada não pode ser usada nessas duas fases. Por favor, note que cada API de memória compartilhada pode ser usada em diferentes fases. Por exemplo, a API set pode ser usada na fase init.

Sempre leia a documentação ao usá-la. Claro, a documentação do OpenResty às vezes contém erros e omissões, então você precisa verificá-los com testes reais.

A seguir, vamos modificar o conjunto de testes para garantir que a fase init possa executar a API get do dicionário compartilhado.

Como podemos encontrar o conjunto de casos de teste relacionados à memória compartilhada? Os casos de teste do OpenResty estão todos no diretório /t e nomeados regularmente, ou seja, número-auto-incrementado-nome-da-função.t. Pesquise por shdict, e você encontrará 043-shdict.t, o conjunto de casos de teste da memória compartilhada, que contém cerca de 100 casos de teste, incluindo testes para várias circunstâncias normais e anormais.

Vamos tentar modificar o primeiro caso de teste.

Você pode substituir a fase content por uma fase init e remover o código supérfluo para ver se a interface get funciona. Você não precisa entender como o caso de teste é escrito, organizado e executado neste estágio. Você só precisa saber que ele está testando a interface get.

 === TEST 1: string key, int value
     --- http_config
         lua_shared_dict dogs 1m;
     --- config
         location = /test {
             init_by_lua '
                 local dogs = ngx.shared.dogs
                 local val = dogs:get("foo")
                 ngx.say(val)
             ';
         }
     --- request
     GET /test
     --- response_body
     32
     --- no_error_log
     [error]
     --- ONLY

Você deve ter notado que no final do caso de teste, adicionei a flag --ONLY, o que significa ignorar todos os outros casos de teste e executar apenas este, aumentando assim a velocidade de execução. Mais tarde, na seção de testes, explicarei especificamente as várias tags.

Após a modificação, podemos executar o caso de teste com o comando prove.

prove t/043-shdict.t

Então, você receberá um erro que corrobora os limites de fase descritos na documentação.

nginx: [emerg] "init_by_lua" directive is not allowed here

Pergunta 2: Quando a função get tem múltiplos valores de retorno?

Vamos olhar para a segunda pergunta, que pode ser resumida a partir da documentação oficial. A documentação começa com a descrição syntax desta interface.

value, flags = ngx.shared.DICT:get(key)

Em circunstâncias normais.

  • O primeiro parâmetro value retorna o valor correspondente à key no dicionário; no entanto, quando a key não existe ou expira, o valor value é nil.
  • O segundo parâmetro, flags, é um pouco mais complicado; se a interface set definir flags, ele as retorna. Caso contrário, não.

Se a chamada da API der errado, value retorna nil, e flags retorna uma mensagem de erro específica.

A partir das informações resumidas na documentação, podemos ver que local v = dogs:get("Jim") é escrito com apenas um parâmetro de recebimento. Tal escrita é incompleta porque cobre apenas o cenário de uso típico sem receber um segundo parâmetro ou realizar tratamento de exceções. Poderíamos modificá-lo da seguinte forma.

local data, err = dogs:get("Jim")
if data == nil and err then
    ngx.say("get not ok: ", err)
    return
end

Como com a primeira pergunta, podemos pesquisar o conjunto de casos de teste para confirmar nosso entendimento da documentação.

  === TEST 65: get nil key
     --- http_config
         lua_shared_dict dogs 1m;
     --- config
         location = /test {
             content_by_lua '
                 local dogs = ngx.shared.dogs
                 local ok, err = dogs:get(nil)
                 if not ok then
                     ngx.say("not ok: ", err)
                     return
                 end
                 ngx.say("ok")
             ';
         }
     --- request
     GET /test
     --- response_body
     not ok: nil key
     --- no_error_log
     [error]

Neste caso de teste, a interface get tem uma entrada nil, e a mensagem de erro retornada é nil key. Isso verifica que nossa análise da documentação está correta e fornece uma resposta parcial à terceira pergunta. Pelo menos, a entrada para get não pode ser nil.

Pergunta 3: Qual é o tipo de entrada da função get?

Quanto à terceira pergunta, que tipo de parâmetros de entrada para get ele pode ser? Vamos verificar a documentação primeiro, mas infelizmente, você descobrirá que a documentação não especifica quais são os tipos legais de chaves. O que devemos fazer?

Não se preocupe. Pelo menos sabemos que a key pode ser do tipo string e não pode ser nil. Você se lembra dos tipos de dados em Lua? Além de strings e nil, há números, arrays, tipos booleanos e funções. Os dois últimos são desnecessários como chaves, então só precisamos verificar os dois primeiros: números e arrays. Podemos começar pesquisando no arquivo de teste por casos em que números são usados como key.

=== TEST 4: number keys, string values

Com este caso de teste, você pode ver que números também podem ser usados como chaves, e internamente eles serão convertidos em strings. E arrays? Infelizmente, o caso de teste não cobre isso, então precisamos tentar nós mesmos.

$ resty --shdict 'dogs 10m' -e 'local dogs = ngx.shared.dogs
 dogs:get({})
 '

Sem surpresa, o seguinte erro foi relatado.

ERROR: (command line -e):2: bad argument #1 to 'get' (string expected, got table)

Em resumo, podemos concluir que os tipos de key aceitos pela API get são strings e números.

Então, há um limite de comprimento para a chave de entrada? Há um caso de teste correspondente aqui.

=== TEST 67: get a too-long key
     --- http_config
         lua_shared_dict dogs 1m;
     --- config
         location = /test {
             content_by_lua '
                 local dogs = ngx.shared.dogs
                 local ok, err = dogs:get(string.rep("a", 65536))
                 if not ok then
                     ngx.say("not ok: ", err)
                     return
                 end
                 ngx.say("ok")
             ';
         }
     --- request
     GET /test
     --- response_body
     not ok: key too long
     --- no_error_log
     [error]

Quando o comprimento da string é 65536, você será informado de que a chave é muito longa. Você pode tentar mudar o comprimento para 65535, embora apenas 1 byte a menos, mas não haverá mais erros. Isso significa que o comprimento máximo da chave é exatamente 65535.

Resumo

Finalmente, gostaria de lembrá-lo de que, na API do OpenResty, qualquer valor de retorno com uma mensagem de erro deve ter uma variável para recebê-lo e fazer o tratamento de erros, caso contrário, cometerá um erro. Por exemplo, se a conexão errada for colocada no pool de conexões, ou se a chamada da API falhar e continuar a lógica por trás dela, isso fará as pessoas reclamarem incessantemente.

Então, se você encontrar um problema ao escrever código OpenResty, qual é a sua maneira usual de resolvê-lo? É documentação, listas de discussão ou outros canais?

Sinta-se à vontade para compartilhar este artigo com seus colegas e amigos para que possamos nos comunicar e melhorar.