YOUTRUST で本当に起こった不具合の話 〜問題編〜

こんにちは、YOUTRUST のやまでぃ(YOUTRUST/X)です。

最近のわたくしごとですが

先日、1年振りにグランドピアノで Summer を弾いてみました。

www.youtube.com

会場は代々木のピアノスタジオ マイレッスンさん

piano.my-lesson.jp

1.5年前に友達から電子ピアノを譲ってもらったのをきっかけに、隙あらば家で弾いて練習しています。

自分で好きな曲が弾けて楽しいし、良い気分転換にもなるし、気がついたら風呂も沸いているので結構気に入っている趣味です。

今日はなんの話?

先日 YOUTRUST で実際に発生してしまっていた不具合についてのお話です。

まずは以下の擬似コードをご覧ください。

class User < ActiveRecord::Base
  has_one :hoge
end

class Hoge < ActiveRecord::Base
  belongs_to :user
end
class HogeController < ApplicationController
  before_action :authenticate_user! # 認証処理

  def create
    # `current_user` → 認証された User インスタンス
    result = HogeQuery.run(operation_user: current_user)
    return head :ok if result

    use_case = CreateHogeUseCase.run(operation_user: current_user)

    if use_case.success?
      head :ok
    else
      head :bad_request
    end
  end
end
class HogeQuery
  include QueryRunnable # .run → .new.run みたいなクラスメソッドを定義している

  def initialize(operation_user:)
    @operation_user = operation_user
  end

  def run
    Hoge.where(user: @operation_user).exists?
  end
end
class CreateHogeUseCase
  include UseCase::Base # .run → .new.tap(&:run) みたいなクラスメソッドを定義している

  attr_reader :hoge

  def initialize(operation_user:)
    @operation_user = operation_user
  end

  def run
    # 排他制御のためのロック
    @operation_user.with_lock do
      command = CreateHogeCommand.run(user: @operation_user)

      if command.success?
        @hoge = command.hoge
      else
        errors.add(:base, 'failed')
        raise ActiveRecord::Rollback
      end
    end
  end
end
class CreateHogeCommand
  include Command::Base

  attr_reader :hoge

  validate :validate_user

  def initialize(user:)
    @user = user
  end

  def run
    @hoge = Hoge.create!(user: @user)
  end

  private

  def validate_user
    if Hoge.where(user: @user).exists?
      errors.add(:base, 'invalid')
    end
  end
end
module Command::Base
  extend ActiveSupport::Concern
  include ActiveModel::Model

  module ClassMethods
    def run(**args)
      new(**args).tap { |command| command.valid? && command.run }
    end
  end

  def success?
    errors.none?
  end
end

説明用の超簡素化されたコードなのですが、「Hoge レコードがなければ作る」というとってもシンプルなものになっています。

こちらはご覧の通り User は Hoge を一つのみ所有できることを想定しています。

ですが、こちらのコードには不具合が潜んでおり、同じ User に対して複数の Hoge レコードが作成されてしまう可能性があります。(もちろんデータベースのテーブルに UNIQUE インデックスを貼っていたらエラーが発生して作成はされませんが、今回はインデックスがないと仮定してください。)

僕は最初、「完璧なロジックだ。複数作成される訳がない。データベース側の不具合なのでは?」と少し本気で思っていました。(ごめんなさい)

▼コードを読み解くための参考リンク

github.com

tech.youtrust.co.jp

tech.youtrust.co.jp

不具合の詳細の解説

…をしても良いのですが、折角なので読者の方にも少し考えていただいて、解説は次回の記事にて行いたいと思います。

  • 「原因が分かった!早く答えを言いたい!」
  • 「全然分からなくて夜も眠れない!次回の記事まで待てない!」
  • 「YOUTRUST のエンジニアリングに興味がある!話を聞きたい!」

と思われた方は是非ともわたくしとカジュアル面談 しましょう!

もちろんそのまま下記募集に応募くださっても大丈夫です!

herp.careers

それでは今回は以上となります。

次回の解説編にご期待ください!