Blog

GinのContextを何となく使ったら危険だった

はじめに

GoのWebフレームワークとして著名なものにGinがあります。

GinにおいてHTTPリクエストを取り扱うにはgin.Context型の構造体を取り扱いますが、これをなんとなく使うと危険な使い方をしかねないため、注意が必要というお話です。

何となく書いてたもの

GinでDBアクセスするアプリを書こうとしており、OpenTelemetry(OTEL)を入れるような設定を入れようとしていました。
主な流れを取り出すと以下のようになります。実際はdatabase/sql直接ではなくてORM経由だったり、ファイルが分かれていたりします。

r.ContextWithFallback = trueがOTELのために追加したもので、これはGin公式のOTEL向けサンプルを参考にしたものになります。

https://github.com/gin-gonic/examples/tree/master/otel

現在はREADMEが更新されており、This configuration is necessary for the example to work but may not be ideal for production use. と本番向けではないことが明示されています。

危ないポイント

gin.ContextはGo標準のcontext.Contextインターフェースを実装しているので、context.Contextとして振る舞うことができます。
デフォルトではHTTPリクエストのデータを持たず、ContextWithFallbackオプションを有効にすることでリクエストのデータを引き継ぐ…のですが、原則使うべきではありません

Go標準のcontext.Contextはスレッドセーフであることが期待されますが、gin.Contextはそうではありません。gin.ContextはGin内部でsync.Poolを使ってプーリングされるように実装されており、中身を消して再利用されます。したがって、

  • ハンドラ関数の中でContextを渡され、cancelを待つread処理
    • 上記例だとdatabase/sqlが待つ
  • ginがContextの中身を消去しようとするwrite処理

が競合することになります。これはテスト時にgo test -raceオプションを付けて実行すると、race detectorが反応することで検出することができます。

正しくはgin.Contextの内部にあるRequest.Context()を取り出せば良いです。こちらはnet/httpによって作成されるものであり、再利用されることはありません。
Echoなど他のフレームワークでも同じような実装方法となっています。

リクエストのライフサイクル

上記によりGin特有の問題は解消されますが、そもそもリクエストのContextを引き継いでよいのかという問題も別途存在します。

リクエストのContextはHTTPコネクション切断によりキャンセルされるため、DBに渡すと未コミットのトランザクションがあればロールバックされる可能性があります。
アプリケーションの要件によりますが、コネクションが切れても処理を続行したいという場合には、別のContextを作成する必要があります。

ただしこれではContextに含まれる値も初期化されることになります。
これではContext経由でGinのデータにアクセスする処理、たとえば

  • OTELのmiddlewareでContextに入れたトレースIDをもとにSpanを作成
  • middlewareでContextに入れたリクエストIDをログ出力する

のような機能が使えなくなってしまいます。

このような場合は、キャンセルのみ引き継がないようにします。(要Go 1.21+)

このままだと一切のキャンセル処理がなくなります。安全のため自力でキャンセル処理を入れたい場合、WithoutCancel()してからWithCancel()すればよいでしょう。

これで値は引き継ぎつつ、キャンセル伝搬から切り離す事ができます。

おわりに

Ginのgin.Contextはスレッドセーフではありません。context.Contextインターフェースを実装していることから勘違いしがちなのですが、context.Contextとして扱わないようにしましょう。
またアプリケーション要件によっては、キャンセルの伝搬を止めるような実装が必要となることもあります。
みなさんも注意していただければと思います。

ニフティでは、
さまざまなプロダクトへ挑戦する
エンジニアを絶賛募集中です!
ご興味のある方は以下の採用サイトより
お気軽にご連絡ください!

ニフティに興味をお持ちの方は
キャリア登録をぜひお願いいたします!

connpassでニフティグループに
参加いただくと
イベントの
お知らせが届きます!