文档与测试用例:解决OpenResty开发问题的强大工具

API7.ai

October 23, 2022

OpenResty (NGINX + Lua)

OpenRestyの原則といくつかの重要な概念を学んだ後、いよいよAPIの学習を始めます。

私の個人的な経験から言うと、OpenRestyのAPIを学ぶのは比較的簡単なので、多くの記事を費やす必要はありません。あなたは疑問に思うかもしれません:APIは最も一般的で重要な部分ではないのか?なぜ多くの時間を費やさないのか?これには主に2つの理由があります。

まず、OpenRestyは非常に詳細なドキュメントを提供しています。他の多くのプログラミング言語やプラットフォームと比較して、OpenRestyはAPIのパラメータと戻り値の定義だけでなく、完全で実行可能なコード例も提供しており、APIがさまざまな境界条件をどのように処理するかを明確に示しています。

API定義に従って、コード例と注意点を示すのは、OpenRestyドキュメントの一貫したスタイルです。したがって、APIの説明を読んだ後、すぐにサンプルコードを自分の環境で実行し、パラメータとドキュメントを変更して検証し、理解を深めることができます。

第二に、OpenRestyは包括的なテストケースを提供しています。前述したように、OpenRestyのドキュメントはAPIのコード例を示しています。しかし、スペースの制約により、ドキュメントにはさまざまな異常な状況でのエラーレポートと処理、および複数のAPIの使用方法は示されていません。

しかし、心配しないでください。これらの内容のほとんどは、テストケースセットで見つけることができます。

OpenResty開発者にとって、最良のAPI学習資料は公式ドキュメントとテストケースであり、これらは専門的で読者に優しいものです。

魚を与えるのではなく、魚の釣り方を教えましょう。実際の例を使って、OpenResty開発においてドキュメントとテストケースセットの力をどのように発揮するかを体験してみましょう。

shdictのget APIを例に

NGINX共有メモリ領域に基づく共有辞書(shared dictionary)は、複数のワーカー間でデータにアクセスし、レート制限、キャッシュなどのデータを保存できるLua辞書オブジェクトです。共有辞書に関連するAPIは20以上あり、OpenRestyで最もよく使われる重要なAPIです。

最もシンプルな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.conflua_shared_dictディレクティブを使用してメモリブロックを追加する必要があります。この例では、"dogs"という名前で10Mのサイズです。nginx.confを変更した後、プロセスを再起動し、ブラウザやcurlコマンドでアクセスして結果を確認します。

これは少し面倒に思えるかもしれません。もっと簡単に変更してみましょう。このようにresty CLIを使用すると、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関数の戻り値が1つだけですが、複数の戻り値がある場合とは?
  3. get関数の入力の型は何か?長さの制限はあるか?

これらの質問を軽視しないでください。これらはOpenRestyをより深く理解するのに役立ちます。それぞれについて説明します。

質問1: どのフェーズで共有メモリ関連のAPIを使用できないのか?

最初の質問を見てみましょう。答えは簡単で、ドキュメントにはAPIが使用できる環境を示す専用のcontext(つまりコンテキストセクション)があります。

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*

ご覧の通り、initinit_workerフェーズは含まれていません。つまり、共有メモリのget APIはこれらの2つのフェーズでは使用できません。各共有メモリAPIは異なるフェーズで使用できることに注意してください。例えば、set APIはinitフェーズで使用できます。

使用する際は常にドキュメントを読んでください。もちろん、OpenRestyのドキュメントには時々誤りや抜けがあるので、実際のテストで検証する必要があります。

次に、テストセットを変更して、initフェーズで共有辞書のget APIが実行できるかどうかを確認しましょう。

共有メモリに関連するテストケースセットはどのように見つけるのでしょうか?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フラグを追加したことに気づいたでしょう。これは他のすべてのテストケースを無視し、この1つだけを実行することを意味し、実行速度を向上させます。後のテストセクションで、さまざまなタグについて具体的に説明します。

変更後、proveコマンドでテストケースを実行できます。

prove t/043-shdict.t

すると、ドキュメントに記載されているフェーズの制限を裏付けるエラーが表示されます。

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

質問2: get関数が複数の戻り値を返すのはいつか?

2番目の質問を見てみましょう。これは公式ドキュメントから要約できます。ドキュメントはこのインターフェースのsyntax説明から始まります。

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

通常の場合。

  • 最初のパラメータvalueは、辞書内のkeyに対応する値を返します。ただし、keyが存在しないか期限切れの場合、valuenilです。
  • 2番目のパラメータflagsは少し複雑です。setインターフェースでflagsが設定されている場合、それを返します。そうでない場合は返しません。

API呼び出しが失敗した場合、valuenilを返し、flagsは特定のエラーメッセージを返します。

ドキュメントに要約された情報から、local v = dogs:get("Jim")という書き方は不完全であることがわかります。なぜなら、典型的な使用シナリオしかカバーしておらず、2番目のパラメータを受け取ったり、例外処理を行ったりしていないからです。以下のように変更できます。

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が入力され、返されるerrメッセージはnil keyです。これにより、ドキュメントの分析が正しいことが確認され、3番目の質問に対する部分的な答えも得られます。少なくとも、getの入力はnilであってはなりません。

質問3: get関数の入力の型は何か?

3番目の質問について、getの入力パラメータはどのような型が許されるのでしょうか?まずドキュメントを確認しますが、残念ながらドキュメントにはkeyの合法な型が指定されていません。どうすればよいでしょうか?

心配しないでください。少なくとも、keyが文字列型であり、nilであってはならないことはわかっています。Luaのデータ型を覚えていますか?文字列とnil以外に、数字、配列、ブール型、関数があります。後者2つはkeyとして不必要なので、最初の2つ、数字と配列を検証する必要があります。まず、テストファイルで数字がkeyとして使用されているケースを検索します。

=== TEST 4: number keys, string values

このテストケースから、数字もkeyとして使用でき、内部的には文字列に変換されることがわかります。配列はどうでしょうか?残念ながら、テストケースではカバーされていないので、自分で試す必要があります。

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

まとめると、get APIが受け入れるkeyの型は文字列と数字です。

では、入力keyの長さに制限はあるのでしょうか?これに対応するテストケースがあります。

=== 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の場合、keyが長すぎると表示されます。長さを65535に変更してみてください。たった1バイト少ないだけで、エラーは発生しなくなります。つまり、keyの最大長は65535です。

まとめ

最後に、OpenResty APIでは、エラーメッセージを含む戻り値は必ず変数で受け取り、エラー処理を行う必要があることを思い出してください。そうしないと、間違った接続が接続プールに入ったり、API呼び出しが失敗した後にロジックを続行したりするなど、人々を絶えず不満にさせることになります。

では、OpenRestyコードを書く際に問題に遭遇した場合、通常どのように解決しますか?ドキュメント、メーリングリスト、または他のチャネルでしょうか?

この記事を同僚や友人と共有して、コミュニケーションと改善を図りましょう。