[Fix] 폴더 삭제 잠금이 다른 폴더와 다른 사용자에게 번지지 않게 한다 - #322
Merged
Merged
Conversation
- #233 을 막으려고 넣은 lockAllByUserId 가 사용자 폴더 전체를 배타 잠금으로 잡아서, 삭제와 아무 상관 없는 폴더로 들어오는 문제 등록까지 삭제가 끝날 때까지 줄을 섰다. 기다리는 요청이 커넥션을 하나씩 물고 있어서 Hikari 풀(기본 10개)이 바닥나면 다른 사용자의 단순 조회까지 connection-timeout 30초를 채우고 실패했다 - deleteFoldersWithProblems 는 이제 삭제 대상 서브트리만 잡는다. lockAllByIdIn 으로 대상 폴더를 기본 키로 잠그고, lockAllByParentFolderIdIn 으로 한 단계씩 내려가며 하위 폴더를 잠근다. 내려가는 조회를 전부 잠금 조회로 둬야 REPEATABLE READ 스냅숏이 박히지 않아서, 잠금을 기다리다 커밋된 등록도 삭제 대상에 그대로 들어온다 - 그 사이에 새 폴더가 끼어들 자리는 없다. 어떤 폴더 아래에 폴더를 만들거나 옮기려면 findParentFolderForShare 로 그 부모를 공유 잠금해야 하는데, 우리가 배타 잠금을 쥔 뒤에는 기다렸다가 삭제된 부모를 보고 FOLDER_NOT_FOUND 로 거절된다 - 전체 폴더 삭제(deleteAllUserFoldersWithProblems)는 어차피 사용자 폴더 전부가 대상이라 lockAllByUserId 를 그대로 뒀다 - 복습 알림 취소가 문제마다 problem_id 범위 UPDATE 를 돌면서 uq_problem_review_reminder_seq 인덱스 끝의 갭까지 next-key lock 으로 잡고 있었다. problem_id 는 계속 커지므로 그 갭은 대개 supremum 이고, 폴더 삭제가 커밋될 때까지 그 뒤에 등록되는 모든 문제의 예약 INSERT 가 사용자와 무관하게 막혔다. 취소 대상을 먼저 읽고 기본 키로 UPDATE 하도록 바꿨다 - performance_schema 로 잠금 범위를 고정하는 FolderDeleteLockBlastRadiusTest 를 추가했다. 삭제 트랜잭션이 잠그는 folder 행이 삭제 대상 서브트리뿐인지, idx_folder_user_id 에 잠금이 안 잡히는지, 무관한 폴더와 다른 계정의 등록이 안 막히는지를 본다 - 고아 데이터 쪽 계약은 FolderDeleteProblemRegisterRaceTest 9개가 그대로 통과한다 Closes #319
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
✔️ 연관 이슈
📝 작업 내용
폴더 하나를 지우는 동안 그 폴더와 아무 상관 없는 문제 등록이 줄을 서고, 그 대기가 커넥션 풀을 바닥내 다른 계정의 요청까지 실패하던 것을 고쳤습니다.
lockAllByUserId)과 복습 알림 취소 쪽(원래 있던cancelByProblem)입니다. 폴더 쪽만 고쳐서는 다른 계정 등록이 그대로 막혔습니다.lockAllByIdIn으로 대상 폴더를 기본 키로 잠그고lockAllByParentFolderIdIn으로 한 단계씩 내려갑니다.WHERE id IN (...)으로 지웁니다.1. 원인은 잠금 두 개였습니다
FolderService.deleteFoldersWithProblemsidx_folder_user_id등치 스캔으로 사용자 폴더 전체 배타 잠금ProblemReviewReminderService.cancelPendingByProblemproblem_id범위 UPDATE 로uq_problem_review_reminder_seq인덱스 끝 갭까지 next-key lockproblem_id가 계속 커지니 그 INSERT 가 전부 supremum 갭에 들어갑니다performance_schema.data_lock_waits에problem_review_reminder.uq_problem_review_reminder_seq RECORD X,INSERT_INTENTION [supremum pseudo-record]가 그대로 찍혔습니다.connection-timeout기본값 30초를 채우고 실패합니다. dev/prod yml 에spring.datasource.hikari설정이 없어서 풀은 기본값 10개입니다.2. 서브트리만 잠가도 고아 데이터가 안 생기는 이유
findParentFolderForShare로 그 부모를 공유 잠금해야 하는데, 우리가 배타 잠금을 쥔 뒤에는 기다렸다가 삭제된 부모를 보고 기존FOLDER_NOT_FOUND로 거절됩니다.deleteAllUserFoldersWithProblems)는 그대로 뒀습니다. 어차피 사용자 폴더 전부가 대상이라 좁힐 것이 없고, 계정 정리 때만 타는 드문 경로입니다.3. #291 의 갭 잠금은 폴더 쪽만 없어집니다
idx_folder_user_id에는 이제 잠금이 안 잡힙니다. 기본 키 등치 조회는REC_NOT_GAP이라, 인접 사용자 구간을 덮던 갭이 사라집니다. 재현 테스트가 이 조건을 단언합니다.parent_folder_id조회에는 갭 잠금이 남습니다. 다만 덮는 구간이 "그 사용자의user_id구간 전체" 에서 "삭제 대상 폴더를 부모로 가진 항목 앞뒤" 로 줄어듭니다.👤 사용자 영향
FOLDER_NOT_FOUND(404) 를 받습니다. 새 에러 코드는 없습니다.🔌 API 호환성
🗄️ DB migration
🔐 인증/권한
findFolderEntity(folderId, userId)를 잠금 조회 결과 검증으로 옮기면서validateFolderOwner와 루트 폴더 거절이 그대로 남아 있는지, 요청받은 순서대로 판정되는지 확인했습니다)✅ 검증 결과
FolderDeleteLockBlastRadiusTest). 순서는 기존 경합 테스트와 같게 삭제 트랜잭션을 커밋 직전에 세워 두고 잽니다. sleep 은 쓰지 않았습니다.performance_schema.data_locks의folderPRIMARY 레코드 잠금이 대상 폴더와 그 하위 폴더뿐인지,idx_folder_user_id에 잠금이 없는지uq_problem_review_reminder_seqsupremum 갭이 그대로 찍혔습니다.FolderDeleteProblemRegisterRaceTest) 전부 통과합니다.concurrency,folder,problem패키지 551개 통과, 실패 0test통과 (3,502개, 실패 0, 건너뜀 0).origin/develop최신([Fix] 신버전이 남긴 mission_log 가 구버전 기기의 적립을 막지 않게 한다 #320, [Fix] 요일 없는 주간 복습 알림 검증을 앱 버전으로 가른다 #321 머지분) 위에 리베이스한 뒤 다시 돌린 결과입니다.문제 등록 한 건이 커넥션을 두 개 동시에 씁니다. 이게 30초짜리 전역 실패를 만든 마지막 조각인데, 폴더 삭제와는 다른 원인이라 이 PR 에서 건드리지 않았습니다.
ProblemReviewReminderService.handleProblemCreated가AFTER_COMMIT+REQUIRES_NEW라, 바깥 트랜잭션의 커넥션이 아직 반납되기 전에 예약 INSERT 용 커넥션을 하나 더 빌립니다.connection-timeout을 채웁니다. [Fix] 폴더 삭제 잠금이 전역 장애로 번진다 - 등록이 60초 뒤 504 인데 저장은 되고 다른 사용자까지 실패한다 #319 에 적힌 "한 번은 500 에 30.4초" 가 이 모양입니다.spring.datasource.hikari.maximum-pool-size가 dev/prod 양쪽에 없어서 기본값 10 으로 도는 것도 같이 보시면 좋겠습니다(Tomcatthreads.max는 50 입니다).🚀 배포 리스크
lockAllByUserId는 항상 id 오름차순이라 이 창이 없었습니다. 교착이 나면 MySQL 이 한쪽을 즉시 롤백하므로 그 요청 하나가 500 이 되고, 지금처럼 전역으로 번지지는 않습니다.↩️ 롤백/대응 방법