Документация и тестовые случаи: мощные инструменты для решения проблем разработки в OpenResty

API7.ai

October 23, 2022

OpenResty (NGINX + Lua)

После изучения принципов и нескольких основных концепций OpenResty, мы наконец приступаем к изучению API.

По моему личному опыту, изучение API OpenResty относительно простое, поэтому для его описания не потребуется много статей. Вы можете задаться вопросом: разве API не является самой распространенной и важной частью? Почему не уделить ему много времени? Есть два основных соображения.

Во-первых, OpenResty предоставляет очень подробную документацию. По сравнению со многими другими языками программирования или платформами, OpenResty предоставляет не только определения параметров API и возвращаемых значений, но и полные и запускаемые примеры кода, которые ясно показывают, как API обрабатывает различные граничные условия.

Следование определению API с примерами кода и предупреждениями — это последовательный стиль документации OpenResty. Поэтому, прочитав описание API, вы можете сразу же запустить пример кода в своей среде и изменить параметры и документацию, чтобы проверить их и углубить свое понимание.

Во-вторых, OpenResty предоставляет полные тестовые примеры. Как я уже упоминал, документация OpenResty показывает примеры кода API. Однако из-за ограничений по объему документ не представляет отчеты об ошибках и обработку в различных аномальных ситуациях, а также метод использования нескольких API.

Но не беспокойтесь. Большую часть этого содержимого вы можете найти в наборе тестовых примеров.

Для разработчиков OpenResty лучшими материалами для изучения API являются официальная документация и тестовые примеры, которые профессиональны и дружелюбны к читателям.

Дай человеку рыбу, и ты накормишь его на день; научи человека ловить рыбу, и ты накормишь его на всю жизнь. Давайте на реальном примере испытаем, как использовать силу документации и набора тестовых примеров в разработке OpenResty.

Возьмем API get shdict в качестве примера

На основе общей области памяти NGINX, общий словарь (shared dictionary) — это объект словаря Lua, который может получать доступ к данным через несколько воркеров и хранить данные, такие как ограничение скорости, кэш и т.д. Существует более 20 API, связанных с общим словарем — это наиболее часто используемый и важный API в OpenResty.

Давайте возьмем самую простую операцию get в качестве примера; вы можете перейти по ссылке на документацию для сравнения. Следующий минималистичный пример кода адаптирован из официальной документации.

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) } } } }

Как краткое примечание, прежде чем мы сможем использовать общий словарь в коде Lua, нам нужно добавить блок памяти в nginx.conf с директивой lua_shared_dict, который назван "dogs" и имеет размер 10M. После изменения nginx.conf необходимо перезапустить процесс и получить доступ с помощью браузера или команды curl, чтобы увидеть результаты.

Не кажется ли это немного утомительным? Давайте изменим его более просто. Как вы можете видеть, использование CLI resty таким образом имеет тот же эффект, что и встраивание кода в nginx.conf.

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

Теперь вы знаете, как работают вместе nginx.conf и код Lua, и вы успешно запустили методы set и get общего словаря. Обычно большинство разработчиков останавливаются на этом. Здесь есть несколько вещей, на которые стоит обратить внимание.

  1. На каких этапах нельзя использовать API, связанные с общей памятью?
  2. Мы видим в примере кода, что функция get имеет только одно возвращаемое значение. Тогда когда будет более одного возвращаемого значения?
  3. Какой тип входных данных у функции get? Есть ли ограничение по длине?

Не недооценивайте эти вопросы; они могут помочь нам лучше понять OpenResty, и я проведу вас через них по отдельности.

Вопрос 1: На каких этапах нельзя использовать API, связанные с общей памятью?

Давайте рассмотрим первый вопрос. Ответ прост; в документации есть специальный раздел context (т.е. раздел контекста), который перечисляет среды, в которых можно использовать 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*

Как вы можете видеть, фазы init и init_worker не включены, что означает, что API get общей памяти нельзя использовать на этих двух этапах. Обратите внимание, что каждый API общей памяти может использоваться на разных этапах. Например, API set можно использовать на этапе init.

Всегда читайте документацию при использовании. Конечно, документация OpenResty иногда содержит ошибки и упущения, поэтому вам нужно проверять их с помощью реальных тестов.

Далее давайте изменим набор тестов, чтобы убедиться, что на этапе init можно запустить API get общего словаря.

Как мы можем найти набор тестов, связанный с общей памятью? Тестовые примеры OpenResty все размещены в каталоге /t и названы регулярно, т.е. self-incremented-number-function-name.t. Поищите shdict, и вы найдете 043-shdict.t, набор тестовых примеров для общей памяти, который содержит около 100 тестовых примеров, включая тесты для различных нормальных и аномальных обстоятельств.

Давайте попробуем изменить первый тестовый пример.

Вы можете заменить фазу content на фазу init и удалить лишний код, чтобы увидеть, работает ли интерфейс get. На этом этапе вам не нужно понимать, как написан, организован и запущен тестовый пример. Вам нужно только знать, что он тестирует интерфейс 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

Вы должны были заметить, что в конце тестового примера я добавил флаг --ONLY, что означает игнорирование всех других тестовых примеров и выполнение только этого, что повышает скорость выполнения. Позже в разделе тестов я специально объясню различные теги.

После изменения мы можем запустить тестовый пример с командой prove.

prove t/043-shdict.t

Затем вы получите ошибку, которая подтверждает ограничения этапов, описанные в документации.

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

Вопрос 2: Когда функция get имеет несколько возвращаемых значений?

Давайте рассмотрим второй вопрос, который можно обобщить из официальной документации. Документация начинается с описания syntax этого интерфейса.

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

В нормальных условиях.

  • Первый параметр value возвращает значение, соответствующее key в словаре; однако, когда key не существует или истек, значение value равно nil.
  • Второй параметр, flags, немного сложнее; если интерфейс set устанавливает флаги, он возвращает их. В противном случае нет.

Если вызов API завершается ошибкой, value возвращает nil, а flags возвращают конкретное сообщение об ошибке.

Из информации, обобщенной в документации, мы видим, что local v = dogs:get("Jim") написано только с одним принимающим параметром. Такое написание неполное, так как оно охватывает только типичный сценарий использования без получения второго параметра или обработки исключений. Мы могли бы изменить его следующим образом.

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

Как и в первом вопросе, мы можем поискать в наборе тестовых примеров, чтобы подтвердить наше понимание документации.

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

В этом тестовом примере интерфейс get имеет входные данные nil, и возвращаемое сообщение об ошибке — nil key. Это подтверждает, что наш анализ документации верен, и дает частичный ответ на третий вопрос. По крайней мере, входные данные для get не могут быть nil.

Вопрос 3: Какой тип входных данных у функции get?

Что касается третьего вопроса, какие типы входных параметров может принимать get? Давайте сначала проверим документацию, но, к сожалению, вы обнаружите, что документация не указывает, какие типы ключей являются допустимыми. Что нам делать?

Не беспокойтесь. По крайней мере, мы знаем, что key может быть строковым типом и не может быть nil. Помните ли вы типы данных в Lua? Помимо строк и nil, есть числа, массивы, булевы типы и функции. Последние два не нужны в качестве ключей, поэтому нам нужно проверить только первые два: числа и массивы. Мы можем начать с поиска в тестовом файле случаев, когда числа используются как key.

=== TEST 4: number keys, string values

С этим тестовым примером вы можете видеть, что числа также могут использоваться в качестве ключей, и внутри они будут преобразованы в строки. А как насчет массивов? К сожалению, тестовый пример не охватывает это, поэтому нам нужно попробовать самим.

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

Неудивительно, что была сообщена следующая ошибка.

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

В итоге мы можем заключить, что типы key, принимаемые API get, — это строки и числа.

Есть ли ограничение по длине входящего ключа? Здесь есть соответствующий тестовый пример.

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

Когда длина строки равна 65536, вам будет сообщено, что ключ слишком длинный. Вы можете попробовать изменить длину на 65535, хотя всего на 1 байт меньше, но ошибок больше не будет. Это означает, что максимальная длина ключа составляет ровно 65535.

Заключение

Наконец, я хотел бы напомнить вам, что в API OpenResty любое возвращаемое значение с сообщением об ошибке должно иметь переменную для получения и обработки ошибок, иначе это приведет к ошибке. Например, если неправильное соединение попадает в пул соединений, или если вызов API завершается неудачей, продолжение логики за ним вызывает постоянные жалобы.

Итак, если вы столкнулись с проблемой при написании кода OpenResty, как вы обычно решаете ее? Это документация, списки рассылки или другие каналы?

Поделитесь этой статьей с вашими коллегами и друзьями, чтобы мы могли общаться и улучшать.