go-sql-driverにcontext.Context
対応するプルリクエスト
go-sql-driver/mysql#608
を送って取り込んでもらいました!!
現時点ではまだ正式リリースされていませんが、次のリリース(version 1.4)から使えるようにはずです。
masterブランチではすでに使えるようになっているので、引き続き人柱募集中です。
コネクションプーリングを実装していて、自分も「context.Context
サポートしたい!」というかたのために、
実装の概要をメモとして残しておきます。
おおまかな仕組み
- 「contextの監視のみを行うgoroutine(以下、watcher goroutine)」をあらかじめ起動しておく
- 「やりたい処理を実際に実行するgoroutine(以下、executor goritune)」とchannelを経由して
context.Context
をやり取りする
watcher goroutineがこの実装で一番重要な部分です。
watcher goroutine の実装
一番重要な watcher goroutine の実装例から見てみましょう (実際には細かい最適化などが入るため、マージされたコードとは異なります)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
|
watcher
, finished
, closech
の3つの channel を経由して
executor goroutine と通信を行います。
executor goroutine の実装
executor goritune の実装例は以下のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
mc.doSomthing()
が実際に行いたい処理なのですが、これに ctx
を渡していないのがポイントです。
watcher goroutine に ctx
の監視を任せているので、executor goroutine 側では監視しなくてもいいのです。
executor goritune と watcher goroutine 間の通信
executor goritune と watcher goroutine 間の通信は主に
watcher
channel と finished
channel が担当します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
キャンセルの実装
context.Context
がキャンセルされたときに、executor goroutineを強制終了する処理は、
コネクションを強制的に Close
することで行っています。
ちょっと強引な気はしますが、キャンセルされるような状況に陥った時点で正常な通信なんて出来ていないので、
まあいいかと、このような実装になっています。
もっと賢いキャンセルの方法があるかもしれませんが、キャンセルされない場合のほうが圧倒的に多いので、
余計なオーバーヘッドは避けたいというのもあります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
|
これらの関数は (executor|watcher) 両方の goroutine から呼ばれる可能性があるため、 以下の二点が非常に重要です。
- cancelでは コネクションを実際にCloseする前 にエラー内容を記録する
- これが逆だと executor がキャンセルを見逃してしまう場合がある
- sync package や sync/atomic package を使って goroutine-safe に書く
FAQ(よくあるであろう質問)
こっちの実装の方がいいんじゃないの?と実装中に自問自答した内容を FAQと称して残しておきます。
close(watcher)していないのはなぜ?
最初は watcher goroutine の実装は以下のようになっていて、
close(watcher)
で watcher goroutine を終了させようかと考えてました。
1 2 3 |
|
しかしこの実装では mc.watcher <- ctx
のところで close
されていないかを毎回確認する必要があり、
channelを使うメリットが薄れてしまうので廃案となりました。
close(finished)していないのはなぜ?
監視の終了に close(finished)
を使うという案も考えました。
しかしこの実装が廃案になったのには大きく二つの理由があります。
一つ目は「監視の終了は同期していなければならない」からです。
close(finished)
を使った方法では executor goroutine が監視の終了を通知しても、
watcher goroutine が実際に監視を終了するタイミングは goroutine スケジューラの気分次第で遅れてしまう可能性があります。
すると watcher goroutine がクエリキャンセルしたときには、 executor goroutine では既に次のクエリが実行さており、
間違ったクエリをキャンセルしてしまうという事故が起こりえます。
finished <- struct{}{}
を使った方法ならこれは起こりません。
executor goroutine が監視の終了を通知するのと、
watcher goroutine が実際に監視を終了するのとが同期しているので、
確実にキャンセルしたいクエリだけをキャンセルできます。
実際、PostgreSQLのGo driver実装は、最初 close(finished)
で実装されていたものが、
finished <- struct{}{}
に置き換えられています(実装時には知らなくて、この記事を書いているときに知った)。
二つ目は「channelの再利用ができない」という理由です。
一度 close
した channel は open
することはできないので、新規に channel を作る必要があります。
これにはメモリ確保が必要になるので、パフォーマンス面で不利になります。
QueryContextの中でfinishを直接呼んでいないのはなぜ?
QueryContext の実装をよく見てみると rows.finish = mc.finish
しているだけで、
QueryContext の中では finish
を呼んでいません。
これはなぜかというと QueryContext
の実行が終了した後、
rows の読み取り中に、context.Context
がキャンセルされる場合があるからです。
たとえば以下のコードで、rows.Err()
は context.Canceled
になっているべきです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
この挙動は net/http を参考にしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
|
BeginTxの中ではfinishを直接呼んでいるのはなぜ?
BeginTx
では finish()
を呼んでいます。
BeginTx
終了後にトランザクションがキャンセルされる場合を考えると、
QueryContext
と同様に tx.finish = mc.finish
となりそうですが、そうはなっていません。
これは database/sql が代わりに監視してくれていて、
context.Context
がキャンセルされると自動的にRollbackしてくれるからです。
実は rows にも同様の監視処理が入っているので勝手に Close
してくれます。
しかし、packetの読み書きを context.Context
対応にする必要があり、
実装コスト・実行コストが大きそうだったので手を付けていません。
まとめ
executor goroutine と watcher goroutine を使った context.Context
対応の実装例を紹介しました。