ActiveRecord::Baseを継承したクラスのインスタンスがGCされないことがあった

irb(main):004:0> RUBY_VERSION
=> "2.4.4"
irb(main):006:0> Rails.version
=> "4.2.11.1"

特定のユーザ達になんらかの処理を行うという、よくあるバッチ処理でメモリリークが起きたということを書く。結論を書くとインスタンス変数をやめたらGCしてくれるようになった。

事件が起きたのは、OAuth2クライアントがAPIコールするだけのバッチ処理。当時発生していたクラスの構成は下記の通り。

class User < ActiveRecord::Base
  has_one :the_service_account

  def run
    the_service_account.run
  end
end
def TheServiceAccount < ActiveRecord::Base
  belongs_to :user

  def run
    api_client.run
  end

  private def api_client
    @api_client ||= ApiClient.new(user)
  end
end
class ApiClient
  def initialize(user)
    self.user = user
    @oauth2_token = OAuth2::AccessToken.new(
      build_client,
      user.the_service_account.access_token,
      user.the_service_account.refresh_token,
      user.the_service_account.expires_at,
    )
  end

  def run
    call_api
  end
end

というように、外部サービスへのアクセスに使うトークンを管理する TheServiceAccount、外部サービスのAPIを呼ぶための ApiClientがある。

冒頭に書いたように、このUserモデルに対してfind_eachすると、メモリ使用量が線形に増えていった。

User.find_each do |user|
  user.run
end

find_eachしているのにメモリが増え続けているということは、GCされるべきオブジェクトが残っているのだろうと考え、下記のようにインスタンス変数からローカル変数に変更したらGCされるようになった。

class ApiClient
  def initialize(user)
    self.user = user
  end

  def run
    oauth2_token = OAuth2::AccessToken.new(
      build_client,
      user.the_service_account.access_token,
      user.the_service_account.refresh_token,
      user.the_service_account.expires_at,
    )
    call_api(oauth2_token)
  end
end

find_eachを使うとメモリリークを起こす!手動でGC.startをするべしって記事をたまに見るんだけど、モデルの実装に原因があることもあるのだ。