Skip to content

Commit

Permalink
Add: resolve race condition
Browse files Browse the repository at this point in the history
  • Loading branch information
TaehyeonPark committed Feb 5, 2025
1 parent 4a59be0 commit 758b428
Show file tree
Hide file tree
Showing 3 changed files with 39 additions and 35 deletions.
54 changes: 24 additions & 30 deletions src/schedules/autoProcessingRoom.js
Original file line number Diff line number Diff line change
@@ -1,54 +1,48 @@
const { userModel, roomModel, chatModel } = require("../modules/stores/mongo");
const logger = require("../modules/logger");
const { emitChatEvent } = require("../modules/socket");
const { userModel, roomModel } = require("../modules/stores/mongo");
const logger = require("../modules/logger");

const MS_PER_MINUTE = 60000;

// 탑승자가 1명인 상태로 탑승일이 지난 방에 대해서 정산 완료 처리
// 탑승자가 1명인 상태로 탑승 시간이 지난 방에 대해서 정산 완료 처리
module.exports = (app) => async () => {
try {
const io = app.get("io");
const expiredDate = new Date(Date.now() - 90 * MS_PER_MINUTE).toISOString();
const arrivalDate = new Date(Date.now() - 60 * MS_PER_MINUTE).toISOString();

const expiredDate = new Date(Date.now() - 60 * MS_PER_MINUTE).toISOString();
const arrivalDate = new Date(Date.now()).toISOString();
const candidateRooms = await roomModel.find({
$and: [
{ time: { $gte: expiredDate } },
{ time: { $lte: arrivalDate } },
{ "part.0": { $exists: true }, "part.1": { $exists: false } },
{ "part.0.settlementStatus": { $nin: ["paid", "sent"] } },
{ "part.0.settlementStatus": { $nin: ["paid", "sent"] } }, // "sent"의 경우 로직상 불가능 하지만, 문서화 측면에서 의도적으로 남겨두었음음.
],
});

await Promise.all(
candidateRooms.map(async ({ _id: roomId, time, part }) => {
const countArrivalChat = await chatModel.countDocuments({
roomId,
type: "arrival",
});
if (countArrivalChat > 0) return;
const minuteDiff = Math.floor((Date.now() - time) / MS_PER_MINUTE);
if (minuteDiff <= 0) return;
candidateRooms.map(async ({ _id: roomId, part }) => {
const user = await userModel.findById(part[0].user._id);

// 정산 채팅을 보냅니다.
await emitChatEvent(io, {
roomId: roomId,
type: "arrival",
content: minuteDiff.toString(),
type: "settlement",
content: user.id,
authorId: user._id,
});
// user에게 doneroom 으로 이전
const user = await userModel.findById(part[0].userId);
user.doneRooms.push(roomId);

const userOngoingRoomIndex = user.ongoingRoom.indexOf(roomId);
if (userOngoingRoomIndex === -1) {
await user.save();
return false;
}
user.ongoingRoom.splice(userOngoingRoomIndex, 1);

await user.save();
// 1명의 참여자만 존재하는 room에 대하여 정산 완료 처리
await roomModel.findByIdAndUpdate(roomId, {
["part.0.settlementStatus"]: "paid",
settlementTotal: 1,
});

// room에 대한 정산 완료 처리 isOver
await roomModel.findByIdAndUpdate(roomId, { isOver: true });
// Atomic update로 각 Room을 한번에 제거 및 추가함.
// 아토믹하게 처리하지 않을 경우 각 Promise가 동일한 user의 여러 ongoingRoom 또는 doneRoom을 동시에 수정하여 경합조건이 발생할 수 있음에 유의.
await userModel.findByIdAndUpdate(user._id, {
$pull: { ongoingRoom: roomId },
$push: { doneRoom: roomId },
});
})
);
} catch (err) {
Expand Down
3 changes: 1 addition & 2 deletions src/schedules/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ import autoProcessingRoom from "./autoProcessingRoom";
const registerSchedules = (app: Express) => {
cron.schedule("*/5 * * * *", notifyBeforeDepart(app));
cron.schedule("*/10 * * * *", notifyAfterArrival(app));
cron.schedule("1-59/10 * * * *", autoProcessingRoom(app));

cron.schedule("1,11,21,31,41,51 * * * *", autoProcessingRoom(app));
if (naverMap.apiId && naverMap.apiKey) {
cron.schedule("0,30 * * * * ", updateMajorTaxiFare(app));
cron.schedule("0 18 * * *", updateMinorTaxiFare(app));
Expand Down
17 changes: 14 additions & 3 deletions src/services/rooms.js
Original file line number Diff line number Diff line change
Expand Up @@ -485,19 +485,30 @@ const searchHandler = async (req, res) => {

const searchByUserHandler = async (req, res) => {
try {
// lean()이 적용된 user를 response에 반환해줘야 하기 때문에 user를 한 번 더 지정한다.
let user = await userModel
.findOne({ id: req.userId })
.populate({
path: "ongoingRoom",
options: { limit: 1000 },
options: {
limit: 1000,
// ongoingRoom 은 시간 오름차순 정렬
sort: { time: 1 },
},
populate: roomPopulateOption,
})
.populate({
path: "doneRoom",
options: { limit: 1000 },
options: {
limit: 1000,
// doneRoom 은 시간 내림차순 정렬
sort: { time: -1 },
},
populate: roomPopulateOption,
});
})
.lean();

// 정산완료여부 기준으로 진행중인 방과 완료된 방을 분리해서 응답을 전송합니다.
const response = {};
response.ongoing = user.ongoingRoom.map((room) =>
formatSettlement(room, { isOver: false })
Expand Down

0 comments on commit 758b428

Please sign in to comment.