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

ゾンビコードは血肉にする🧟

Avatar for fukuc fukuc
September 27, 2025

 ゾンビコードは血肉にする🧟

ゾンビコードに出くわした経験はありますか?
どうしようもないコードに出くわしたときは自分の経験血に変換しちゃいましょう。
ref
- https://blog.willnet.in/entry/2021/12/23/184123
- https://github.com/steveklabnik/request_store
- https://docs.ruby-lang.org/ja/latest/class/Fiber.html

Avatar for fukuc

fukuc

September 27, 2025
Tweet

More Decks by fukuc

Other Decks in Programming

Transcript

  1. 見つけたゾンビ 🧟 tax.rb tax_rule.rb class Tax < ApplicationRecord has_many :tax_rules,

    class_name: 'TaxRule', foreign_key: :tax_id, dependent: :destroy accepts_nested_attributes_for :tax_rules, allow_destroy: true TAX_RATE = 10 REDUCED_TAX_RATE = 8 INCLUSIVE_TAX_RATE = 0 # 税率ルールに更新があった場合にキャッシュをクリアする # このメソッドは、税率ルールの新規作成、更新、削除時に呼び出される def clear_tax_rules_cache RequestStore[:"tax_rules_#{id}"] = nil end end class TaxRule < ApplicationRecord acts_as_paranoid auto_increment_from 1_000 belongs_to :tax, class_name: 'Tax', foreign_key: :tax_id, optional: true after_commit :clear_cache # 税率ルールに更新があった場合、税率テンプレート側のキャッシュをクリアする def clear_cache return if tax.nil? tax.clear_tax_rules_cache end end ゾンビはこちら🧟
  2. なぜゾンビと言えるのか? Rails app EC shop EC admin Puma(app server) 注文リクエストA

    税率ルール変更リクエスト thread pool 3/5 theads EC事業者 顧客A 注文Aスレッド 税率ルール変更スレッド シングルプロセス/マルチスレッド プロセス... メモリ空間などシステムリソースが割り当てられた独立した 実行単位であり、実行中のプログラムを指します。 スレッド... プロセス内の実行単位で、プロセスのメモリ空間を他スレッドと共有 できる。 注文リクエストB 顧客B 注文Bスレッド
  3. RequestStoreとは... RequestStore gem : https://github.com/steveklabnik/request_store RailsでHTTPリクエスト単位でのグローバル変数 を安全に使うためのライブラリ “ the storage

    is local to that request “ スレッドローカルとは、スレッドごとに独立したデータを持つ仕組み。 そのためスレッド間で共有されることはない。 module RequestStore class Middleware def initialize(app) @app = app end def call(env) RequestStore.begin! status, headers, body = @app.call(env) body = Rack::BodyProxy.new(body) do RequestStore.end! RequestStore.clear! end returned = true [status, headers, body] ensure unless returned RequestStore.end! RequestStore.clear! end end end end ライブラリを読み込むと専用の Rackミドルウェアが差し込まれ、リクエスト終了 時にキャッシュが自動クリアされる module RequestStore def self.store Thread.current[:request_store] ||= {} end
  4. 実際のスレッドの状態 Rails app EC shop/EC admin 注文Aスレッド rate: 10 リクエスト完了後(スレッドプールに戻る前)

    実行中のスレッドの状態 税率ルール変更ス レッド 注文Bスレッド 空 rate: 8 Rails app EC shop/EC admin 注文Aスレッド 空 税率ルール変更ス レッド 注文Bスレッド 空 空 DB 注文スレッドAローカルに税率10%をキャッ シュ DB上の税率10% -> 8% 注文スレッドBに変更後の 税率8%をキャッシュ たとえ税率ルール変更後、注文スレッドAのリクエスト が未完了でキャッシュが破棄されていなくても、注文 スレッドBが注文スレッドAのキャッシュを参照するわ けではない。
  5. 見つけたゾンビ 🧟 tax.rb tax_rule.rb class Tax < ApplicationRecord has_many :tax_rules,

    class_name: 'TaxRule', foreign_key: :tax_id, dependent: :destroy accepts_nested_attributes_for :tax_rules, allow_destroy: true TAX_RATE = 10 REDUCED_TAX_RATE = 8 INCLUSIVE_TAX_RATE = 0 # 税率ルールに更新があった場合にキャッシュをクリアする # このメソッドは、税率ルールの新規作成、更新、削除時に呼び出される def clear_tax_rules_cache RequestStore[:"tax_rules_#{id}"] = nil end end class TaxRule < ApplicationRecord acts_as_paranoid auto_increment_from 1_000 belongs_to :tax, class_name: 'Tax', foreign_key: :tax_id, optional: true after_commit :clear_cache # 税率ルールに更新があった場合、税率テンプレート側のキャッシュをクリアする def clear_cache return if tax.nil? tax.clear_tax_rules_cache end end ゾンビはこちら🧟 => 別スレッドのローカルに税率はキャッシュされるし、各リクエスト完了後キャッシュは専用のミドル ウェアの処理により自動で破棄されるため
  6. 今回のゾンビコードの辛いところ (;´Д`) • RequestStoreは内部でThread.currentを使用している。 Thread.current[:hoge]で取得できる値は実はスレッドローカル変数ではなく、 ファイバーローカル変数 ◦ Thread#[]=で設定した値はファイバーを切り替えると参照できなくなる。 ファイバーAで設定した値はファイバーBでは参照できない。 •

    アプリケーション内の他のライブラリでファイバーを使用している可能性も考え られ、そちらでファイバーの切り替えが行われている場合、期待する値の参照 が困難になる可能性がある • ゾンビコードかもしれないが、Fiber切り替えによる予期せぬ動作を防ぐ安全 装置として機能してしまっている可能性もある ・`Thread.current[:hoge]`はスレッドローカル変数を参照していると思いきや実際はファイバーローカル変数だった:https://blog.willnet.in/entry/2021/12/23/184123 ・class Fiber: https://docs.ruby-lang.org/ja/latest/class/Fiber.html
  7. • ライブラリの使い方を正しく理解した上で使用すること ◦ 内部実装を覗いてみて何が行われるのかを正しく理解する ◦ よくわかっていないものは使わない方が良い • 「機能していない処理 = 利益につながっていないコード」のメンテナンスコストの

    発生を頭に入れておくこと ゾンビコードを生まないために ⚔ 一朝一夕で優れたエンジニアになることは難しいので、地道な研鑽が明日 の自分と周りを助けることに繋がる