Conversation
#158 은 한 요청 안에서 갱신이 두 번 일어나는 경우를 막았지만, 동시에 들어온 두 실행 흐름은 막지 못했다. 커밋된 토큰을 먼저 읽는 가드와 실제 갱신 사이에 잠금이 없어서, 둘 다 그 틈을 통과하면 같은 리프레시 토큰을 들고 GitHub 에 간다. GitHub 은 리프레시 토큰을 매번 회전시키므로 먼저 도착한 쪽만 성공하고, 뒤에 온 쪽은 bad_refresh_token 을 맞은 뒤 catch 에서 연동을 통째로 지운다 — 앞선 흐름이 방금 받아온 멀쩡한 토큰까지 함께 사라진다. 갱신 경로에 진입하는 주체가 늘어 창도 넓어졌다. 예전에는 사용자 요청뿐이었지만 지금은 DomainVerificationWorker(#160)와 StuckDeploymentRecoveryWorker(#161)가 1분마다 같은 경로를 탄다. 갱신 구간 전체를 유저 행 잠금으로 직렬화한다. 뒤에 온 쪽은 앞이 끝날 때까지 기다렸다가 잠금을 얻은 뒤 상태를 다시 보고, 이미 갱신돼 있으면 GitHub 을 부르지 않고 그 토큰을 그대로 쓴다. 잠금은 저장과 같은 트랜잭션에 있어야 한다. 바깥에서 잠그고 저장만 REQUIRES_NEW 로 떼면 안쪽이 바깥의 잠금을 기다리다 교착된다. 그래서 GithubUserTokenRefresher 의 메서드 하나가 잠금·조회·GitHub 호출·저장을 모두 REQUIRES_NEW 안에서 끝낸다. ForbiddenException 은 롤백 대상에서 뺐다 — bad_refresh_token 일 때 토큰을 지우고 예외를 던지는데, 롤백되면 그 지움이 사라져 다음 호출이 같은 무효 토큰으로 또 간다. 빠른 경로(잠금 없이 커밋된 토큰 읽기)는 그대로 남겨, 대부분의 호출은 DB 잠금까지 가지 않는다. GithubTokenCleaner 는 이 클래스로 흡수했다. 이름과 달리 저장·삭제·조회를 모두 갖고 있었고, 이제 갱신까지 한 곳에서 소유하는 편이 맞다. Closes #162 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fix(auth): 동시 갱신이 겹쳐 GitHub 연동이 지워지던 문제
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.
develop → main. #166 하나뿐이고 마이그레이션 변경은 없다.
무엇을 고치나
#158 은 한 요청 안에서 갱신이 두 번 일어나는 경우를 막았지만, 동시에 들어온 두 흐름은 막지 못했다. 커밋된 토큰을 먼저 읽는 가드와 실제 갱신 사이에 잠금이 없어서 둘 다 그 틈을 통과하면 같은 리프레시 토큰으로 GitHub 에 간다. GitHub 은 리프레시 토큰을 매번 회전시키므로 뒤에 온 쪽이
bad_refresh_token을 맞고, 그 catch 가 앞선 흐름이 방금 받아온 멀쩡한 토큰까지 지운다.창이 넓어졌다. 예전에는 사용자 요청뿐이었지만 지금은
DomainVerificationWorker(#160)와StuckDeploymentRecoveryWorker(#161)가 1분마다 같은 경로를 탄다. 어제 dev 와 운영 양쪽에서 연동이 끊긴 것을 실제로 겪었다.해결
갱신 구간 전체를 유저 행 잠금(
SELECT ... FOR UPDATE)으로 직렬화한다. 뒤에 온 흐름은 기다렸다가 잠금을 얻은 뒤 상태를 다시 보고, 이미 갱신돼 있으면 GitHub 을 부르지 않는다.잠금은 저장과 같은 트랜잭션에 있어야 한다 — 바깥에서 잠그고 저장만
REQUIRES_NEW로 떼면 교착된다. 그래서 잠금·조회·GitHub 호출·저장을 한REQUIRES_NEW안에서 끝낸다.ForbiddenException은 롤백 대상에서 뺐다(토큰을 지운 뒤 던지는데 롤백되면 그 지움이 사라진다).빠른 경로(잠금 없이 커밋된 토큰 읽기)는 유지해서 대부분의 호출은 DB 잠금까지 가지 않는다.
검증
GithubUserTokenRefresherTest) — 잠금을 기다린 흐름이 승자의 토큰을 쓰는지, 무관한 실패(5xx)로는 연동을 지우지 않는지 등./gradlew test전체 통과, CI 통과, dev 배포 성공(f2c243e)관련: #158, #160, #161
🤖 Generated with Claude Code