$30 off During Our Annual Pro Sale. View Details »

タイムアウトにご用心 / Timeout might break application state

Fumiaki MATSUSHIMA
October 10, 2021
2.1k

タイムアウトにご用心 / Timeout might break application state

https://kaigionrails.doorkeeper.jp/events/127339

Kaigi on Rails _2021_ new LT 発表資料

Fumiaki MATSUSHIMA

October 10, 2021
Tweet

Transcript

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

    View Slide

  2. 松島 史秋
    GitHub, Twitter
    @mtsmfm

    View Slide

  3. https://ninirb.github.io

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  11. 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

    View Slide

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

    View Slide

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

    View Slide

  14. 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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  22. デフォルトやばいのでは?
    - 警告だけでも提案したが通らなかった
    - rack-timeout gem が歴史的に TERM するわけではな

    - 危ないという説明も既にある (doc/risks.md)
    - Puma のデフォルトは Clustered mode じゃない

    View Slide

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

    - Middleware っぽいところでやってると特に危ない
    - rack-timeout は term_on_timeout を必ず true に
    - 具体的な再現コードなどは
    https://zenn.dev/mtsmfm/articles/d55d739104e87c

    View Slide

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

    View Slide