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>
14 KiB
kind, slug, title, topic, project, status, sourceRevision, rootTreeNode, evidenceCapturedOn, body, assets, evidence, source
| kind | slug | title | topic | project | status | sourceRevision | rootTreeNode | evidenceCapturedOn | body | assets | evidence | source | |||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| CASE | a20-f011-grpccompletionreconciler-arraylist | GrpcCompletionReconciler 의 pending 만 잠금 없는 ArrayList 다 | grpc-and-streaming | clean-architecture-backend-template | 게시 전 | 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 | case:a20-f011-grpccompletionreconciler-arraylist | 2026-09-04 | case-a20-f011-grpccompletionreconciler-arraylist.body.md |
|
|
|
GrpcCompletionReconciler 의 pending 만 잠금 없는 ArrayList 다
GrpcCompletionReconciler.pending 은 ArrayList 이고, reconcile 과 clearPending 이 잠금 없이 구조를 바꾸는 동안 pendingCases() 가 잠금 없이 그것을 순회한다. 같은 리프의 GrpcCancellationCoordinator 는 같은 모양의 맵을 들고 공개 메서드 다섯을 모두 synchronized 로 닫았다. 다만 이 타입은 오늘 어디에서도 만들어지지 않는다.
관계
- queued 를 줄이는 유일한 연산이 상한을 읽지 않는 승격 안에 있다 그 기록은 원자 타입 위의 검사 후 실행이고 여기는 잠금 없는 컬렉션이다. 둘 다 타입이 주는 보증과 코드가 필요로 하는 보증이 다른 자리다.
- 의도적으로 스레드 안전하지 않은 타입이 하나 있다
그 기록의
BoundedByteSink는 안전하지 않다는 것을 자바독에 적고 호출마다 새로 만들어 쓴다. 여기에는 그런 표기도 그런 사용 규약도 없다. - 실행 코드가 없어서 동시성 계약이 전부 문서다 그 리프는 실행 코드가 없어 계약을 문서로만 표현한다. 여기는 실행 코드가 있고 그것을 부르는 코드가 없으며, 운영 문서가 그 동작을 미리 적어 두었다.
문제
GrpcCompletionReconciler 가 원장이 답을 주지 못한 연산을 목록에 모은다.
그 목록이 어떤 자료구조이고 어느 메서드가 그것을 바꾸거나 읽는지, 무엇이 언제 넣는지, 같은 리프가 같은 모양을 어떻게 다루는지 확인했다.
결론
pending 은 new ArrayList<>()(:25)다. 닿는 자리는 셋인데 하는 일이 다르다 — reconcile:79 의 add 와 clearPending:91 의 remove 는 구조를 바꾸고, pendingCases:86 의 List.copyOf 는 순회해 복사한다. 어느 쪽에도 잠금이 없고 동기화 마커 일곱 가지가 모두 0 이다.
들어가는 조건은 좁다. :64~:77 이 COMMITTED·FAILED_TERMINAL·업무 조회가 있는 NOT_FOUND 를 모두 먼저 반환하므로, :78 에 닿는 것은 GrpcCompletionResolution:50 이 정의한 UNKNOWN 과 IN_PROGRESS 뿐이다. 앞쪽은 원장 장애 시점이다.
같은 리프에 같은 모양이 하나 더 있다. GrpcCancellationCoordinator:27 도 동시 자료구조가 아닌 LinkedHashMap 을 오래 들고 있는데, 공개 메서드 register·cancel·markCommitBoundaryCrossed·businessEffectAborted·reason 이 모두 synchronized 다. grpc-policy 안에서 그런 파일은 여덟이다.
위험은 리스트 구조에 갇혀 있다. pending 은 밖으로 나가지 않고 원소인 PendingCase 는 record 다.
프로덕션 호출자는 없다. 자기 파일 밖 main 참조가 0 개이고, 같은 검색이 짝 클래스에서는 0 이 아니다.
운영 런북이 이 클래스의 동작과 에스컬레이션 조건을 적어 두었지만, 같은 문서 :3~:6 이 이 가족 전부가 build-only 라 프로덕션에서 도는 것이 없다고 먼저 밝힌다. 요구하는 스위치는 코드에 정의가 0 개다.
판정은 P2 이고 원문과 같다. 코드 쪽 사실은 확인됐고, 요청 경로라는 근거는 그것을 부르는 자리가 없어 오늘 서지 않는다. 대신 그 상황을 다룰 절차가 이미 문서에 있고 리스트는 지금 모양 그대로라는 사실이 남는다.
검증 환경
OpenJDK : 21.0.12 확인 방식 : 클래스 자바독과 목록 선언 확인, pending 이 나오는 줄 전수와 구조 변경·순회 지점 구분, 동기화 마커 일곱을 이 파일·짝 파일·리프 전체 세 열로 계수, 같은 모양의 필드를 가진 클래스 검색과 그 공개 메서드 확인, reconcile 의 분기 전수와 requiresReconciliation 정의 확인, 경로 제한 없는 참조 검색과 대조 타입 계수, 운영 런북의 앞머리와 해당 절차 확인, 그 런북이 요구하는 프로퍼티의 정의 검색 소스 수정 : x
재현 조건
- 목록 필드의 선언과 클래스 자바독을 읽는다.
- 그 이름이 나오는 줄을 전부 찾고 구조를 바꾸는 것과 아닌 것을 가른다.
- 동기화 마커를 여러 이름으로 세되, 같은 검색을 같은 리프의 다른 파일에도 걸어 검색이 도는지 확인한다.
- 그 검색을 리프 전체에 걸어 마커를 가진 파일을 센다.
- 같은 모양의 필드를 가진 클래스를 찾고 그 공개 메서드의 잠금 여부를 읽는다.
- 목록에 넣는 줄까지 오는 분기를 처음부터 따라가고, 그 조건 메서드의 정의를 읽는다.
- 이 타입의 참조를 경로 제한 없이 검색하고, 같은 방식으로 대조 타입도 센다.
- 문서 참조가 있으면 그 문서의 앞머리부터 읽어 절차에 조건이 걸려 있는지 본다.
- 그 문서가 요구하는 설정 프로퍼티가 코드에 정의돼 있는지 찾는다.
본문
GrpcCompletionReconciler 는 원장이 답을 주지 못한 gRPC 호출을 목록에 모아 두는 93 줄짜리 클래스다. 그 목록은 new ArrayList<>()(:25)이고 감싸는 것이 없다.
pending 을 바꾸는 두 자리와 순회하는 한 자리
:::evidence key="a20-f011-grpccompletionreconciler-arraylist" alt="저장소 루트에서 돌린 정적 검색 출력 137줄. GrpcCompletionReconciler 의 클래스 자바독과 ArrayList 선언이 8번부터 26번 줄까지 실리고, pending 이라는 이름이 나오는 줄이 전부 나오는데 선언과 add 와 copyOf 와 remove 넷에 예외 메시지 둘이다. 동기화 마커 일곱 가지를 이 파일과 같은 리프의 GrpcCancellationCoordinator 와 리프 전체에 각각 걸어 세 열로 비교한 표가 나오는데 이 파일은 전부 0 이고 짝 파일은 synchronized 다섯이다. 이어서 같은 검색을 리프 62 파일에 걸어 표시자를 가진 여덟 파일이 나열된다. pending.add 가 실행되는 조건이 reconcile 본문 전체와 함께 실리고, requiresReconciliation 이 UNKNOWN 이나 IN_PROGRESS 일 때만 참이라는 정의가 따라온다. 같은 모양의 필드를 가진 클래스 다섯이 나오고 그중 GrpcCancellationCoordinator 의 공개 메서드가 전부 synchronized 인 것이 보인다. 참조 검색은 자기 파일 밖 main 이 0 개이고 대조 타입은 0 이 아니다. 마지막으로 운영 런북의 첫 아홉 줄이 실려 이 가족 전부가 build-only 라 여기 있는 것 중 프로덕션에서 도는 것이 없다고 문서가 스스로 밝히는 대목이 나오고, 그 런북이 요구하는 스위치가 코드에 정의된 파일 수와 지원 매트릭스의 같은 서술, 그리고 이 클래스에 기대하는 절차가 이어진다." caption="ArrayList 선언과 pending 이 나오는 줄 전부 · 표시자 일곱을 이 파일·짝·리프에 건 세 열 비교 · 리프 62 중 여덟 · add 의 실행 조건과 requiresReconciliation 정의 · 같은 모양 필드 다섯과 짝의 synchronized · 자기 파일 밖 main 0 · 런북의 build-only 자기 규정 — 137줄 · exit 0" zoom="true" :::
pending 이라는 이름은 여섯 줄에 나온다. 선언(:25), reconcile 안의 add(:79), pendingCases() 의 List.copyOf(:86), clearPending 의 remove(:91), 그리고 PendingCase 생성자의 예외 메시지 둘(:34, :37)이다.
셋이 잠금 없이 같은 리스트에 닿는데 하는 일이 다르다. add(:79)와 remove(:91)는 구조를 바꾸고, List.copyOf(pending)(:86)은 그 목록을 순회해 복사한다.
원문은 두 실패 모드를 따로 적었다. 동시 add 는 원소 유실이나 ArrayIndexOutOfBoundsException 이고, add 가 도는 중의 List.copyOf 는 ConcurrentModificationException 이나 널 원소로 인한 NullPointerException 이다. pending 이 담는 것은 사람이 조정해야 하는 연산이므로 유실은 조정되지 않은 채 잊히는 연산이 된다. 이 기록은 그 실행을 재현하지 않았다.
위험이 리스트 구조에 갇혀 있기는 하다. pending 은 private final 이고 밖으로 나가는 것은 List.copyOf 로 만든 불변 복사뿐이라 가변 참조가 새지 않는다. PendingCase 는 record 라 원소 자체도 불변이다.
항목이 들어가는 조건
reconcile:62~:82 를 따라가면 add 에 닿는 길이 좁다.
:64~:67 이 COMMITTED 와 FAILED_TERMINAL 을 먼저 돌려보낸다. :68~:77 은 NOT_FOUND 이고 업무 조회가 주어진 경우인데, 조회가 결과를 보이면 :71 에서, 아니면 :76 에서 반환한다. 어느 쪽이든 :78 에 닿지 않는다.
:78 의 requiresReconciliation() 은 GrpcCompletionResolution:50 에서 status == UNKNOWN || status == IN_PROGRESS 다.
즉 목록에 쌓이는 것은 원장을 조회하지 못했거나 청구가 아직 진행 중인 경우다. 앞쪽은 원장 장애 시점이고, 그때가 호출이 가장 몰리는 때다.
같은 리프의 짝은 같은 모양을 잠근다
동기화 마커 일곱 가지를 이 파일과 GrpcCancellationCoordinator 와 리프 전체에 각각 걸었다. 이 파일은 전부 0 이고, 짝 파일은 synchronized 다섯이다. 같은 검색이 한쪽에서 0 이 아닌 값을 내므로 검색이 헛돌지 않는다.
두 클래스는 모양이 같다. GrpcCancellationCoordinator:27 이 Map<String, GrpcCancellableOperation> operations = new LinkedHashMap<>() 를 들고, 이 클래스가 List<PendingCase> pending = new ArrayList<>() 를 든다. 둘 다 동시 자료구조가 아니고 둘 다 오래 사는 객체다.
갈리는 것은 그다음이다. 짝의 공개 메서드는 register(:45), cancel(:69), markCommitBoundaryCrossed(:90), businessEffectAborted(:100), reason(:110)이 모두 synchronized 다. 이 클래스에는 하나도 없다.
이 짝만 그런 것도 아니다. 리프를 전부 훑으면 여덟 파일이 잠금이나 동시 자료구조를 쓴다. 이 리프가 동시성을 다루지 않는 것이 아니다.
런북은 자기가 아직 안 도는 것을 알고 쓰였다
운영 런북이 이 클래스를 안내한다는 것은 원문에 없다. docs/runbooks/grpc-platform-operations.md:36~:37 은 UNKNOWN 이 원장을 조회할 수 없었다는 뜻이고 아무것도 결론지을 수 없으며 그 사례는 이 클래스가 큐에 넣고 나중에 재시도한다고 적는다. :41~:42 는 그 목록이 여러 차례에 걸쳐 자라면 에스컬레이션하라고 적는다.
그러나 같은 문서 :3~:6 이 먼저 조건을 건다. 범위가 :grpc:* 가족이고 오늘 전부 build-only 이며 모든 leaf 의 runtime_memberships 가 비어 있어 여기 있는 것 중 프로덕션에서 도는 것이 없다는 것이다. 그렇게 미리 쓰는 이유도 적는다 — 이 문서가 다루는 상태들은 당직자가 새벽 세 시에 원리부터 풀어낼 수 있는 것이 아니고, 동작을 런북보다 먼저 출하하면 그것을 처음 만나는 사람이 그 일을 하게 된다는 것이다.
:9 는 ca-skeleton.grpc.platform.enabled 가 true 여야 플랫폼이 시작된다고 적는데, 그 프로퍼티를 정의하는 파일이 저장소에 0 개다. docs/compatibility/grpc-support-matrix.md:66~:67 도 배포 아티팩트가 이 가족을 싣지 않는다고 따로 적는다.
그러므로 오늘 이 절차를 밟는 운영자는 없다. 남는 것은 그 절차가 이미 적혀 있고, 배선되는 순간 그것이 읽는 대상이 이 잠금 없는 리스트라는 것이다.
이 타입을 만드는 프로덕션 코드가 없다
자기 파일 밖에서 이 타입을 쓰는 main 파일은 0 개다. 참조는 자기 파일 둘, GrpcCompletionReconcilerTest 다섯 줄, 그리고 문서 셋이다. 같은 검색을 GrpcCancellationCoordinator 에 걸면 0 이 아니다.
원문과 갈리는 자리
원문은 이 리프에서 스레드 안전성을 명시적으로 다루는 유일한 클래스가 GrpcSerializedStreamWriter 라고 적었다. 62 파일 중 여덟이 동시성 구성을 쓴다.
원문은 reconcile 이 완료 결과가 불확실한 호출마다 불린다고 적었다. 목록에 쌓이는 조건은 그보다 좁아서 UNKNOWN 과 IN_PROGRESS 둘뿐이고, NOT_FOUND 는 업무 조회가 있는 경로에서 :78 에 닿지도 않는다.
pending 이 ArrayList 라는 것, 두 자리가 잠금 없이 구조를 바꾸고 한 자리가 잠금 없이 순회한다는 것, 마커가 0 이라는 것은 원문대로다.
등급에 대해
원본 분석의 등급은 P2 다. 이 기록은 새로 매기지 않는다.
상류가 P2 로 둔 근거는 요청 경로에서 동기화 없는 리스트가 변경된다는 것이다. 자료구조와 표시자 부재는 확인했고, 요청 경로라는 부분은 부르는 코드가 없어 오늘 성립하지 않는다.
같은 무게로 반대편에 놓이지 않는 사실이 하나 있다. 이 코드가 오늘 도는 것은 아니지만, 그것이 다룰 상황과 그때의 절차는 이미 문서에 적혀 있다. 배선은 남은 작업이고 리스트는 지금 모양 그대로다.
확인하지 못한 것
스레드를 여럿 띄워 동시 호출로 원소가 사라지는 것을 재현하지 않았다. 리스트 타입과 마커 부재와 세 자리가 하는 일을 읽은 데까지다.
UNKNOWN 상태를 만들어 넣지 않았다. 분기와 조건 정의를 코드로 따라갔다.
다른 브랜치에 이 클래스를 물리는 코드가 있는지 확인하지 않았다. 이 리비전의 main 에 없다는 것까지다.
다른 브랜치에 이 클래스를 물리는 코드가 있는지 확인하지 않았다. 이 리비전의 main 에 없다는 것까지다.