문제와 원인
최근 10분 제보가 9건인 사용자에게 요청 2개가 동시에 들어오면, 분리된 COUNT와 INSERT 사이에서 두 요청 모두 9건을 읽었다. 각 요청이 저장을 진행해 최종 11건이 됐고 응답은 둘 다 HTTP 201이었다.
수정 전 코드
POST
좌우로 이동해 전체 내용을 확인하세요. 키보드: ←·→, Home·End
const recentReportCount = await repository.countRecentForUser(
userKey,
new Date(Date.now() - REPORT_RATE_WINDOW_MS).toISOString(),
);
if (recentReportCount >= REPORT_RATE_LIMIT) {
return json(
{ error: "제보 요청이 너무 많습니다. 10분 후 다시 시도해 주세요." },
429,
{ "Retry-After": String(REPORT_RATE_WINDOW_MS / 1_000) },
);
}
const id = crypto.randomUUID();
const createdAt = new Date().toISOString();
await repository.create({ id, userKey, category, title, description, questionId, createdAt });
return json({ report: { id, status: "new", createdAt } }, 201);
countRecentForUser / create
좌우로 이동해 전체 내용을 확인하세요. 키보드: ←·→, Home·End
async countRecentForUser(userKey: string, since: string) {
const row = await this.connection().prepare(`
SELECT COUNT(*) AS count
FROM user_reports
WHERE user_key = ? AND created_at >= ?
`).bind(userKey, since).first<{ count: number }>();
return Number(row?.count ?? 0);
}
좌우로 이동해 전체 내용을 확인하세요. 키보드: ←·→, Home·End
INSERT INTO user_reports (
id, user_key, category, title, description, page_path,
question_id, status, admin_note, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, '', ?, 'new', '', ?, ?)
수정 후 코드
제보 저장 SQL
사전 개수 조회를 제거하고 같은 사용자의 최근 10분 제보 수가 10건 미만일 때만 INSERT하도록 바꿨다. 사용자 키·구간 시작 시각·한도 10을 SQL에 바인딩한다.
좌우로 이동해 전체 내용을 확인하세요. 키보드: ←·→, Home·End
INSERT INTO user_reports (
id, user_key, category, title, description, page_path,
question_id, status, admin_note, created_at, updated_at
) SELECT ?, ?, ?, ?, ?, '', ?, 'new', '', ?, ?
WHERE (
SELECT COUNT(*) FROM user_reports
WHERE user_key = ? AND created_at >= ?
) < ?;
영향받은 행 수가 1개면 저장 성공을 반환한다.
좌우로 이동해 전체 내용을 확인하세요. 키보드: ←·→, Home·End
return Number(result.meta.changes ?? 0) === 1;
첫 요청이 10번째 행을 저장하면 다음 요청의 meta.changes는 0이 된다. 처리 함수는 저장 성공에 HTTP 201, 한도 거절에 HTTP 429·Retry-After: 600을 반환한다.
검증 결과
Wrangler의 로컬 D1에서 원본 POST 처리 함수·저장소를 수정 전후 각각 5회 실행했다. 매회 별도 테스트 사용자에게 최근 제보 9건을 넣고 요청 2개를 동시에 보냈으며, 수정 전에는 두 COUNT가 끝난 뒤 삽입하도록 실행을 겹쳤다.
좌우로 이동해 전체 내용을 확인하세요. 키보드: ←·→, Home·End
| 실행 | 초기 제보 수 | 수정 전 응답·최종 행 수 | 수정 후 응답·최종 행 수 |
|---|---|---|---|
| 1회 | 9건 | 201·201, 11건 | 201·429, 10건 |
| 2회 | 9건 | 201·201, 11건 | 201·429, 10건 |
| 3회 | 9건 | 201·201, 11건 | 201·429, 10건 |
| 4회 | 9건 | 201·201, 11건 | 201·429, 10건 |
| 5회 | 9건 | 201·201, 11건 | 201·429, 10건 |
좌우로 이동해 전체 내용을 확인하세요. 키보드: ←·→, Home·End
| 5회 집계 | 수정 전 | 수정 후 |
|---|---|---|
| 요청 10개 중 새 저장 | 10건 | 5건 |
| 최근 10분 한도 초과 | 5회 | 0회 |
| 최종 제보 수 최소·최대 | 11·11건 | 10·10건 |