Files
document-haness/docs/clean-architecture-backend-template/final/evidence/raw/analysis-finding-a05-f024.txt
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

130 lines
9.3 KiB
Plaintext

# 회수 게이트웨이 전문 (64 줄)
JpaQuotaReclaimGateway.java:1 package dev.caskeleton.adapter.outbound.persistence.fileserver;
JpaQuotaReclaimGateway.java:2
JpaQuotaReclaimGateway.java:3 import dev.caskeleton.adapter.outbound.persistence.fileserver.entity.QuotaReservationEntity;
JpaQuotaReclaimGateway.java:4 import dev.caskeleton.adapter.outbound.persistence.fileserver.repository.FileserverQuotaRepository;
JpaQuotaReclaimGateway.java:5 import dev.caskeleton.application.fileserver.api.metadata.QuotaScope;
JpaQuotaReclaimGateway.java:6 import dev.caskeleton.application.fileserver.cleanup.QuotaReclaimGateway;
JpaQuotaReclaimGateway.java:7 import java.time.Clock;
JpaQuotaReclaimGateway.java:8 import java.time.Instant;
JpaQuotaReclaimGateway.java:9 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
JpaQuotaReclaimGateway.java:10 import org.springframework.data.domain.Limit;
JpaQuotaReclaimGateway.java:11 import org.springframework.stereotype.Repository;
JpaQuotaReclaimGateway.java:12
JpaQuotaReclaimGateway.java:13 /**
JpaQuotaReclaimGateway.java:14 * Returns reclaimed bytes to a scope's committed total.
JpaQuotaReclaimGateway.java:15 *
JpaQuotaReclaimGateway.java:16 * <p>Committed usage is spread over many rows, so a reclaim is drawn down newest-first across them
JpaQuotaReclaimGateway.java:17 * until the amount is satisfied. Newest-first matters: the most recently committed rows are the
JpaQuotaReclaimGateway.java:18 * ones a delete is most likely to correspond to, and drawing from them keeps historical rows from
JpaQuotaReclaimGateway.java:19 * being hollowed out by unrelated deletions.
JpaQuotaReclaimGateway.java:20 *
JpaQuotaReclaimGateway.java:21 * <p>Each draw-down is conditional on the row still holding at least that many bytes, so two
JpaQuotaReclaimGateway.java:22 * cleanup workers reclaiming at once cannot push the ledger negative — the loser simply moves to
JpaQuotaReclaimGateway.java:23 * the next row.
JpaQuotaReclaimGateway.java:24 *
JpaQuotaReclaimGateway.java:25 * <p>A remainder that no row can absorb is dropped rather than carried. The ledger's floor is zero:
JpaQuotaReclaimGateway.java:26 * a scope cannot owe negative bytes, and a reclaim that outruns the recorded total means the total
JpaQuotaReclaimGateway.java:27 * was already understated, which a negative balance would not fix.
JpaQuotaReclaimGateway.java:28 */
JpaQuotaReclaimGateway.java:29 @Repository
JpaQuotaReclaimGateway.java:30 @ConditionalOnProperty(prefix = "app.fileserver-platform", name = "enabled", havingValue = "true")
JpaQuotaReclaimGateway.java:31 public class JpaQuotaReclaimGateway implements QuotaReclaimGateway {
JpaQuotaReclaimGateway.java:32
JpaQuotaReclaimGateway.java:33 private static final Limit RECLAIM_PAGE = Limit.of(64);
JpaQuotaReclaimGateway.java:34
JpaQuotaReclaimGateway.java:35 private final FileserverQuotaRepository reservations;
JpaQuotaReclaimGateway.java:36 private final Clock clock;
JpaQuotaReclaimGateway.java:37
JpaQuotaReclaimGateway.java:38 public JpaQuotaReclaimGateway(FileserverQuotaRepository reservations, Clock clock) {
JpaQuotaReclaimGateway.java:39 this.reservations = reservations;
JpaQuotaReclaimGateway.java:40 this.clock = clock;
JpaQuotaReclaimGateway.java:41 }
JpaQuotaReclaimGateway.java:42
JpaQuotaReclaimGateway.java:43 @Override
JpaQuotaReclaimGateway.java:44 public void reclaim(QuotaScope scope, long bytes) {
JpaQuotaReclaimGateway.java:45 if (bytes < 0) {
JpaQuotaReclaimGateway.java:46 throw new IllegalArgumentException("bytes must not be negative");
JpaQuotaReclaimGateway.java:47 }
JpaQuotaReclaimGateway.java:48 if (bytes == 0) {
JpaQuotaReclaimGateway.java:49 return;
JpaQuotaReclaimGateway.java:50 }
JpaQuotaReclaimGateway.java:51 Instant now = clock.instant();
JpaQuotaReclaimGateway.java:52 long outstanding = bytes;
JpaQuotaReclaimGateway.java:53 for (QuotaReservationEntity committed :
JpaQuotaReclaimGateway.java:54 reservations.findCommittedWithBytes(scope.type(), scope.value(), RECLAIM_PAGE)) {
JpaQuotaReclaimGateway.java:55 if (outstanding == 0) {
JpaQuotaReclaimGateway.java:56 return;
JpaQuotaReclaimGateway.java:57 }
JpaQuotaReclaimGateway.java:58 long draw = Math.min(outstanding, committed.getCommittedBytes());
JpaQuotaReclaimGateway.java:59 if (reservations.reduceCommitted(committed.getReservationId(), draw, now) == 1) {
JpaQuotaReclaimGateway.java:60 outstanding -= draw;
JpaQuotaReclaimGateway.java:61 }
JpaQuotaReclaimGateway.java:62 }
JpaQuotaReclaimGateway.java:63 }
JpaQuotaReclaimGateway.java:64 }
# 그 한 번의 질의가 무엇을 돌려주는가
FileserverQuotaRepository.java:106 /** Committed rows for a scope that still carry bytes, newest first. */
FileserverQuotaRepository.java:107 @Query(
FileserverQuotaRepository.java:108 """
FileserverQuotaRepository.java:109 select q from QuotaReservationEntity q
FileserverQuotaRepository.java:110 where q.scopeType = :scopeType
FileserverQuotaRepository.java:111 and q.scopeValue = :scopeValue
FileserverQuotaRepository.java:112 and q.status = 'COMMITTED'
FileserverQuotaRepository.java:113 and q.committedBytes > 0
FileserverQuotaRepository.java:114 order by q.updatedAt desc
FileserverQuotaRepository.java:115 """)
FileserverQuotaRepository.java:116 List<QuotaReservationEntity> findCommittedWithBytes(
FileserverQuotaRepository.java:117 @Param("scopeType") String scopeType, @Param("scopeValue") String scopeValue, Limit limit);
# 그 차감이 0 행을 갱신했을 때 무엇이 되는가
FileserverQuotaRepository.java:119 /**
FileserverQuotaRepository.java:120 * Gives back part of a committed row.
FileserverQuotaRepository.java:121 *
FileserverQuotaRepository.java:122 * <p>The guard is what makes concurrent reclaims safe: a row that another reclaim already drew
FileserverQuotaRepository.java:123 * down below {@code amount} updates zero rows, and the caller moves to the next row instead of
FileserverQuotaRepository.java:124 * driving the ledger negative.
FileserverQuotaRepository.java:125 */
FileserverQuotaRepository.java:126 @Modifying(clearAutomatically = true, flushAutomatically = true)
FileserverQuotaRepository.java:127 @Query(
FileserverQuotaRepository.java:128 """
FileserverQuotaRepository.java:129 update QuotaReservationEntity q
FileserverQuotaRepository.java:130 set q.committedBytes = q.committedBytes - :amount,
FileserverQuotaRepository.java:131 q.version = q.version + 1,
FileserverQuotaRepository.java:132 q.updatedAt = :now
FileserverQuotaRepository.java:133 where q.reservationId = :reservationId
FileserverQuotaRepository.java:134 and q.committedBytes >= :amount
FileserverQuotaRepository.java:135 """)
FileserverQuotaRepository.java:136 int reduceCommitted(
FileserverQuotaRepository.java:137 @Param("reservationId") UUID reservationId,
FileserverQuotaRepository.java:138 @Param("amount") long amount,
FileserverQuotaRepository.java:139 @Param("now") Instant now);
# 회수를 부르는 자리
adapter/outbound/persistence-jpa · postgresqlIntegrationTest · PostgreSqlFileserverReclamationIntegrationTest.java:341 quotaReclaim.reclaim(scope, 800);
adapter/outbound/persistence-jpa · postgresqlIntegrationTest · PostgreSqlFileserverReclamationIntegrationTest.java:361 quotaReclaim.reclaim(scope, 5_000);
application-core · main · DefaultCleanupService.java:185 quotaGateway.reclaim(QuotaScope.ofNamespace(record.namespace().value()), size);
# 같은 게이트웨이를 그대로 부른 결과 (리포지터리만 프록시)
probe:22 static QuotaReservationEntity committedRow(long bytes) {
probe:42 ledger.add(committedRow(1));
probe:51 case "findCommittedWithBytes" -> {
probe:54 return ledger.subList(0, Math.min(limit, ledger.size()));
probe:56 case "reduceCommitted" -> {
probe:76 new JpaQuotaReclaimGateway(repo, Clock.fixed(Instant.EPOCH, ZoneOffset.UTC));
probe:77 gateway.reclaim(new QuotaScope("NAMESPACE", "probe"), askedBytes);
JpaQuotaReclaimGateway.reclaim 을 그대로 부른 결과
원장에 1바이트 확정 행 64 개 · 64 바이트 회수 요청 → 페이지 질의 1 회 · 차감 64 회 · 회수 못한 바이트 0
원장에 1바이트 확정 행 65 개 · 65 바이트 회수 요청 → 페이지 질의 1 회 · 차감 64 회 · 회수 못한 바이트 1
원장에 1바이트 확정 행 200 개 · 200 바이트 회수 요청 → 페이지 질의 1 회 · 차감 64 회 · 회수 못한 바이트 136
# 그 경계를 짚는 시험이 있는가
시험에서 64 를 쓰는 줄 : 0 개
시험에서 65 를 쓰는 줄 : 0 개
시험에서 RECLAIM_PAGE 를 쓰는 줄 : 0 개
회수를 다루는 시험 파일 :
src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/JpaQuotaReclaimGateway.java
src/application-core/src/main/java/dev/caskeleton/application/fileserver/cleanup/QuotaReclaimGateway.java
src/application-core/src/test/java/dev/caskeleton/application/fileserver/testkit/RecordingQuotaReclaimGateway.java