2024.12.24
ビジネスが急速に変化する現代は「OODAサイクル」と親和性が高い 流通卸売業界を取り巻く5つの課題と打開策
Rackミドルウェア入門のためのRackミドルウェア(全1記事)
リンクをコピー
記事をブックマーク
塩井美咲氏(以下、塩井):すみません、なんの脈絡もなくRackの話をしようとしています。他意はありません。
(会場笑)
先にお伝えしておきたいことがあって、今回、発表資料の枚数がだいたい60枚ぐらいあります。私はもともとすごく早口なんですけど、今日はちょっと自分の限界に挑戦してみたいと思っています(笑)。よろしくお願いします。詳細な資料についてはのちほど共有します。
私はしおいといいます。普段はこの辺り(Asakusa.rb、Tama.rb、Fukuoka.rb)のコミュニティにいます。最近気になるライブラリは「Rack」。ということで、今日はそのお話をします。
さっそく内容に入りますが、要はRackミドルウェアを作りましたというお話をしたいと思っています。なぜ作ったのか、どんなものができたのか、そしてその実装と、あとはそれが実際に役に立ったのかについてお話しします。
最初は目的と成果物について。まずイメージ図を見ていただきたいのですが。これはよくあるRackの機構を表したイメージ図で、一番下の赤い枠に囲まれているものがRackアプリケーション。それを取り囲むようにして、Rackミドルウェアが入れ子状に積み重なっている。それぞれのRackミドルウェアが、それぞれの機能を追加しているイメージだと思ってもらえるといいかと思います。この入れ子構造が、全体で1つのWebアプリケーションになっています。
Webアプリケーションにリクエストを送信したあと、レスポンスが返ってくると思うんですが、Rackミドルウェアによってはこのときにレスポンスヘッダに変更を加えている場合があります。このRackミドルウェアを通す前後で、レスポンスヘッダにどういった変更があったのかを調べたいと考えました。例えば、このBのミドルウェアを通すときに、どんな変更が加わったのか、などです。
そこで作ったのが、レスポンスヘッダの変化をターミナルに表示するためのRackミドルウェア「TraceHeader」です。使い方は、これはRailsの場合ですが、insert_beforeを使って調べてみたいミドルウェアの直前に追加します。そうすると、アプリケーションにアクセスが発生するたびに、レスポンスヘッダの差分がターミナルに表示されます。
では、実装について触れていきます。前提をまず共有したいんですけれども、RackアプリケーションとRackミドルウェアは、共通の規格に沿って実装されています。その規格とは、実行環境を引数にとるインスタンスメソッド「call」を実装していることです。
Rackアプリケーションの場合、このcallメソッドが呼ばれたときに、ステータスコード、ヘッダ、ボディの3つの要素を持つ配列を返します。
Rackミドルウェアの場合は、一般的に@appというインスタンス変数を持っていて、この中には、自分の内側にあるクラスのインスタンスが入っています。callメソッドが呼ばれると、このcallメソッドの中で、(@appに代入されている)内側のクラスのインスタンスに対して、さらにcallを呼ぶ、ということをやっています。
もう少し詳しく見ていくと、インスタンス化するときに、自分の内側にあるクラスのインスタンスを引数の中に入れて、それがそのまま@appの中に代入されています。callメソッドが呼ばれると、この内側のクラスのインスタンスに対して連鎖的にcallが呼ばれていき、最終的に一番内側のRackアプリケーションにたどり着いて、ステータスコード、ヘッダ、ボディの配列を返します。
ここからわかることは、Rackミドルウェアは共通の規格に沿って実装されているので、どのミドルウェアに対してどの時点でcallを呼んだとしても、最終的に必ず「ステータスコード 、ヘッダ、ボディ」という同じ形の配列が返ってくるということです。
そこで立てた仮説です。対象のミドルウェア、今回の例はBのミドルウェアですが、Bのミドルウェアとその直前に呼ばれているCのミドルウェアに対してそれぞれcallメソッドを呼べば、それぞれのcallメソッド呼び出しに対して「ステータスコード 、ヘッダ、ボディ」の形の配列が返ってくるはずなので、その配列同士を比較することによって差分が取れるのではないか、と考えました。
ただ、これにはちょっと課題があります。Webアプリケーションは、リクエストが来たときに一番外側のミドルウェアから順番にcallメソッドが呼ばれていき、一番内側ののRackアプリケーションまで一気に到達してレスポンスを返すため、途中で処理を挟むことができません。つまり、BのミドルウェアとCミドルウェアの間で処理を止められないということです。
そこで、TracePointです。TracePointの詳細については割愛しますが、簡単に説明すると、プログラムを実行しているときに起こるイベント、例えばメソッドの呼び出しやメソッドのリターンなどといったタイミングに、いろいろな処理を挟み込めるRubyの標準ライブラリです。
では実際に、TraceHeaderの実装について紹介します。まずは内部の構造についてです。2つのクラスと、1つのモジュールを実装しています。
まずクラスのほうには、TraceHeaderクラスとResultクラスがあります。
TraceHeaderクラスでは、TracePointを使用して、自分よりも内側にあるすべてのミドルウェアの情報を収集します。TraceHeaderミドルウェア自身はアプリケーションに変更を加えないので、自分よりも後に実行されるミドルウェアに対して、自分の1つ前のミドルウェアの返り値をそのまま渡します。これは後続の処理のために必要な処理です。
Resultクラスは、集めた情報の中から、ターゲットのミドルウェアとその直前に実行されているミドルウェアのインスタンスだけを抽出して、それぞれcallを実行して返り値を比較し、その差分を返します。
Describableモジュールについてはターミナル出力時の文字列の体裁を整えているだけなので、詳細は割愛します。
処理の流れは、この図のようなイメージです。TraceHeaderミドルウェアのインスタンスに対してcallが呼ばれると、このcallメソッドの中で、ターゲットのミドルウェアに対するcallメソッドの呼び出しを行います。
この処理をフックにしてTracePointが実行されます。これによって、自分よりも前に実行されているすべてのミドルウェアの情報を収集し、その中から調べたいミドルウェアのインスタンスだけを抽出、callを呼んで返り値を比較して差分を取得する、と。取得した差分については、outputメソッドで、ターミナルに出力するということをやっています。
ターゲットのミドルウェアの返り値については、そのまま後続のミドルウェアに渡します。
ということで、コードはこんな感じです。まず、7行目でDescribable#outputメソッドを使用するためにモジュールをincludeしています。
9行目からのinitializeメソッドの中身は図のようになっていて、まずターゲットのミドルウェアが入っているインスタンス変数@appと、TracePointの実行結果を格納する空の配列が@datasとして定義されています。また、ターゲットのミドルウェアの返り値を格納するための変数@fixed_appが定義されていますが、まだこの時点ではnilです。
15行目からのcallメソッドの中は図のようになっています。
まず16行目を見てもらいたいのですが、ここでtracerというメソッドが登場しています。このtracerの中身は、TracePointのインスタンスです。このtracerに対して呼んでいるenableメソッドは、ブロックの中で@app.callを実行しています。ここでは、この処理をフックにしてTracePointを実行する、ということをやっています。
TracePointを実行した結果は、resultというメソッドを通じてターミナルに表示するためのoutputメソッドに渡します(17行目)。最終的にターゲットのミドルウェアの返り値は、そのまま後続のミドルウェアに渡ります(18行目)。
30行目からがtracerメソッドの詳細になるのですが、まず31行目でTracePointをインスタンス化しています。このとき、引数に:callと:returnというシンボルをそれぞれ入れています。これは「メソッドの呼び出しとリターンのとき、このブロックの中の処理が実行される」ということを意味しています。
具体的にはどんな処理が実行されるのかというと、まずif文で条件を絞っています。最初の条件は、「実行されているイベントが、Rackミドルウェアのインスタンスに対するcallメソッドの呼び出しあるいはリターンかどうか」です。
さらに次の行で「実行されているイベントが、まだ処理されていないRackミドルウェアに対するcallメソッドの呼び出しであるかどうか」で条件を絞っています。条件を満たした場合は、@datasという空の配列の中に、実行中の各ミドルウェアのクラス、インスタンス、実行環境を保存しています。
ここが重要な部分なのですが、まず①の部分、tp.selfというのは、callメソッドが呼ばれているオブジェクト、すなわちこのミドルウェアのインスタンスのことです。②の部分、tp.binding.local_variable_getは、このcallメソッドが呼ばれた時点の引数envを指しています。
留意点としては、このときの各ミドルウェアのインスタンスと引数のenvは、「今実行しているTraceHeaderミドルウェア」と「ターゲットのミドルウェア」、「ターゲットの直前のミドルウェア」、すべてのミドルウェアで同じものを指しています。
このインスタンスと引数envは、callメソッドによって値が変更されることがあります。同じオブジェクトに対して何度もcallメソッドを呼んでしまうと、本来の正しい返り値を渡せなくなり、アプリケーション自体が壊れてしまう可能性があります。
ですので回避策として、今回はActiveSupport#deep_dupを使ってディープコピーを行なっています。こうすることによって、TraceHeaderのミドルウェア内で使われるオブジェクトは、本来のアプリケーションの実行とは無関係のオブジェクトとして値を取得できます。
あとは、最後の条件になりますが、「実行中のイベントがcallメソッドのリターンである場合」、ターゲットのミドルウェアの返り値を@fixed_appに代入しています。
また、ここでresultというメソッドが実装されています。これは、配列の差分を取得するResultクラスをインスタンス化したものです。インスタンス化するとき、引数として「ターゲットのミドルウェアのインスタンス(@app)」と、先ほどの「TracePointの実行結果を格納した配列(@datas)」を渡しています。
ということで、次はResultクラスを見てみます。まず、5行目からのinitializeメソッドの中で、ターゲットのミドルウェアのインスタンスと、TracePointの実行結果を確認した配列を引数に取っています。ここからターゲットのミドルウェアの情報と、ターゲットの直前に呼ばれたミドルウェアの情報を抽出できます。
これらをそれぞれ@target_hashと@inner_hashというインスタンス変数の中に入れています。このとき、それぞれのインスタンス変数の中身は図のようなハッシュになっています。ミドルウェアのクラス、インスタンス、実行時の環境を表す情報が入っています。
この情報を元にしてそれぞれに対してcallメソッドを呼べます。そうするとそれぞれ返り値の配列、つまり「ステータスコード、ヘッダ、ボディ」の配列を取得できます。今回はヘッダの部分だけほしいので、配列の二番目の要素であるヘッダを取得するためインデックス[1]を渡すことによって、ターゲットのヘッダと、直前のヘッダを取得できます。
あとは、22行目でミドルウェアを通した前後で共通のヘッダ名を抽出、次に26行目で新しく追加されたヘッダ名を抽出、その後30行目で値が書き換わったヘッダを抽出し、最後に47行目でヘッダ名からヘッダの値を取得しています。
取得した値を元に、新しく追加されたヘッダを返すnew_headersメソッドと、値が書き換わったヘッダを返すchanged_headersメソッドとして実装しています。そしてそれぞれをoutputメソッドに渡してでターミナルを表示しています。
ということで、これまで処理の流れをおさらいします。
TraceHeaderミドルウェアのインスタンスに対してcallが呼ばれると、callメソッドの中でターゲットのミドルウェアに対してcallを実行し、TracePointを実行。その後ターゲットのミドルウェアのインスタンスに対してcallを呼んで差分を取得。最後にターミナルに表示をしつつ、ターゲットのミドルウェアの返り値は後続のミドルウェアに渡す、ということをやっています。
おつかれさまでした。
(会場拍手)
実はまだ続きがあって(笑)、有効性の検証をしたいと思っています。
今回はActionDispatch::Cookiesミドルウェアを通して、新しく追加されたヘッダをターミナルに表示できるか検証してみたいと思います。検証環境としては、Scaffoldしただけの簡単なアプリケーションを、サーバを立ち上げて、適当なページにアクセスする、という方法を取りました。
その結果……無事にターミナルに表示されました。
参加者:お〜。
塩井:ちょっと見にくいと思うのですが、ちゃんとターミナルのスクリーンショットをを撮ってきました。
(会場拍手)
ありがとうございます(笑)。もう少し拡大してみると、これがミドルウェア名、そして新しいヘッダの名前と値、値が書き換わったヘッダの名前と値が表示されているのがわかります。
結果から得られたのは、ーーいろいろな発見があったんですがーー追加されたヘッダがSet-Cookieという名前であること、値は図のようなかたちになっているということ、デフォルトでpath属性がルートに設定されているということ、デフォルトでHttpOnlyになっていることなどなど。
もうちょっと調べたいと思ったのは、ETagはどのミドルウェアを通しても書き換わることがわかり、この辺りについては自分の理解が及んでいないので要勉強だなと思っています。
ところで、今回発表資料を作るに当たって、Fukuoka.rbという福岡のRubyコミュニティにレビューをお願いしました。そうしたところ、Fukuoka.rbのうづらさん(@udzura)からこのような感想をいただいくことができました。TraceHeaderの有効性、あったと言ってもいいじゃないでしょうか? みなさまからのプルリクエスト、いつでもお待ちしております。
ということで、今回ご協力いただいたRubyistのみなさんに感謝しつつ、ご清聴ありがとうございました。
(会場拍手)
司会者:どうもありがとうございました。あと何分ありますか?
スタッフ:3分あります。
司会者:あと3分? 余裕じゃないですか。
塩井:やった!(笑)。
司会者:まだもう1回、回せますよ。
(会場笑)
なにかしゃべり足りてない?
塩井:しゃべり足りていないこと? 早口ですみませんでした、本当に……。
司会者:じゃあ質問にタイムいきましょうか。なにか今の中でここが難しかったようなものがあれば。
質問者1:興味本位ですが、これを実装するのにどれぐらい時間かかりました?
塩井:だいたい毎週火曜日にAsakusa.rbで取り組む、という感じだったので、そのペースで言うと、2ヶ月から3ヶ月弱ぐらいかかっています。ただ、最後のほうはかなり根を詰めてやりました。
質問者1:毎週2時間で×8みたいな。8じゃなくて10とかっていう感じですか?
塩井:そうですね。そちらにいらっしゃるようさん(@youchan)をはじめAsakusa.rbの人々がたくさん相談にのって下さったおかげで何とかなったという感じですね。この(Special Thanksの)方々のおかげです(笑)。
司会者:コミュニティの力が。
塩井:本当に。
司会者:あっ、質問、もう1人。
質問者2:今日はありがとうございます。最初作ったときとミドルウェアの名前が違ったと思うんですけれども、名前がかっこいい名前に変わった経緯とかお聞きしたいなと思いまして。
塩井:(笑)。これはちょっとコンテキストが難しい話なんですけど、これはもともと、Asakusa.rbで実装していたとき、その場にいた方が「(名前は)header_miruo_kunでいいんじゃない?」と仰っていたんですね。header_miruo_kun……私も途中までそう呼んでいて、別にそのままでもよかったんですが、なんか今回の発表に際して、「header_miruo_kunクラス」って連呼したくなかった……以上です。
(会場笑)
司会者:ありがとうございました。ほかになにか気になることや質問はありますか? 大丈夫ですか。じゃあ以上にしたいと思います。塩井さん、どうもありがとうございました。
(会場拍手)
関連タグ:
2025.01.09
マッキンゼーのマネージャーが「資料を作る前」に準備する すべてのアウトプットを支える論理的なフレームワーク
2025.01.15
若手がごろごろ辞める会社で「給料を5万円アップ」するも効果なし… 従業員のモチベーションを上げるために必要なことは何か
2025.01.10
プレゼンで突っ込まれそうなポイントの事前準備術 マッキンゼー流、顧客や上司の「意思決定」を加速させる工夫
2025.01.07
資料は3日前に完成 「伝え方」で差がつく、マッキンゼー流プレゼン準備術
2025.01.08
職場にいる「嫌われた上司」がたどる末路 よくあるダメな嫌われ方・良い嫌われ方の違いとは
2025.01.07
1月から始めたい「日記」を書く習慣 ビジネスパーソンにおすすめな3つの理由
2025.01.14
コンサルが「理由は3つあります」と前置きする理由 マッキンゼー流、プレゼンの質を向上させる具体的Tips
2025.01.10
職場にいる「できる上司」と「できない上司」の違いとは 優秀な人が辞めることも…マネジメントのNGパターン
2025.01.09
記憶力に自信がない人におすすめな「メモ」の取り方 無理に覚えようとせず、精神的にも楽になる仕事術
2025.01.08
どんなに説明しても話が伝わらない“マトリョーシカ現象”とは? マッキンゼー流、メッセージが明確になる構造的アプローチ
安野たかひろ氏・AIプロジェクト「デジタル民主主義2030」立ち上げ会見
2025.01.16 - 2025.01.16
国際コーチング連盟認定のプロフェッショナルコーチ”あべき光司”先生新刊『リーダーのためのコーチングがイチからわかる本』発売記念【オンラインイベント】
2024.12.09 - 2024.12.09
NEXT Innovation Summit 2024 in Autumn特別提供コンテンツ
2024.12.24 - 2024.12.24
プレゼンが上手くなる!5つのポイント|話し方のプロ・資料のプロが解説【カエカ 千葉様】
2024.08.31 - 2024.08.31
育て方改革第2弾!若手をつぶす等級制度、若手を育てる等級制度~等級設定のポイントから育成計画策定まで~
2024.12.18 - 2024.12.18