Goのデータベースドライバが壊れた接続を消す方法

database/sql が壊れた接続をコネクションプールから削除するための、昔からある流れは、Query()やExec()が driver.ErrBadConn を返すと、database/sqlはその接続を閉じてリトライするというものです。 ErrBadConnには接続が壊れたことを示す役割とリトライするべきことを示す役割を兼ねているので、たとえばクエリを投げた後に結果を受信しようとReadしてエラーになった場合はErrBadConnを返すことができず、次にその接続が使われるときにErrBadConnを返すことになりました。

その後、利用し終わった接続を次回再利用するまでの間に状態のリセットなどを可能にしようということで SessionResetter が導入されました。このインターフェイスがErrBadConnを返すことで、次回利用を待つことなく接続を閉じれるようになりました。

SessionResetterが導入された当時は接続をプールに戻してから次にプールから取り出すまでの間に非同期的に呼ばれていました。例えばMySQLの wait_timeout のように時間によって接続が壊れる場合、プールに戻してResetSession()が呼ばれた後に再利用までしばらくの間があき、その間に接続が壊れるという場合があったので、クエリ実行時のErrBadConnはまだ重要でした。

しかし、非同期的な ResetSession() の呼び出しが database/sql のプール管理に余計な複雑さを招いていたので、Go 1.15で呼び出しタイミングが変更されてプールから接続を取り出したときに呼ばれるようになりました。これでクエリの最初にErrBadConnを返さないといけないケースは大分減りました。 また、Go 1.15では driver.Validator というインターフェイスも追加され、 IsValid() が false を返したら接続が壊れたことを伝えられるようになりました。IsValid()はクエリ実行後、コネクションをプールに戻す前に呼び出されるので、クエリ中にコネクションが壊れた場合にそれを database/sql にすぐに伝えられるようになりました。

このため、クエリを実行する Exec(Contect) や Query(Context) が ErrBadConn を返すのは、クエリを実行する過程でエラーが起こったけれどもリトライしても絶対に安全だと言える限られたケースだけになりました。

さらに、Go 1.18からは database/sql が ErrBadConn をチェックするときに err == ErrBadConn ではなく errors.Is(err, ErrBadConn) を使うようになりました。なので独自のエラー型を返す場合でも、例えば Is(ErrBadConn) が true を返すようにすれば ErrBadConn を返すのと同じくリトライをさせることができます。しかし安全にリトライできない場合は接続が壊れたことは IsValid() で示すべきですし、リトライする場合はせっかく独自のエラーを返してもそのエラーは捨てられてしまうので、手間をかけてErrBadConnと内容のある独自エラーを組み合わせる必要があるのかは疑問です。

まとめ

ドライバーを実装するとき、接続を壊す方法は3つあります。

  • Validator -- コネクションをプールに戻すときに呼ばれるので、 IsValid() で false を返せばその接続は再利用せずに捨てる。
  • Query() 等のエラーで ErrBadConn を返す。 -- 接続が壊れているだけでなく、(トランザクション外なら)安全にリトライできることをdatabase/sqlに教える場合に使う。
  • SessionResetter -- コネクションがプールから取り出されるときに呼ばれるので、アイドル中に接続が壊れたことを検知できたのであれば ErrBadConn を返す。
このブログに乗せているコードは引用を除き CC0 1.0 で提供します。