한 레코드에 동시 접속 막자!! with_lock

상황

war-battle-view 에는 상황마다 다른 버튼이 나오고 있다. 그리고 이 상황이란건 프론트의 정보가 백엔드로 갔을 때 서버에서 이를 처리하고 다시 프론트로 넘겨줌으로써 수정이된다.

이렇게 상황이 변경된다면 유저는 자신의 상황에 맞는 버튼이 띄어진다.

문제는, 어떤 유저가 상황을 바꾸기 위해 버튼을 눌렀는데 이를 백엔드에서 처리하기 전에 프론트의 다른 유저가 버튼을 누르는 상황이다.

우리의 경우 예를 들면

  1. A의 길드원이 war-battle 을 신청한다. 이 정보가 액션케이블을 통해 브로드캐스팅되서 전쟁에 참여중인 모든 유저에게 전달된다.

  2. B의 길드원 중 한명은 1에서 날린 정보를 받기 전에( 즉 상황이 변하기 전에) 버튼을 누른다.

  3. 이렇게 되면 A, B 길드 모두 서로에게 Battle 을 요청한 상태가 된다.

해결

버튼을 누르면 그 동작의 결과는 상당히 빠르게 모든 길드원에게 전달된다.

하지만 아무리 빠르게 전달이 된다한들 백엔드에서 해결을 해주지 않으면 문제는 어떻게든 다시 발생할 수 밖에 없다.

문제의 해결 방법을 알기 전에 코드를 먼저 보자.

def create_dual_match_for(user, rule_id, target_score)
  match =
    Match.create(
      match_type: 'dual',
      status: 'pending',
      rule_id: rule_id,
      target_score: target_score,
      )
  card = Scorecard.create(user_id: user.id, match_id: match.id, side: 'left')
  user.update_status('playing')
  match
end

지금 문제가 발생했을 때 A, B 길드원 모두 match 를 만드는게 문제이므로 동시에 접근하는 행동을 막아야한다.

동시에 접근하는 행동을 막기 위해서는 적절한 무언가를 찾고 그 무언가에 LOCK 을 거는 걸어야한다. 자 그러면 적절한 무언가는 무엇인가? 아주 간단하다. 문제는 WAR 에서 발생하므로 WAR 와 관련된 무언가를 찾고 Lock 을 걸면 되는것!!!

def create_war_match(user, params)
  war = War.find(params[:war_id])
  match = nil
  war.with_lock do
    raise "War match 이미 진행 중" if war.pending_or_progress_battle_exist?
    create_params = {
      match_type: params[:match_type],
      status: 'pending',
      rule_id: params[:rule_id],
      target_score: params[:target_score],
      }
    match = war.matches.create(create_params)
    card = match.scorecards.create(user_id: user.id, side: 'left')
    user.update_status('playing')
    war.set_schedule_at_no_reply_time(match)
  end
  match
end

A, B 두 길드는 War 를 공유하므로 war 를 찾은다음에 Lock 을 걸면된다~~

Last updated