Documentação e Casos de Teste: Ferramentas Poderosas para Resolver Problemas de Desenvolvimento no OpenResty
API7.ai
October 23, 2022
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.
- Em quais fases não se pode usar as APIs relacionadas à memória compartilhada?
- 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?
- 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 akey
não existe ou expira, o valorvalue
é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.