현재 진행 중인 프로젝트는 레이드 컨텐츠 입장을 위해 큐를 사용한다.
최초로 개발하며 개념 검증 시엔 원시적인 큐만 사용했고, 이는 동시성 문제어 이어질 수 있었다.
따라서 Bull을 도입하게 됐다.
하지만 Bull만 도입한다고 되는 것은 아니었다.
Bull 자체의 ``Job``에 대해선 원자적 처리를 보장하지만 그 이외의 부분에 대해선 원자적 처리가 보장되지 않는 상황이다.
각각의 필요한 모든 부분에 대해 명시적으로 락을 걸어 완전한 원자적 처리를 보장해야 했다.
이러한 처리를 위해 async-lock을 도입했다.
// pendingGroups 접근을 안전하게 하기 위한 헬퍼 메서드
async withPendingGroupsLock(fn) {
return this.pendingGroupsLock.acquire('pendingGroups', fn);
}
위와 같이 락을 걸기 위한 헬퍼를 만들었다.
``fn``의 형태로 함수를 인자로 받는데, 그 함수는 락이 걸린 상태로 동작하게 된다.
await this.withPendingGroupsLock(async () => {
for (const [groupId, group] of this.pendingGroups.entries()) {
const allUsersExist = Array.from(group.userIds).every((uid) => sessionManager.getUser(uid));
if (!allUsersExist) {
logger.info(`그룹 ${groupId}의 일부 유저가 존재하지 않아 그룹 제거`);
this.pendingGroups.delete(groupId);
}
}
});
이런 식으로 락을 걸어 사용할 수 있다.
하지만 데드락을 항상 조심해야 한다.
임시로 처리되게 한 부분을 좀 더 확실한 형태로 바꾸면서 데드락이 발생했다.
분명 코드만 보면 정상적이었음에도 불구, 프로그램이 더 이상 진행되지 않는 것이다.
로그를 찍어보며 어디에서 진행이 막히는지는 파악했으나, 그 이유를 알 수 없었다.
좀 더 들여다보니 그 이유는 `데드락`이었다.
async acceptUserInGroup(user, groupId) {
// ...
// 유효한 플레이어 모두 수락 처리
for (const p of actualMatchedPlayers) {
await this.removeAcceptQueueInUser(p);
p.setMatched(false);
}
// ...
}
return null;
}
``acceptUserInGroup``을 호출하는 상위 함수엔 이미 ``withPendingGroupsLock``이 걸려있다.
그 상태에서 ``this.removeAcceptQueueInUser``가 정상 진행 시 호출되는데, 여기가 문제였다.
async removeAcceptQueueInUser(user) {
return this.withPendingGroupsLock(async () => {
//...
});
}
이미 잡은 락을 또 잡고 있었다.
이러니 진행이 될 수가 있나...
``removeAcceptQueueInUser``의 락을 제거하고 나니 정상적으로 동작하는 것을 확인할 수 있었다.
이건 에러도 없다.
티를 안 내니 찾기가 쉽지 않다.
동시성 문제라는 것이 정말 만만하지 않다.
사람 잡는 요물이라고 하기에 손색없으리라.
'Camp > T.I.L.' 카테고리의 다른 글
[TIL #34] Docker 및 Docker Compose 올려보기 (0) | 2024.12.03 |
---|---|
[TIL #33] 마법의 엘리베이터 (0) | 2024.11.20 |
[TIL #32] 자바스크립트와 싱글톤 (0) | 2024.11.18 |
[TIL #31] 상태 업데이트의 벽 (0) | 2024.10.31 |
[TIL #30] 모의 면접을 준비하며 (0) | 2024.10.30 |