Files
document-haness/docs/clean-architecture-backend-template/tech-log-studio/non-atomic-check-then-act/case/case-a-rotation-that-overwrites-and-a-generation-that-cannot-be-reclaimed.md
T
DongHyeonkaandClaude Opus 5 b2963105a8 docs(keycloak-session-store): import the session-storage lab as a new project
The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.

Follows the import procedure in README.md.

  source/     the originating repository verbatim — 78 documents, 28 SVGs,
              8 manifests, plus .source-revision recording the commit
  final/      the SSOT
    document.md   729 lines written from the 29 experiment documents, not
                  concatenated: what was predicted, what was measured, and
                  where the measurement itself was wrong
    evidence/raw    125 outputs, flattened to <experiment>__<file> because
                    the originals collided (01-baseline.txt appeared three
                    times) and the audit only globs the top level
    evidence/meta   one per raw file; command and exitCode are null and the
                    README says why rather than inventing them
    evidence/browser  22 captures
    assets/       three diagrams through techviz
    .techviz/     their VizSpecs

A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.

Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.

verify-pipeline.py passes. audit-records.py reports no issues.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 22:51:59 +09:00

144 lines
11 KiB
Markdown

---
kind: CASE
slug: a-rotation-that-overwrites-and-a-generation-that-cannot-be-reclaimed
title: 회전이 비교 후 교체가 아니라 덮어쓰기이고, 세대 계수기는 음수가 되면 회수되지 않는다
topic: non-atomic-check-then-act
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: case:a-rotation-that-overwrites-and-a-generation-that-cannot-be-reclaimed
evidenceCapturedOn: 2026-09-02
assets:
- key: a-rotation-that-overwrites-and-a-generation-that-cannot-be-reclaimed
file: ../../../final/evidence/rendered/a-rotation-that-overwrites-and-a-generation-that-cannot-be-reclaimed.svg
evidence:
- ../../../final/evidence/raw/a-rotation-that-overwrites-and-a-generation-that-cannot-be-reclaimed.txt
source:
- 분석 문서는 grpc-client 편 §17.1 이 회전의 덮어쓰기를, §17.2 가 비원자적 감소를, §17.3 이 배수 목록 순회를 다룬다. 권고는 각각 비교 후 교체, 하한을 포함한 원자 갱신, 복사 후 쓰기 배열이다. 세대가 실제 채널을 들고 있지 않다는 것은 같은 문서 §16 이 확인하지 못한 것으로 적어 둔 자리다.
- 가족을 건넌 재현은 플랫폼 편 §7.4 와 교차 범위 편 §6 이 다룬다.
---
# 회전이 비교 후 교체가 아니라 덮어쓰기이고, 세대 계수기는 음수가 되면 회수되지 않는다
채널 런타임 레지스트리의 회전이 읽고 판단한 뒤 조건 없이 쓴다. 같은 모듈의 세대 계수기는 확인하고 별도로 감소시켜, 음수가 되면 그 세대를 영영 회수할 수 없다.
## 관계
- **원자 타입 위의 검사 후 실행과 비교 후 교체 루프**
이 사례가 속한 구조다.
- **같은 자격 증명 회전 결함이 한 가족에서 닫히고 다른 가족에서 재현됐다**
같은 자료구조 오용이 옆 모듈에도 있는데, 그 모듈의 사례는 다른 가족이 이미 닫은 형태의 재현이다.
- **배수 완료가 자기가 읽은 값으로 상태를 다시 써서 진행 중인 자격증명 회전을 되돌린다**
옆 모듈의 그 결과를 다루는 기록이다.
## 문제
채널 런타임은 세대를 갖는다. 설정이 바뀌면 새 세대를 만들고, 옛 세대는 진행 중인 호출이 끝날 때까지 배수 목록에 남아 있다가 회수된다.
준비하고 교체하고 배수하는 순서를 지키는 연산이 셋이다. 교체와 계수와 회수다.
## 결론
앞의 둘이 같은 형태의 결함을 갖는다. 읽기와 쓰기가 원자 타입 위에서 두 연산으로 갈라진다. 셋째는 다른 곳에서 어긋난다. 동기화 목록을 잠그지 않고 순회한다.
교체는 현재 런타임을 읽어 판단한 뒤 조건 없이 쓴다. 두 회전이 겹치면 나중 것이 먼저 것을 덮고, 덮인 쪽은 배수 목록에 오르지 못한 채 사라진다. 그 세대 위에서 시작된 호출은 아무도 세지 않는다. 같은 클래스의 첫 설치는 비교 후 교체를 쓴다.
계수기 감소는 확인 후 실행이다. 값이 0보다 큰지 확인하고 별도 연산으로 감소시킨다. 증가와 감소가 짝을 유지하는 한 이 값은 0 아래로 내려가지 않는다. 짝 없는 감소가 섞이면 두 스레드가 같은 값을 읽고 둘 다 감소시킬 수 있다.
두 번째 오용은 스스로 회복되지 않는다. 조용함을 판정하는 술어가 계수기를 0과 비교하고, 배수가 시작된 런타임은 증가를 거절하므로, 0 아래로 내려간 값은 그 자리에 머문다.
같은 자료구조 오용이 옆 모듈의 자격증명 회전 관리자에도 있다. 그 관리자의 사례는 다른 가족이 계약 테스트로 이미 닫은 형태의 재현이다.
## 검증 환경
OpenJDK : 21.0.12
확인 방식 : 읽기와 쓰기가 별개 연산이라는 코드 형태 확인과 호출자 계수
소스 수정 : x
## 재현 조건
1. 레지스트리의 회전 메서드에서 읽기와 쓰기를 찾고, 그 사이에 비교 후 교체가 있는지 확인한다.
2. 같은 클래스의 설치 메서드가 무엇을 쓰는지 대조하고, 배수 목록에 들어가는 값이 무엇인지 확인한다.
3. 세대가 들고 있는 필드를 나열하고, 이 모듈이 grpc 타입을 참조하는지 센다.
4. 계수기의 확인과 감소, 증가 경로의 배수 거절, 조용함 판정의 비교 대상을 확인한다.
5. 그 메서드들의 프로덕션 호출자를 센다.
6. 회수와 배수 목록 반환이 어떤 목록을 어떻게 순회하는지 확인한다.
7. 옆 모듈의 배수 완료 메서드 전문을 읽고 그 파일의 원자 갱신을 센다.
## 본문
<!-- body:start -->
설정이 바뀌면 레지스트리가 새 세대를 만들어 포인터를 옮기고 옛 세대를 배수 목록에 넣는다. 준비하고 교체하고 배수하는 순서다.
그 순서를 지키는 세 연산이 각각 다른 곳에서 어긋난다. 앞의 둘은 원자 타입 위에서 읽고 판단한 뒤 따로 쓴다. 셋째는 다른 형태다 — 동기화 목록을 잠그지 않고 순회한다.
## 같은 클래스가 같은 참조를 두 가지로 다룬다
:::evidence key="a-rotation-that-overwrites-and-a-generation-that-cannot-be-reclaimed" alt="코드베이스에서 회전 메서드가 참조를 읽고 판단한 뒤 조건 없이 쓰는 구간과 같은 클래스의 첫 설치가 쓰는 비교 후 교체, 그 파일의 비교 후 교체 매치 수와 배수 목록에 들어가는 값, 세대가 들고 있는 네 필드와 이 모듈의 grpc 참조 수, 계수기를 확인 후 감소시키는 두 메서드와 그 파일의 원자 갱신 매치 수와 배수 중에 증가를 거절하는 분기와 조용함 판정, 그 메서드의 프로덕션 호출자 수, 동기화 목록을 잠그지 않고 순회하는 두 메서드와 회수의 프로덕션 호출자 수, 옆 모듈의 배수 완료 전문과 그 파일의 원자 갱신 매치 수, 그리고 두 모듈의 런타임 소속을 뽑은 출력 77줄. 세대가 채널을 들고 있지 않고 두 모듈 어디에도 원자 갱신이 없다는 것이 그 출력에 보인다." caption="회전은 덮어쓰기 · 설치는 비교 후 교체 · 세대의 네 필드와 grpc 참조 0 · 계수기의 확인 후 감소 · 잠금 밖 순회 둘 · 옆 모듈의 판단 없는 쓰기 — 77줄" zoom="true"
:::
회전은 프로파일의 참조를 꺼내고, 현재 런타임을 지역 변수에 담고, 새 세대가 그것을 이어받는지 확인한 뒤, 조건 없이 쓴다.
같은 클래스의 첫 설치는 다르다. 널에서 새 런타임으로 비교 후 교체하고, 실패하면 이미 설치되어 있다는 뜻으로 다룬다. 이 파일의 비교 후 교체는 그 한 번뿐이다.
읽기와 쓰기 사이에 다른 회전이 끼면 나중 쓰기가 먼저 쓰기를 덮는다. 배수 목록에 들어가는 것은 이전 값 하나이므로, 덮인 대체본은 어디에도 등록되지 않는다. 그 세대는 배수도 회수도 되지 않고, 그 위에서 시작된 호출은 아무도 세지 않는다.
## 그 세대가 들고 있는 것은 채널이 아니다
세대의 필드는 넷이다. 세대 기술자와 계수기 둘과 배수 플래그다.
이 모듈의 프로덕션 코드에는 grpc 타입 참조가 0 이다. 닫아야 할 채널 객체가 여기에는 없다. 잃는 것은 자원이 아니라 추적이다.
## 계수기는 원자 타입 위에서 원자적이지 않다
두 종료 메서드가 같은 순서로 같은 일을 한다. 값이 0보다 큰지 읽어 보고, 그다음 별도 연산으로 감소시킨다. 이 파일에는 그 둘을 한 연산으로 묶는 호출이 없다.
그 확인은 하한이지만 원자적인 하한이 아니다. 감소가 증가와 짝을 이루는 동안에는 값이 음수가 되지 않는다. 두 스레드가 동시에 종료 안에 있으면 계수기는 최소 2 이기 때문이다.
짝 없는 감소가 하나라도 섞이면 달라진다. 같은 호출을 두 번 끝내거나, 시작이 거절된 뒤에도 종료를 부르면, 두 스레드가 같은 값을 읽고 둘 다 감소시킬 수 있다.
이 저장소에는 그 호출자가 아직 없다. 감소가 그 보호를 호출자에게 맡기고 있다는 것이 형태의 결함이다.
## 내려간 값은 그 자리에 머문다
조용함을 판정하는 술어가 두 계수기를 각각 0 과 비교한다.
계수기를 올리는 두 경로는 배수가 시작된 런타임의 작업을 거절한다. 회전이 이전 세대의 배수를 시작하고 목록에 넣으므로, 배수 목록에 오른 런타임은 새 작업을 받지 않고 따라서 값이 다시 올라가지 않는다.
0 아래로 내려간 값은 그래서 돌아오지 않는다. 그 세대는 회수 대상이 되지 못한 채 목록에 남는다.
## 회수는 다른 곳에서 어긋난다
배수 목록은 동기화 래퍼로 감싼 리스트다. 그 래퍼는 개별 연산만 동기화하고, 순회는 호출자가 목록을 잠그고 해야 한다는 것이 그 API 의 계약이다.
회수 메서드는 잠그지 않고 스트림으로 거르고, 그 결과를 목록에서 뺀다. 배수 목록을 밖으로 내주는 메서드도 잠그지 않고 복사한다. 복사도 순회다.
어느 쪽이든 회전이 동시에 항목을 더하면 순회 중 변경이 된다.
읽고 지우는 두 단계가 원자적이지 않은 것도 같지만, 이쪽은 무해하다. 그 사이에 조용해진 세대는 다음 호출에서 회수된다.
## 옆 모듈은 판단조차 없다
자격증명 회전 관리자가 상태를 하나의 참조로 들고 있다. 회전은 그것을 읽어 담고 승계를 판정한 뒤 조건 없이 쓴다.
배수 완료는 판정이 없다. 읽은 값의 현재 세대로 새 상태를 만들어 그대로 쓴다. 그래서 읽기와 쓰기 사이에 회전이 끼면 그 회전이 활성화한 세대가 지워지고 이전 세대가 다시 현재가 된다. 방금 교체하기 전의 자격 재료가 다시 현재 값이 된다.
그 파일에는 비교 후 교체도, 원자 갱신도, 동기화 블록도 0 이다.
이 형태는 처음 나온 것이 아니다. 다른 가족이 같은 회전 결함을 동시성 계약 테스트로 닫고 그 이력을 남겼는데, 이쪽 가족의 회전 관리자가 그것을 반복한다.
## 배선되지 않은 두 모듈
두 모듈 모두 모듈 레지스트리의 런타임 소속이 비어 있다. 계수기를 움직이는 메서드도, 회수를 부르는 메서드도 프로덕션 호출자가 0 이다.
남는 것은 형태다. 이 클래스는 같은 참조를 한 곳에서는 비교 후 교체로 다루고 다른 곳에서는 덮어쓴다.
## 확인하지 못한 것
경합 자체를 돌려서 만들어 보지는 않았다. 두 모듈 모두 런타임 소속이 비어 있고, 이 연산들을 부르는 프로덕션 코드도 없다.
경합을 실행으로 재현하지 않았다. 판정은 읽기와 쓰기가 별개 연산이라는 코드 형태와 조용함 술어의 비교 대상에 근거한다.
<!-- body:end -->