Documentación y Casos de Prueba: Herramientas Poderosas para Resolver Problemas de Desarrollo en OpenResty

API7.ai

October 23, 2022

OpenResty (NGINX + Lua)

Después de aprender los principios y algunos conceptos esenciales de OpenResty, finalmente vamos a comenzar a aprender la API.

Desde mi experiencia personal, aprender la API de OpenResty es relativamente fácil, por lo que no se necesitan muchos artículos para presentarla. Puedes preguntarte: ¿no es la API la parte más común y esencial? ¿Por qué no dedicar mucho tiempo a ella? Hay dos consideraciones principales.

Primero, OpenResty proporciona una documentación muy detallada. En comparación con muchos otros lenguajes de programación o plataformas, OpenResty no solo proporciona los parámetros de la API y las definiciones de valores de retorno, sino también ejemplos de código completos y ejecutables, mostrando claramente cómo la API maneja diversas condiciones límite.

Seguir la definición de la API con ejemplos de código y advertencias es un estilo consistente de la documentación de OpenResty. Por lo tanto, después de leer la descripción de la API, puedes ejecutar inmediatamente el código de ejemplo en tu entorno y modificar los parámetros y la documentación para verificarlos y profundizar tu comprensión.

Segundo, OpenResty proporciona casos de prueba completos. Como mencioné, la documentación de OpenResty muestra ejemplos de código de las API. Sin embargo, debido a limitaciones de espacio, el documento no presenta informes de errores y procesamiento en diversas situaciones anormales y el método de uso de múltiples API.

Pero no te preocupes. Puedes encontrar la mayoría de estos contenidos en el conjunto de casos de prueba.

Para los desarrolladores de OpenResty, los mejores materiales de aprendizaje de la API son la documentación oficial y los casos de prueba, que son profesionales y amigables para los lectores.

Dale un pescado a un hombre y lo alimentarás por un día; enséñale a pescar y lo alimentarás para toda la vida. Usemos un ejemplo real para experimentar cómo aprovechar el poder de la documentación y el conjunto de casos de prueba en el desarrollo de OpenResty.

Tomemos como ejemplo la API get de shdict

Basado en el área de memoria compartida de NGINX, el diccionario compartido (shared dict) es un objeto de diccionario Lua, que puede acceder a datos entre múltiples workers y almacenar datos como limitación de tasa, caché, etc. Hay más de 20 API relacionadas con el diccionario compartido, la API más utilizada y crucial en OpenResty.

Tomemos la operación más sencilla, get, como ejemplo; puedes hacer clic en el enlace de la documentación para comparar. El siguiente ejemplo de código minimizado está adaptado de la documentación 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 nota rápida, antes de poder usar el diccionario compartido en el código Lua, necesitamos agregar un bloque de memoria en nginx.conf con la directiva lua_shared_dict, que se llama "dogs" y tiene un tamaño de 10M. Después de modificar nginx.conf, necesitas reiniciar el proceso y acceder con un navegador o el comando curl para ver los resultados.

¿No parece esto un poco tedioso? Modifiquémoslo de manera más sencilla. Como puedes ver, usar la CLI de resty de esta manera tiene el mismo efecto que incrustar el código en nginx.conf.

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

Ahora sabes cómo funcionan juntos nginx.conf y el código Lua, y has ejecutado con éxito los métodos set y get del diccionario compartido. Generalmente, la mayoría de los desarrolladores se detienen allí. Hay algunas cosas que vale la pena notar aquí.

  1. ¿En qué etapas no se pueden usar las API relacionadas con la memoria compartida?
  2. Vemos en el código de ejemplo que la función get tiene solo un valor de retorno. ¿Cuándo habrá más de un valor de retorno?
  3. ¿Cuál es el tipo de entrada de la función get? ¿Hay un límite de longitud?

No subestimes estas preguntas; pueden ayudarnos a entender mejor OpenResty, y te guiaré a través de ellas individualmente.

Pregunta 1: ¿En qué etapas no se pueden usar las API relacionadas con la memoria compartida?

Veamos la primera pregunta. La respuesta es sencilla; la documentación tiene una sección dedicada context (es decir, sección de contexto) que enumera los entornos en los que se puede usar la API.

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 puedes ver, las fases init y init_worker no están incluidas, lo que significa que la API get de la memoria compartida no se puede usar en estas dos fases. Ten en cuenta que cada API de memoria compartida se puede usar en diferentes fases. Por ejemplo, la API set se puede usar en la fase init.

Siempre lee la documentación cuando la uses. Por supuesto, la documentación de OpenResty a veces contiene errores y omisiones, por lo que debes verificarlos con pruebas reales.

A continuación, modifiquemos el conjunto de pruebas para asegurarnos de que la fase init pueda ejecutar la API get del diccionario compartido.

¿Cómo podemos encontrar el conjunto de casos de prueba relacionados con la memoria compartida? Los casos de prueba de OpenResty se colocan en el directorio /t y se nombran regularmente, es decir, número-auto-incremental-nombre-de-función.t. Busca shdict y encontrarás 043-shdict.t, el conjunto de casos de prueba de memoria compartida, que contiene cerca de 100 casos de prueba, incluyendo pruebas para diversas circunstancias normales y anormales.

Intentemos modificar el primer caso de prueba.

Puedes reemplazar la fase content con una fase init y eliminar el código superfluo para ver si la interfaz get funciona. No necesitas entender cómo se escribe, organiza y ejecuta el caso de prueba en esta etapa. Solo necesitas saber que está probando la interfaz 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

Deberías haber notado que al final del caso de prueba, agregué la bandera --ONLY, lo que significa ignorar todos los demás casos de prueba y solo ejecutar este, mejorando así la velocidad de ejecución. Más adelante en la sección de pruebas, explicaré específicamente las diversas etiquetas.

Después de la modificación, podemos ejecutar el caso de prueba con el comando prove.

prove t/043-shdict.t

Luego, obtendrás un error que corrobora los límites de fase descritos en la documentación.

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

Pregunta 2: ¿Cuándo tiene la función get múltiples valores de retorno?

Veamos la segunda pregunta, que se puede resumir de la documentación oficial. La documentación comienza con la descripción syntax de esta interfaz.

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

En circunstancias normales.

  • El primer parámetro value devuelve el valor correspondiente a la key en el diccionario; sin embargo, cuando la key no existe o expira, el valor de value es nil.
  • El segundo parámetro, flags, es un poco más complicado; si la interfaz set establece flags, los devuelve. De lo contrario, no.

Si la llamada a la API falla, value devuelve nil y flags devuelve un mensaje de error específico.

De la información resumida en la documentación, podemos ver que local v = dogs:get("Jim") está escrito con solo un parámetro de recepción. Tal escritura es incompleta porque solo cubre el escenario de uso típico sin recibir un segundo parámetro o realizar manejo de excepciones. Podríamos modificarlo de la siguiente manera.

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

Al igual que con la primera pregunta, podemos buscar en el conjunto de casos de prueba para confirmar nuestra comprensión de la documentación.

  === 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]

En este caso de prueba, la interfaz get tiene una entrada nil, y el mensaje de error devuelto es nil key. Esto verifica que nuestro análisis de la documentación es correcto y proporciona una respuesta parcial a la tercera pregunta. Al menos, la entrada a get no puede ser nil.

Pregunta 3: ¿Cuál es el tipo de entrada de la función get?

En cuanto a la tercera pregunta, ¿qué tipo de parámetros de entrada puede aceptar get? Primero revisemos la documentación, pero desafortunadamente, encontrarás que la documentación no especifica cuáles son los tipos legales de keys. ¿Qué debemos hacer?

No te preocupes. Al menos sabemos que la key puede ser de tipo cadena y no puede ser nil. ¿Recuerdas los tipos de datos en Lua? Además de cadenas y nil, hay números, arrays, tipos booleanos y funciones. Los dos últimos no son necesarios como keys, por lo que solo necesitamos verificar los dos primeros: números y arrays. Podemos comenzar buscando en el archivo de prueba casos donde se usen números como key.

=== TEST 4: number keys, string values

Con este caso de prueba, puedes ver que los números también pueden usarse como keys, y internamente se convertirán en cadenas. ¿Qué pasa con los arrays? Desafortunadamente, el caso de prueba no lo cubre, por lo que necesitamos probarlo nosotros mismos.

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

Como era de esperar, se reportó el siguiente error.

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

En resumen, podemos concluir que los tipos de key aceptados por la API get son cadenas y números.

¿Hay un límite de longitud de la key que se pasa? Hay un caso de prueba correspondiente aquí.

=== 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]

Cuando la longitud de la cadena es 65536, se te notificará que la key es demasiado larga. Puedes intentar cambiar la longitud a 65535, aunque solo sea 1 byte menos, pero no habrá más errores. Esto significa que la longitud máxima de la key es exactamente 65535.

Resumen

Finalmente, me gustaría recordarte que en la API de OpenResty, cualquier valor de retorno con un mensaje de error debe tener una variable para recibirlo y hacer manejo de errores, de lo contrario se cometerá un error. Por ejemplo, si se coloca una conexión incorrecta en el grupo de conexiones, o si la llamada a la API falla y se continúa con la lógica detrás de ella, hace que la gente se queje incesantemente.

Entonces, si encuentras un problema cuando escribes código de OpenResty, ¿cuál es tu forma habitual de resolverlo? ¿Es la documentación, las listas de correo u otros canales?

Te invito a compartir este artículo con tus colegas y amigos para que podamos comunicarnos y mejorar.