-
Notifications
You must be signed in to change notification settings - Fork 0
mutex lock 문제
simjaesung edited this page Dec 1, 2024
·
1 revision
현재 경찰 활동을 위해 executePolice
메서드 사용 시 락으로 인해 락 경쟁 상황 발생
games MutexMap {
50|BE | _map: Map(0) {},
50|BE | mutex: Mutex {
50|BE | _semaphore: Semaphore {
50|BE | _value: 1,
50|BE | _cancelError: Error: request for lock canceled
50|BE | at Object.<anonymous> (/root/web12-MafiaCamp/BE/node_modules/async-mutex/lib/errors.js:6:22)
50|BE | at Module._compile (node:internal/modules/cjs/loader:1469:14)
50|BE | at Module._extensions..js (node:internal/modules/cjs/loader:1548:10)
50|BE | at Module.load (node:internal/modules/cjs/loader:1288:32)
50|BE | at Module._load (node:internal/modules/cjs/loader:1104:12)
50|BE | at Module.require (node:internal/modules/cjs/loader:1311:19)
50|BE | at require (node:internal/modules/helpers:179:18)
50|BE | at Object.<anonymous> (/root/web12-MafiaCamp/BE/node_modules/async-mutex/lib/Semaphore.js:4:16)
50|BE | at Module._compile (node:internal/modules/cjs/loader:1469:14)
50|BE | at Module._extensions..js (node:internal/modules/cjs/loader:1548:10),
50|BE | _queue: [],
50|BE | _weightedWaiters: []
50|BE | }
50|BE | }
50|BE | }
- 한 작업이 계속 락을 잡고 있어서 다른 작업이 타임 아웃으로 실패하는 상황
문제 해결 과정을 기록합니다.
@Injectable()
export class MafiaCountdownTimer implements CountdownTimer {
private readonly stopSignals = new MutexMap<GameRoom, Subject<any>>();
private readonly pauses = new MutexMap<GameRoom, boolean>();
async start(room: GameRoom, situation: string): Promise<void> {
if (await this.stopSignals.has(room)) {
throw new DuplicateTimerException();
}
await this.stopSignals.set(room, new Subject());
await this.pauses.set(room, false);
let timeLeft: number = TIMEOUT_SITUATION[situation];
const currentSignal = await this.stopSignals.get(room);
let paused = await this.pauses.get(room);
return new Promise<void>((resolve) => {
interval(1000).pipe(
takeUntil(currentSignal),
takeWhile(() => timeLeft > 0 && !paused),
).subscribe({
next: async () => {
paused = await this.pauses.get(room);
room.sendAll('countdown', {
situation: situation,
timeLeft: timeLeft,
});
timeLeft--;
},
complete: async () => {
room.sendAll('countdown-exit', {
situation: situation,
timeLeft: timeLeft,
});
await this.stop(room);
resolve();
},
});
});
}
private async pause(room: GameRoom): Promise<void> {
await this.pauses.set(room, true);
}
private async cleanup(room: GameRoom): Promise<void> {
const signal = await this.stopSignals.get(room);
if (!signal) {
// 기존에는 signal 유무와 상관없이 항상 Exception이 터지는 코드라서 수정하였습니다.
throw new NotFoundTimerException();
}
signal.next(null);
signal.complete();
await this.stopSignals.delete(room);
await this.pauses.delete(room);
}
async stop(room: GameRoom): Promise<void> {
await this.pause(room);
await this.cleanup(room);
}
}
- Timer에서 락을 걸며 진행하고 있음
- 그런데 Timer는 한번 실행되면 계속 진행이 되므로 진정한 동시성 문제가 발생할 가능성이 낮음
- 그래서 다른 작업이 타임아웃으로 인해 실패
@Injectable()
export class TotalGameManager implements VoteManager, PoliceManager {
private readonly games = new MutexMap<GameRoom, Map<string, PlayerInfo>>();
private readonly ballotBoxs = new MutexMap<GameRoom, Map<string, string[]>>();
private readonly policeInvestigationMap = new MutexMap<GameRoom, boolean>();
async register(gameRoom: GameRoom, players: Map<GameClient, MAFIA_ROLE>): Promise<void> {
if (!await this.games.get(gameRoom)) {
const gameInfo = new Map<string, PlayerInfo>();
players.forEach((role, client) => {
gameInfo.set(client.nickname, { role, status: USER_STATUS.ALIVE });
});
await this.games.set(gameRoom, gameInfo);
console.log('gameRoom', await this.games.get(gameRoom));
}
}
private async killUser(gameRoom: GameRoom, player: string): Promise<void> {
const gameInfo = await this.games.get(gameRoom);
if (!gameInfo) {
throw new NotFoundGameRoomException();
}
const playerInfo = gameInfo.get(player);
playerInfo.status = USER_STATUS.DEAD;
gameInfo.set(player, playerInfo);
gameRoom.sendAll('vote-kill-user', { player, job: playerInfo.role });
}
async registerBallotBox(gameRoom: GameRoom): Promise<void> {
const ballotBox = await this.ballotBoxs.get(gameRoom);
const candidates: string[] = ['INVALIDITY'];
if (!ballotBox) {
const gameInfo = await this.games.get(gameRoom);
if (!gameInfo) {
throw new NotFoundGameRoomException();
}
const newBallotBox = new Map<string, string[]>();
gameInfo.forEach((playerInfo, client) => {
if (playerInfo.status === USER_STATUS.ALIVE) {
newBallotBox.set(client, []);
candidates.push(client);
}
});
// 무효표 추가
newBallotBox.set('INVALIDITY', []);
await this.ballotBoxs.set(gameRoom, newBallotBox);
} else {
ballotBox.forEach((votedUsers, client) => {
candidates.push(client);
});
}
gameRoom.sendAll('send-vote-candidates', candidates);
}
/*
무효표인 경우 to에 INVALIDITY로 보내면 됩니다.
*/
async cancelVote(gameRoom: GameRoom, from: string, to: string): Promise<void> {
await this.checkVoteAuthority(gameRoom, from);
const ballotBox = await this.ballotBoxs.get(gameRoom);
console.log(from, to, ballotBox);
const toVotes = ballotBox.get(to);
const voteFlag = this.checkVote(ballotBox, from);
if (voteFlag) {
ballotBox.set(to, toVotes.filter(voteId => voteId !== from));
}
this.sendVoteCurrentState(ballotBox, gameRoom);
}
private sendVoteCurrentState(ballotBox: Map<string, string[]>, gameRoom: GameRoom) {
const voteCountMap: Record<string, number> = {};
ballotBox.forEach((votedUsers, client) => {
voteCountMap[client] = votedUsers.length;
});
gameRoom.sendAll('vote-current-state', voteCountMap);
}
private async checkVoteAuthority(gameRoom: GameRoom, from: string): Promise<void> {
const game = await this.games.get(gameRoom);
if (!game) {
throw new NotFoundBallotBoxException();
}
const fromClientInfo = game.get(from);
if (fromClientInfo.status !== USER_STATUS.ALIVE) {
throw new UnauthorizedUserBallotException();
}
}
/*
무효표인 경우 to에 INVALIDITY로 보내면 됩니다.
*/
async vote(gameRoom: GameRoom, from: string, to: string): Promise<void> {
await this.checkVoteAuthority(gameRoom, from);
const ballotBox = await this.ballotBoxs.get(gameRoom);
const toVotes = ballotBox.get(to);
const voteFlag = this.checkVote(ballotBox, from);
if (!voteFlag) {
toVotes.push(from);
ballotBox.set(to, toVotes);
}
this.sendVoteCurrentState(ballotBox, gameRoom);
}
private checkVote(ballotBox: Map<string, string[]>, fromInfo: string): boolean {
let voteFlag = false;
ballotBox.forEach((votedUser) => {
if (votedUser.some(voteId => voteId === fromInfo)) {
voteFlag = true;
}
});
return voteFlag;
}
async primaryVoteResult(gameRoom: GameRoom): Promise<VOTE_STATE> {
const ballotBox = await this.ballotBoxs.get(gameRoom);
if (!ballotBox) {
throw new NotFoundBallotBoxException();
}
await this.voteForYourself(ballotBox);
const newBalletBox = new Map<string, string[]>();
const maxVotedUsers = this.findMostVotedUser(ballotBox);
if ((maxVotedUsers.length === 1 && maxVotedUsers[0] !== null) || (maxVotedUsers.length > 1)) {
/*
투표결과가 1등이 있는 경우 혹은 공동이 있는 경우
*/
maxVotedUsers.forEach((votedUser) => {
if (votedUser !== null) {
newBalletBox.set(votedUser, []);
}
});
newBalletBox.set('INVALIDITY', []);
await this.ballotBoxs.set(gameRoom, newBalletBox);
gameRoom.sendAll('primary-vote-result', maxVotedUsers);
return VOTE_STATE.PRIMARY;
}
gameRoom.sendAll('primary-vote-result', maxVotedUsers);
return VOTE_STATE.INVALIDITY;
}
private findMostVotedUser(ballotBox: Map<string, string[]>): string[] {
let maxVotedUsers: string[] = [];
let maxCnt = -1;
ballotBox.forEach((votedUsers, client) => {
if (maxCnt < votedUsers.length) {
maxVotedUsers = [client];
maxCnt = votedUsers.length;
} else if (maxCnt === votedUsers.length) {
maxVotedUsers.push(client);
}
});
return maxVotedUsers;
}
private voteForYourself(ballotBox: Map<string, string[]>) {
ballotBox.forEach((votedUsers, client) => {
if (!this.checkVote(ballotBox, client) && client !== 'INVALIDITY') {
votedUsers.push(client);
}
});
}
async finalVoteResult(gameRoom: GameRoom): Promise<VOTE_STATE> {
const ballotBox = await this.ballotBoxs.get(gameRoom);
if (!ballotBox) {
throw new NotFoundBallotBoxException();
}
const mostVotedUser = this.findMostVotedUser(ballotBox);
await this.ballotBoxs.delete(gameRoom);
if (mostVotedUser.length === 1 && mostVotedUser[0] !== null) {
await this.killUser(gameRoom, mostVotedUser[0]);
} else {
gameRoom.sendAll('vote-kill-user', null);
}
return VOTE_STATE.FINAL;
}
async executePolice(gameRoom: GameRoom, police: string, criminal: string): Promise<void> {
const investigationFlag = await this.policeInvestigationMap.get(gameRoom);
let policeFlag = false;
let criminalFlag = false;
let criminalJob: MAFIA_ROLE;
const userInfos = await this.games.get(gameRoom);
console.log('gameRoom2', gameRoom);
console.log('games', this.games);
console.log('userInfos', userInfos);
userInfos.forEach((playerInfo, client) => {
if (police === client && playerInfo.role === MAFIA_ROLE.POLICE && playerInfo.status === USER_STATUS.ALIVE) {
policeFlag = true;
} else if (criminal === client && playerInfo.status === USER_STATUS.ALIVE) {
criminalFlag = true;
criminalJob = playerInfo.role;
}
});
if (!investigationFlag && policeFlag && criminalFlag) {
await this.policeInvestigationMap.set(gameRoom, true);
const policeClient = gameRoom.clients.find(
(client) => client.nickname === police,
);
if (policeClient) {
policeClient.send('police-investigation-result', { criminal, criminalJob });
}
}
}
async finishPolice(gameRoom: GameRoom): Promise<void> {
await this.policeInvestigationMap.delete(gameRoom);
}
async initPolice(gameRoom: GameRoom): Promise<void> {
await this.policeInvestigationMap.set(gameRoom, false);
}
}
- JS/TS에서는 비교할 때 참조 동일성을 사용하므로 객체를 key로 하는게 아닌 객체의 roomId를 key로 만들어 봄
- 그러면 해당 string key를 통해 비교하므로 문제가 안 생긴다고 생각
@Injectable()
export class TotalGameManager implements VoteManager, PoliceManager {
private readonly games = new MutexMap<string, Map<string, PlayerInfo>>();
private readonly ballotBoxs = new MutexMap<string, Map<string, string[]>>();
private readonly policeInvestigationMap = new MutexMap<string, boolean>();
async register(gameRoom: GameRoom, players: Map<GameClient, MAFIA_ROLE>): Promise<void> {
if (!await this.games.get(gameRoom.roomId)) {
const gameInfo = new Map<string, PlayerInfo>();
players.forEach((role, client) => {
gameInfo.set(client.nickname, { role, status: USER_STATUS.ALIVE });
});
await this.games.set(gameRoom.roomId, gameInfo);
console.log('gameRoom', await this.games.get(gameRoom.roomId));
}
}
private async killUser(gameRoom: GameRoom, player: string): Promise<void> {
const gameInfo = await this.games.get(gameRoom.roomId);
if (!gameInfo) {
throw new NotFoundGameRoomException();
}
const playerInfo = gameInfo.get(player);
playerInfo.status = USER_STATUS.DEAD;
gameInfo.set(player, playerInfo);
gameRoom.sendAll('vote-kill-user', { player, job: playerInfo.role });
}
async registerBallotBox(gameRoom: GameRoom): Promise<void> {
const ballotBox = await this.ballotBoxs.get(gameRoom.roomId);
const candidates: string[] = ['INVALIDITY'];
if (!ballotBox) {
const gameInfo = await this.games.get(gameRoom.roomId);
if (!gameInfo) {
throw new NotFoundGameRoomException();
}
const newBallotBox = new Map<string, string[]>();
gameInfo.forEach((playerInfo, client) => {
if (playerInfo.status === USER_STATUS.ALIVE) {
newBallotBox.set(client, []);
candidates.push(client);
}
});
// 무효표 추가
newBallotBox.set('INVALIDITY', []);
await this.ballotBoxs.set(gameRoom.roomId, newBallotBox);
} else {
ballotBox.forEach((votedUsers, client) => {
candidates.push(client);
});
}
gameRoom.sendAll('send-vote-candidates', candidates);
}
/*
무효표인 경우 to에 INVALIDITY로 보내면 됩니다.
*/
async cancelVote(gameRoom: GameRoom, from: string, to: string): Promise<void> {
await this.checkVoteAuthority(gameRoom, from);
const ballotBox = await this.ballotBoxs.get(gameRoom.roomId);
const toVotes = ballotBox.get(to);
const voteFlag = this.checkVote(ballotBox, from);
if (voteFlag) {
ballotBox.set(to, toVotes.filter(voteId => voteId !== from));
}
this.sendVoteCurrentState(ballotBox, gameRoom);
}
private sendVoteCurrentState(ballotBox: Map<string, string[]>, gameRoom: GameRoom) {
const voteCountMap: Record<string, number> = {};
ballotBox.forEach((votedUsers, client) => {
voteCountMap[client] = votedUsers.length;
});
gameRoom.sendAll('vote-current-state', voteCountMap);
}
private async checkVoteAuthority(gameRoom: GameRoom, from: string): Promise<void> {
const game = await this.games.get(gameRoom.roomId);
if (!game) {
throw new NotFoundBallotBoxException();
}
const fromClientInfo = game.get(from);
if (fromClientInfo.status !== USER_STATUS.ALIVE) {
throw new UnauthorizedUserBallotException();
}
}
/*
무효표인 경우 to에 INVALIDITY로 보내면 됩니다.
*/
async vote(gameRoom: GameRoom, from: string, to: string): Promise<void> {
await this.checkVoteAuthority(gameRoom, from);
const ballotBox = await this.ballotBoxs.get(gameRoom.roomId);
const toVotes = ballotBox.get(to);
const voteFlag = this.checkVote(ballotBox, from);
if (!voteFlag) {
toVotes.push(from);
ballotBox.set(to, toVotes);
}
this.sendVoteCurrentState(ballotBox, gameRoom);
}
private checkVote(ballotBox: Map<string, string[]>, fromInfo: string): boolean {
let voteFlag = false;
ballotBox.forEach((votedUser) => {
if (votedUser.some(voteId => voteId === fromInfo)) {
voteFlag = true;
}
});
return voteFlag;
}
async primaryVoteResult(gameRoom: GameRoom): Promise<VOTE_STATE> {
const ballotBox = await this.ballotBoxs.get(gameRoom.roomId);
if (!ballotBox) {
throw new NotFoundBallotBoxException();
}
await this.voteForYourself(ballotBox);
const newBalletBox = new Map<string, string[]>();
const maxVotedUsers = this.findMostVotedUser(ballotBox);
if ((maxVotedUsers.length === 1 && maxVotedUsers[0] !== null) || (maxVotedUsers.length > 1)) {
/*
투표결과가 1등이 있는 경우 혹은 공동이 있는 경우
*/
maxVotedUsers.forEach((votedUser) => {
if (votedUser !== null) {
newBalletBox.set(votedUser, []);
}
});
newBalletBox.set('INVALIDITY', []);
await this.ballotBoxs.set(gameRoom.roomId, newBalletBox);
gameRoom.sendAll('primary-vote-result', maxVotedUsers);
return VOTE_STATE.PRIMARY;
}
gameRoom.sendAll('primary-vote-result', maxVotedUsers);
return VOTE_STATE.INVALIDITY;
}
private findMostVotedUser(ballotBox: Map<string, string[]>): string[] {
let maxVotedUsers: string[] = [];
let maxCnt = -1;
ballotBox.forEach((votedUsers, client) => {
if (maxCnt < votedUsers.length) {
maxVotedUsers = [client];
maxCnt = votedUsers.length;
} else if (maxCnt === votedUsers.length) {
maxVotedUsers.push(client);
}
});
return maxVotedUsers;
}
private voteForYourself(ballotBox: Map<string, string[]>) {
ballotBox.forEach((votedUsers, client) => {
if (!this.checkVote(ballotBox, client) && client !== 'INVALIDITY') {
votedUsers.push(client);
}
});
}
async finalVoteResult(gameRoom: GameRoom): Promise<VOTE_STATE> {
const ballotBox = await this.ballotBoxs.get(gameRoom.roomId);
if (!ballotBox) {
throw new NotFoundBallotBoxException();
}
const mostVotedUser = this.findMostVotedUser(ballotBox);
await this.ballotBoxs.delete(gameRoom.roomId);
if (mostVotedUser.length === 1 && mostVotedUser[0] !== null) {
await this.killUser(gameRoom, mostVotedUser[0]);
} else {
gameRoom.sendAll('vote-kill-user', null);
}
return VOTE_STATE.FINAL;
}
async executePolice(gameRoom: GameRoom, police: string, criminal: string): Promise<void> {
console.log('All game rooms:', await this.games.keys());
console.log('investgationFlag', await this.policeInvestigationMap.keys());
const investigationFlag = await this.policeInvestigationMap.get(gameRoom.roomId);
let policeFlag = false;
let criminalFlag = false;
let criminalJob: MAFIA_ROLE;
const userInfos = await this.games.get(gameRoom.roomId);
console.log('gameRoom2', gameRoom);
console.log('games', this.games);
console.log('userInfos', userInfos);
userInfos.forEach((playerInfo, client) => {
if (police === client && playerInfo.role === MAFIA_ROLE.POLICE && playerInfo.status === USER_STATUS.ALIVE) {
policeFlag = true;
} else if (criminal === client && playerInfo.status === USER_STATUS.ALIVE) {
criminalFlag = true;
criminalJob = playerInfo.role;
}
});
if (!investigationFlag && policeFlag && criminalFlag) {
await this.policeInvestigationMap.set(gameRoom.roomId, true);
const policeClient = gameRoom.clients.find(
(client) => client.nickname === police,
);
if (policeClient) {
policeClient.send('police-investigation-result', { criminal, criminalJob });
}
}
}
async finishPolice(gameRoom: GameRoom): Promise<void> {
await this.policeInvestigationMap.delete(gameRoom.roomId);
}
async initPolice(gameRoom: GameRoom): Promise<void> {
await this.policeInvestigationMap.set(gameRoom.roomId, false);
}
}
- 그래서 key로 변경
- 그래도 안됨
{
provide: VOTE_MANAGER,
useClass: TotalGameManager,
},
{
provide: POLICE_MANAGER,
useClass: TotalGameManager,
}
- 두개가 다른 인스턴스를 가져서 호환이 안되는 문제가 발생일 수도 있음
TotalGameManager,
{
provide: VOTE_MANAGER,
useExisting: TotalGameManager,
}
{
provide: POLICE_MANAGER,
useExisting: TotalGameManager,
},
- 해결 완료
해결책을 기록합니다.
{
provide: VOTE_MANAGER,
useClass: TotalGameManager,
},
{
provide: POLICE_MANAGER,
useClass: TotalGameManager,
}
- 두개가 다른 인스턴스를 가져서 호환이 안되는 문제가 발생일 수도 있음
TotalGameManager,
{
provide: VOTE_MANAGER,
useExisting: TotalGameManager,
}
{
provide: POLICE_MANAGER,
useExisting: TotalGameManager,
},
- 해결 완료
web12-MafiaCamp