コンテンツにスキップ

3.0.0への移行

このガイドは、スクリプト、設定、ビルドを、ngx_mruby 2.xからngx_mruby 3.0.0へ移すためのものです。このガイドが扱う変更は、どれもv3.0.0のリリースノートのアップグレードの前に読む挙動の変更に1回ずつ書いてあります。2.xが何をしたか、3.0.0が何をするか、何に影響するか、何をすればよいかです。このガイドは、そのエントリーのそれぞれにリンクし、手順と例を加えます。エントリーが影響するスクリプトとビルドの見つけ方、スクリプトの変更の形、結果の確かめ方です。先に、動かしているバージョンより新しい2.xのリリースのリリースノートを読んでください。リリースノートはdocs/releasesにあります。

ngx_mruby 2.xは、mruby 3.3.0を同梱しています。ngx_mruby 3の開発のラインは、mrubyの現在の安定版の4.0.0を同梱しています。v3の計画のdocs/proposals/v3-plan.mdは、7節の決定1で、mruby 4.1.0をv3の目標とし、その前のステップとして4.0.0を承認しています。4.1.0には、まだリリース候補しかありません。mruby 4.0.0のエントリーは、ngx_mrubyについて見つけて測った変更で、すべての変更の一覧ではありません。mruby自身が利用者に見える変更をまとめた一覧は、mruby/のdoc/mruby3.4.mdとdoc/mruby4.0.mdです。mruby 4.1.0にはさらに変更があり、リリースノートとこのガイドは、まだそれを扱っていません。

  1. モジュールのビルドの方法を確かめます。ビルドの節を見てください。
  2. エントリーが影響するスクリプトを見つけて、書き換えます。スクリプトの節を見てください。
  3. ログのパーサとアラートの設定を更新します。ログの監視の節を見てください。
  4. mrubyのC APIを使うmrbgemやほかのCのコードをメンテナンスしているなら、そのコードを確かめます。C拡張とmrbgemsの節を見てください。
  5. 結果を確かめます。結果を確かめるの節を見てください。
  • このリリースのbuild_config.rbを使うなら、モジュールをビルドし直してください。同梱のmrubyも一緒にビルドします。詳しくは同梱のmrubyが3.3.0から4.0.0になるにあります。

  • 自前のbuild_config.rbを使うなら、コアのgemを選ぶ行を見つけてください。詳しくは自前のbuild_config.rbは、mruby 4.0.0のコアgemの変更に合わせる必要があるにあります。

    grep -nE 'gembox|mruby-print|hal-posix|mruby-task|mruby-encoding|mruby-sleep|ngx_mruby_core_gems' build_config.rb

    ngx_mrubyのデフォルトのビルドが入れるコアのgemを残すには、このリリースのbuild_config.rbからNGX_MRUBY_CORE_GEMSとngx_mruby_core_gemsをコピーし、そのファイルと同じように、conf.gembox 'full-core'の代わりにngx_mruby_core_gems(conf)を呼んでください。gemを減らすビルドでは、NGX_MRUBY_CORE_GEMS - %w[...]を渡します。一覧に無いgemを加えるには、コアのgemに従ってください。

  • 新しくチェックアウトしたツリーやソースのアーカイブからのビルドは、サードパーティのmrbgemsを、build_config.rb.lockが記録するコミットで取ってきます。詳しくはサードパーティのmrbgemをbuild_config.rb.lockが記録するコミットでビルドするにあります。mruby/buildを残しているツリーでは、ロックを変えたら、次のビルドの前にmruby/buildを消してください。消さないと、クローンに新しいコミットが無いときに、rakeがfatal: reference is not a treeで止まります。

    rm -rf mruby/build

    gemを別のコミットでビルドするには、gemのコミットとbuild_config.rb.lockに従ってください。

  • デフォルトのビルドには、auto-sslのmrbgemと、それと一緒に入っていたクラスがありません。詳しくは既定のビルドからauto-sslのmrbgemと、それに伴うクラスが無くなるにあります。それらを使うスクリプトを探す手順は、スクリプトの節に書いてあります。

  • このリリースのどのビルドにもauto-sslのmrbgemはなく、mrubyのビルドはNGX_MRUBY_AUTO_SSLをもう読みません。詳しくはauto-sslのmrbgemとNGX_MRUBY_AUTO_SSLが無くなるにあります。この変数は効かないので、ビルドの環境から外してください。Nginx::SSL::ACMEで証明書を取得しているサーバーは、アップグレードの前にnginxのngx_http_acme_module、つまりnginx-acmeに移るか、2.xを使い続けてください。次のコマンドで、それを使うビルドと設定を探します。

    grep -rnE 'NGX_MRUBY_AUTO_SSL|auto-ssl|Nginx::SSL::ACME' /path/to/build/scripts /path/to/nginx/conf
  • ./configure --with-mruby-incdirを使うなら、ディレクトリの一覧に<mruby_root>/build/host/includeを加えてください。詳しくはC拡張:mruby.hをインクルードするソースに、生成されたヘッダが必要になるにあります。

  • 自前やサードパーティのmrbgemsを使っているなら、C拡張とmrbgemsの節を見てください。

ほとんどのエントリーは、検索できるメソッドの名前を挙げています。次のコマンドは、設定のスクリプトのファイルからそれらを探します。/path/to/scriptsは、*_handlerのディレクティブが指定するファイルのディレクトリに置き換えてください。

grep -rnE 'Kernel\.|valid_encoding\?|allbits\?|anybits\?|nobits\?|remove_const|module_function|append_features|prepend_features|extend_object|singleton_method_added|\.(included|extended|prepended|inherited|method_added)\(|initialize_copy|respond_to_missing|respond_to\?|method_defined\?|method_missing|instance_methods|\.methods|iv_var|default_proc *=|FileTest|srand|Random\.new|Set\.new|Set\[|flatten_merge|RuntimeError' /path/to/scripts

nginx.confのファイルにある*_codeのディレクティブのインラインのコードも、同じように検索してください。次の項目では、名前だけでは足りないところで何を探すかを書き、変更の例を示します。

  • Hash#inspectとHash#to_sの形式が変わる:値がHashかHashのArrayになるところでinspect、to_s、"#{...}"、%sを使うsprintfやformatを探してください。スクリプトやテストが比べる文字列の=>も探してください。

    h = {"q" => "x"}
    Nginx.rputs h.to_s # 2.x: {"q"=>"x"} 3.0.0: {"q" => "x"}
    Nginx.rputs JSON.generate(h) # {"q":"x"} on both
  • Set#inspectとSet#to_sの形式が変わり、Set#flatten_mergeが無くなる:Setについても、同じように検索してください。set.to_a.join(",")は、どちらのバージョンでも同じ文字列を返します。上の検索は、flatten_mergeも見つけます。s.flatten_merge(t)は、s.merge(Set.new(t).flatten)に書き換えます。tがSetかArrayなら、この式はどちらのバージョンでも、2.xのs.flatten_merge(t)と同じSetを返します。s.merge(t.flatten)がそのSetを返すのは、tがSetのときだけです。tがArrayだと、Array#flattenは中のArrayを平坦化してSetを残し、flatten_mergeとは逆になります。

    t = [2, Set.new([3])]
    Set.new([1]).flatten_merge(t) # 2.x: the Set of 1, 2 and 3 3.0.0: NoMethodError
    Set.new([1]).merge(Set.new(t).flatten) # the Set of 1, 2 and 3 on both
    Set.new([1]).merge(t.flatten) # the Set of 1, 2 and a Set that holds 3, on both
  • NoMethodErrorのメッセージがレシーバのクラスを示す:rescue NoMethodErrorの近くのmessageと、スクリプトの中のundefined methodを探してください。

    e.message == "undefined method 'foo'" # 2.x: true 3.0.0: false (the message goes on with " for NilClass")

    メッセージを比べる代わりに、NoMethodErrorそのものをrescueしてください。

  • privateとprotectedの制限が効く:self以外のレシーバで呼んでいるものを探してください。対象は、initialize、initialize_copy、respond_to_missing?、method_missing、トップレベルでdefで定義したメソッド、raise、sprintf、putsのようなKernelのメソッド、privateかprotectedの下のメソッドです。ただし、ハンドラのコードでは、トップレベルでdefで定義したメソッドはpublicのままなので、レシーバを付けた呼び出しを変える必要はありません。詳しくは#502の構想の1.4節にあります。上の検索は、Kernelに対する呼び出しのKernel\.を見つけます。今も動くのは、Kernel.format、.sprintf、.raise、.sleep、.usleepだけで、Kernel.Integer(x)のようなほかのものは、レシーバの無い呼び出しにします。検索はinstance_methodsとmethodsも見つけます。この2つはprivateのメソッドを含みません。そのような呼び出しの前にrespond_to?で確かめても役に立ちません。respond_to?は、privateのメソッドにもtrueを返します。

    class Router
    def call(r)
    pick(r) # no receiver: works on both
    end
    private
    def pick(r)
    r.uri
    end
    end
    Router.new.pick(r) # 2.x: works 3.0.0: NoMethodError
    Router.new.send(:pick, r) # works on both
    Kernel.Integer("12") # 2.x: 12 3.0.0: NoMethodError (private method 'Integer' called for Module)
    Integer("12") # 12 on both
  • Module#remove_const、#private、#public、#protected、#module_functionとフックがprivateになる:上の検索でほとんど見つかります。クラスやモジュールに対して呼んだ.private(、.public(、.protected(も探してください。たとえば、Foo.remove_const(:X)はFoo.send(:remove_const, :X)に書き換えます。

  • append_features、prepend_features、extend_objectのオーバーライドが呼ばれなくなる:上の検索で見つかります。

    module Plugin
    def self.append_features(base) # 3.0.0: never called, and no error
    base.extend(ClassMethods)
    super
    end
    end
    module Plugin
    def self.included(base) # called on both
    base.extend(ClassMethods)
    end
    end
  • allbits?、anybits?、nobits?がIntegerだけのメソッドになる:上の検索で見つかります。Floatのx.allbits?(1)はx.to_i.allbits?(1)に書き換えます。

  • FileTestがモジュールになる:上の検索で、FileTestが見つかります。書き換える必要があるのは、FileTest.exist?のようなメソッドの呼び出し以外の使い方だけになります。

  • メソッドの本体で定数に代入するとSyntaxErrorになる:スクリプトをコンパイルすると、見つかります。結果を確かめるの節を見てください。

    def setup
    Table = {} # 3.0.0: SyntaxError when the script is compiled
    Object.const_set(:Table, {}) # works on both
    end
  • Floatの値にビット演算が無くなる:Floatになりうる値に使った<<、>>、&、|、^、~を探してください。Floatになりうる値はto_fやFloat()の結果とFloatとの演算の結果です。2.xと同じ数を得る方法は、エントリーにあります。

    x = 1.5
    x << 1 # 2.x: 3 3.0.0: NoMethodError
    (x * 2).to_i # 3, the number of 2.x for x << 1
    x.to_i << 1 # 2, another number
  • String#valid_encoding?が既定のビルドに無くなる:上の検索で見つかります。2.xのデフォルトのbuild_config.rbでは、このメソッドは、どの文字列にもtrueを返しました。そのため、次のような確認はリクエストを拒否したことがなく、消してもかまいません。

    Nginx.return Nginx::HTTP_BAD_REQUEST unless s.valid_encoding?
  • シードを与えたRandomが別の数を返す:上の検索でsrandとRandom.newが見つかります。シードを与えた乱数の生成器の数を期待するテストも探してください。

  • Hash#default_proc=がProcでないオブジェクトにTypeErrorを出す:上の検索で見つかります。h.default_proc = proc { |hash, key| hash[key] = [] }のように、Procを渡してください。

  • キーワード引数を受けるブロックに、キーワード引数を付けてyieldすると動く:変えるものはありません。この場合に備えて書いたrescue ArgumentErrorには、もう到達しません。

  • ハンドラのコードで、ブロックやforの本体の中のreturnがコードを終える:スクリプトで変えるものはありません。ハンドラのコードのトップレベルにあるブロックやforの本体の中のreturnは、もうLocalJumpErrorを起こしません。ハンドラは、コードがreturnの前に設定したレスポンスで答えます。2.xは500で答えていました。2.7.0は、contentハンドラがそれまでに書いた出力とともに200で答えていました。unexpected return (LocalJumpError)のログのアラートは、この場合にはもう出ません。

  • 出力なしで終わるcontentハンドラが、500でなく、空のボディの200で応答する:ボディを書かずに終わることがあるmruby_content_handlerとmruby_content_handler_codeのハンドラを見てください。条件が成り立つときだけ書くハンドラ、""やnilになりうる値をNginx.rputsに渡すハンドラ、レスポンスのヘッダだけを設定するハンドラ、Nginx.return Nginx::HTTP_OKか、201と204以外の200から299のステータスで終わるハンドラです。3.0.0では、これらはContent-Length: 0とともにそのステータスで答えます。ステータスを設定していなければ200です。2.7.1では、500で答えていました。500に頼っていたハンドラは、意図するステータスを設定してください。

    body = Nginx::Request.new.headers_in["X-Answer"].to_s
    Nginx.rputs body # body "": 2.7.1 answers 500 3.0.0 answers 200 with Content-Length: 0
    if body.empty?
    Nginx.return Nginx::HTTP_NOT_FOUND # 404 on both
    else
    Nginx.rputs body
    end
  • 空のボディで終わる出力のボディフィルタが、接続を閉じずにレスポンスを送る:変えるものはありません。Nginx::Filter#body=や#outputで""を設定するボディフィルタや、ボディの無いレスポンスのボディを空のままにするボディフィルタは、ステータスをContent-Length: 0とともに送ります。2.xは、1バイトも送らずに接続を閉じていました。

  • Nginx::Varのアクセサが、ワーカーで最初に使われたときにNginx::Varのメソッドになる:上の検索でrespond_to?、method_defined?、instance_methods、methodsが見つかります。そのうち、Nginx::VarのインスタンスかNginx::Varそのものに対するものを見てください。変数についての答えは、そのワーカーが前にその名前にアクセスしたかどうかで変わるようになりました。代わりにexist?を使ってください。

    v = Nginx::Var.new
    v.respond_to?(:request_method) # 2.x: false 3.0.0: true after the first v.request_method in the worker
    v.exist?("request_method") # true on both

    スクリプトがObjectやKernelに定義したメソッドのうち、スクリプトがv.NAMEで読む変数と同じ名前のものも探してください。たとえば、ハンドラのコードのトップレベルのdefで定義したメソッドです。ワーカーに変数のアクセサができると、v.NAMEはそのメソッドを呼ばずに変数を読みます。そのメソッドの名前を変えてください。検索はmethod_missingも見つけます。Nginx::Var#method_missingをラップするスクリプトは、アクセサができる名前について、最初のアクセスしか見ません。たとえば、Nginx::Varにprependしたモジュールのmethod_missingがsuperを呼ぶ場合です。ラッパーがしていたことは、変数を読むコードへ移してください。

  • Nginx::Connection、Nginx::Server、Nginx::Requestが、Nginx::Varを呼ばずに変数を読む:スクリプトがNginx::Varに定義したメソッドを探してください。class Nginx::Var、Nginx::Var.class_eval、define_methodによる定義と、上の検索で見つかるiv_varです。Nginx::Varのメソッドを変えても、Nginx::Connection#remote_ipやエントリーにあるほかのメソッドの戻り値は、もう変わりません。代わりに、Nginx::Connectionのような、そのメソッドを持つクラスにメソッドを定義してください。定義は、nginxが設定を読むときに1回だけ動くmruby_init_codeに書きます。ハンドラのコードは、同じmrb_stateの中でリクエストのたびに動きます。そのため、ハンドラのコードに書くと、2回目のalias_methodでnginx_remote_ipが新しいremote_ipのコピーになり、それが自分自身を呼ぶことになります。

    mruby_init_code '
    class Nginx::Connection
    alias_method :nginx_remote_ip, :remote_ip
    def remote_ip
    Nginx::Request.new.headers_in["X-Real-IP"] || nginx_remote_ip # works on both
    end
    end
    ';
  • Dirをmrubyのコアgemが定義するようになり、閉じたDirがRuntimeErrorでなくIOErrorを出す:上の検索で、RuntimeErrorが見つかります。そのうち、閉じることのあるDirか、Dirを閉じるブロックを付けたDir.openの例外をrescueしているものを見てください。

    d = Dir.open(path)
    d.close
    begin
    d.read
    rescue RuntimeError # 2.x: rescued 3.0.0: not rescued (IOError)
    end
    begin
    d.read
    rescue IOError, RuntimeError # rescued on both
    end
  • 既定のビルドからauto-sslのmrbgemと、それに伴うクラスが無くなる:次のコマンドでデフォルトのビルドに無くなった定数とDirの2つのメソッドを探します。

    grep -rnE 'Nginx::SSL::ACME|Acme|HttpRequest|OpenSSL|PolarSSL|SimpleHttp|(^|[^:])HTTP::|Base64|Forwardable|FORWARDABLE|Tempfile|tmpdir' /path/to/scripts

    Base64、Dir.tmpdir、Dir.mktmpdirには、デフォルトのビルドで使える代わりがあります。

    Base64.encode(s) # 3.0.0: NameError (uninitialized constant Base64)
    [s].pack('m0') # the same string on both
    Base64.decode(b) # 3.0.0: NameError
    b.unpack1('m') # the same string on both
    Dir.tmpdir # 3.0.0: NoMethodError
    ENV['TMPDIR'] || '/tmp' # works on both

    Dir.mktmpdirの代替は、auto-sslのmrbgemと、それと一緒に入っていたクラスにあります。ほかの定数に必要なgemはconf.gemで追加します。

Dir.children、Dir#each_child、Dir.empty?と、Dirに対するEnumerableのメソッドは、2.xのスクリプトには新しいものです。最初の2つは.と..を含みません。詳しくは新機能にあります。

  • Exception#inspectの形式が変わり、error.logのmrb_run failedの行も変わる:ログのパーサとアラートのルールで、mrb_run failedの行に対するパターンと、行の最後にある括弧の中の例外のクラスを探してください。

    2.x: mrb_run failed: return 500 HTTP status code to client: error: boom (RuntimeError)
    3.0.0: mrb_run failed: return 500 HTTP status code to client: error: #<RuntimeError: boom>

    \(RuntimeError\)$のようなパターンは、#<RuntimeError: に書き換えます。

  • NoMethodErrorのメッセージがレシーバのクラスを示す:メッセージの後に for <class>が続くので、undefined method 'foo'で終わるパターンはもう一致しなくなりました。

  • nginxが設定を読む間のメッセージのレベルがnoticeからinfoになる:ログのパーサ、アラートのルール、起動のスクリプトで、mechanism enabled、compile info、target variable、ngx_mruby NOTICEを探してください。nginxは、起動しても、もうこれらの行を書きません。nginx -tは、これらを[info]で起動時のログに書きます。起動時のログは-eで指定したファイルで、指定しなければnginxに組み込んだエラーログのパスです。デフォルトでは、プレフィックスの下のlogs/error.logです。nginxが起動時のログの行を端末にも出すのはwarn以上だけなので、nginx -tの出力にはこれらの行が出ません。

    2.x, start: [notice] ... ngx_mruby/<version> (mruby/3.3.0) mechanism enabled in ...
    3.0.0, start: no such line
    3.0.0, nginx -t: [info] ... ngx_mruby/<version> (mruby/4.0.0) mechanism enabled in ... (startup log only)
    3.0.0, nginx -t -e stderr: [info] ... ngx_mruby/<version> (mruby/4.0.0) mechanism enabled in ... (terminal)

    モジュールが読み込まれたことをこれらの行で確かめるには、nginx -t -e stderrを実行してその出力を読むか、nginx -tの後に起動時のログを読んでください。

    nginx -t -e stderr 2>&1 | grep 'mechanism enabled'
  • 出力なしで終わるcontentハンドラが、500でなく、空のボディの200で応答する:response body is empty. return NGX_HTTP_INTERNAL_SERVER_ERRORに対するパターンは、メインのリクエストのcontentハンドラが出力なしで終わり、ステータスを設定していないか、nginxが自分では答えない200から299のステータスを設定しているときには、もう一致しません。200未満のステータスと、ほかのハンドラとサブリクエストで出力なしのNginx::HTTP_OKには、今も一致しています。

  • 空のボディで終わる出力のボディフィルタが、接続を閉じずにレスポンスを送る:nginxがこれらのレスポンスを送るので、zero size buf in writerのアラートは、もう出ません。

次のコマンドは、gemのCのソースや、mrubyを組み込むプログラムのCのソースで、下のエントリーが扱う箇所を探します。

grep -rnE 'Data_Wrap_Struct|mrb_data_object_alloc|MRB_SET_INSTANCE_TT|MRB_ARGS_|mruby/ext/io\.h|mrb_alloca|mrb_open|allocf|MRB_NO_PRESYM' src/
  1. すべてのスクリプトを、mruby 4.0.0のmrbcでコンパイルします。このリリースをsh build.shかsh test.shでビルドした後なら、mruby/bin/mrbc -cでスクリプトの構文を確かめられます。mrbc -cはSyntax OKを出力するか、エラーの行を出力して終了ステータス1で終わります。メソッドの本体で定数に代入したときのエラーの行は、<file>:2:3: dynamic constant assignmentです。

    for f in /path/to/scripts/*.rb; do mruby/bin/mrbc -c "$f" || echo "check: $f"; done
  2. スクリプトのうちNginxのクラスを呼ばない部分を、両方のバージョンのmrubyで実行します。mruby 4.0.0のエントリーは、2026年10月4日から10月14日の間に、この方法で測りました。同じRubyのスクリプトを2つのmrubyのバイナリで実行しました。1つはngx_mruby 2.xに同梱のmruby 3.3.0から、もう1つは、mruby 4.0.0からビルドしたものです。どちらもUbuntu 22.04とgcc 11を使うDockerのコンテナの中でビルドし、mrubyのdefaultのgemboxか、各バージョンのngx_mrubyのビルドのコアのgemを使いました。コアのgemは、3.3.0ではfull-coreとiij/mruby-dir、4.0.0ではNGX_MRUBY_CORE_GEMSです。「ngx_mrubyのテストスイートでもこの変化が出ました」と書いたエントリーは、ngx_mruby自身のレスポンスかログの行も変わったものです。同じことをするには、各バージョンのmruby/ディレクトリを別々のディレクトリにコピーし、それぞれでrake MRUBY_CONFIG=defaultを実行して、両方のbin/mrubyでスクリプトを実行します。rakeはmruby/buildにビルドし、そこにはモジュールのビルドが使うmrubyがあります。そのため、このrakeをngx_mrubyのチェックアウトのmruby/で実行しないでください。

  3. テスト用のサーバに、このリリースのモジュールと設定を入れて動かし、各ハンドラに届くリクエストを送ります。そして、error.logで、NoMethodError、TypeError、ArgumentError、SyntaxErrorを含むmrb_run failedの行を探します。

次のものは同じ方法で確かめて変わっていなかったので、確かめる必要はありません。String#sizeと#lengthはバイトを数えます。mruby-encodingのgemはデフォルトのビルドに入っていません。詳しくはコアのgemにあります。ほかに、引数の数が違うときのArgumentError、Exception#message、Struct#inspect、Float#to_s、Integerの除算と剰余、mruby-sleepのKernel#sleepと#usleepも変わっていません。