Slide 1

Slide 1 text

@mtsmfm タイムアウトにご用心

Slide 2

Slide 2 text

松島 史秋 GitHub, Twitter @mtsmfm

Slide 3

Slide 3 text

https://ninirb.github.io

Slide 4

Slide 4 text

https://www.meetup.com/ja-JP/GraphQL-Tokyo/

Slide 5

Slide 5 text

伝えたいこと タイムアウトしたプロセスを再利 用してはいけない

Slide 6

Slide 6 text

どんな問題が起きるか - タイムアウト後、アプリケーションの状態が崩壊 する可能性がある

Slide 7

Slide 7 text

User.where(name: "foo").not_deleted /users_count /users?name=foo User.not_deleted.count

Slide 8

Slide 8 text

User.where(name: "foo").not_deleted /users_count 1. リクエストがくる /users?name=foo User.not_deleted.count

Slide 9

Slide 9 text

User.where(name: "foo").not_deleted /users_count User.not_deleted.count 1. リクエストがくる 2. not_deleted scope を組み立 て中にタイムアウトになる /users?name=foo

Slide 10

Slide 10 text

User.where(name: "foo").not_deleted /users_count 1. リクエストがくる 2. not_deleted scope を組み立 て中にタイムアウトになる /users?name=foo 3. 別のリクエストがくる User.not_deleted.count

Slide 11

Slide 11 text

User.where(name: "foo").not_deleted /users_count 1. リクエストがくる 2. not_deleted scope を組み立 て中にタイムアウトになる 4. タイムアウトしたときにスタックされ ていた where が適用される /users?name=foo 3. 別のリクエストがくる User.where(name: "foo").not_deleted.count User.not_deleted.count

Slide 12

Slide 12 text

どんな問題が起きるか - タイムアウト後、アプリケーションの状態が崩壊 する可能性がある - タイムアウトのタイミング次第では、その瞬間の状態が 保持されてしまうケースがある - 例えば、User.count と書いてあるのに、タイムアウト以 後、そのプロセスは User.where(name: "foo").count 相当の処理をする

Slide 13

Slide 13 text

なぜそうなるのか - ensure 中に Thread#raise されるとそこで ensure が全部実行されることなく終わってしま うから

Slide 14

Slide 14 text

class Stack def stack @stack ||= [] end def with(name) stack.push(name) yield stack ensure stack.pop end end stack = Stack.new stack.with('foo') do p stack.stack #=> ["foo"] stack.with('bar') do p stack.stack #=> ["foo", "bar"] end p stack.stack #=> ["foo"] end

Slide 15

Slide 15 text

ここでタイムア ウトしても セーフ class Stack def stack @stack ||= [] end def with(name) stack.push(name) yield stack ensure stack.pop end end

Slide 16

Slide 16 text

ここでタイムア ウトすると pop されない class Stack def stack @stack ||= [] end def with(name) stack.push(name) yield stack ensure stack.pop end end

Slide 17

Slide 17 text

Rails のバグ? - 違う - あらゆるタイミングで Thread#raise されること を考慮するのは厳しいので諦めている - https://github.com/rails/rails/pull/17607#issuecomme nt-70538060 - Scope 以外にも ensure で状態をリセットすると ころはタイムアウトで壊れる可能性有

Slide 18

Slide 18 text

どんなときに踏みやすいか - 自分で Timeout.timeout を書くことはそんなに ない気がする - 注意すべきは gem、特に Web サーバとか Rack middleware とか

Slide 19

Slide 19 text

rack-timeout の罠 - rails new して書かれている Puma にはタイム アウト機構はなく、rack-timeout が事実上デファ クト - Heroku のチュートリアルでも紹介されていたり - デフォルト設定だとまさにやばいケースに該当 する

Slide 20

Slide 20 text

rack-timeout の罠 - term_on_timeout: true にすれば、タイムアウト したら TERM で殺してくれる - これはデフォルトではない - Puma を Clustered mode で動かしていない場合、true にするとサーバが完全に死ぬ

Slide 21

Slide 21 text

https://github.com/sharpstone/rack-timeout/issues/169

Slide 22

Slide 22 text

デフォルトやばいのでは? - 警告だけでも提案したが通らなかった - rack-timeout gem が歴史的に TERM するわけではな い - 危ないという説明も既にある (doc/risks.md) - Puma のデフォルトは Clustered mode じゃない

Slide 23

Slide 23 text

伝えたいこと - タイムアウトしたプロセスを再利用してはいけな い - Middleware っぽいところでやってると特に危ない - rack-timeout は term_on_timeout を必ず true に - 具体的な再現コードなどは https://zenn.dev/mtsmfm/articles/d55d739104e87c

Slide 24

Slide 24 text

Credits Background pattern from Toptal Subtle Patterns https://www.toptal.com/designers/subtlepatterns/