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

133 lines
11 KiB
Plaintext

# 설계 문서가 쿼터를 무엇으로 정의하는가
design-deviations.md:79 ### 9. Quota settlement is FIFO within a scope
design-deviations.md:82 stragglers by TTL and the `STALE_QUOTA_RESERVATION` cleanup type rather than threading a reservation
design-deviations.md:83 id through the upload session. `QuotaCommitGateway` therefore settles the oldest live reservation in
design-deviations.md:86 Which row closes does not change any quota decision: enforcement sums reserved and committed bytes
design-deviations.md:112 The metadata store, session store, quota service, queues, ledger, and staging locator carry
design-deviations.md:145 | reserve quota + insert record + create session | a reservation that outlived a failed insert holds capacity for a file that never existed |
design-deviations.md:146 | READY transition + quota commit | a finished file whose reservation was never converted holds capacity until the reservation expires |
승인 컨트롤러의 클래스 설명 :
DefaultTransferAdmissionController.java:1 package dev.caskeleton.application.fileserver.quota;
DefaultTransferAdmissionController.java:2
DefaultTransferAdmissionController.java:3 import dev.caskeleton.application.fileserver.api.error.FileTooLargeException;
DefaultTransferAdmissionController.java:4 import dev.caskeleton.application.fileserver.api.error.FileserverErrorCode;
DefaultTransferAdmissionController.java:5 import dev.caskeleton.application.fileserver.api.error.FileserverFailureContext;
DefaultTransferAdmissionController.java:6 import dev.caskeleton.application.fileserver.api.error.QuotaExceededException;
DefaultTransferAdmissionController.java:7 import dev.caskeleton.application.fileserver.api.error.StorageFullException;
DefaultTransferAdmissionController.java:8 import dev.caskeleton.application.fileserver.api.error.TransferAdmissionRejectedException;
DefaultTransferAdmissionController.java:9 import dev.caskeleton.application.fileserver.api.metadata.QuotaScope;
DefaultTransferAdmissionController.java:10 import java.util.Map;
DefaultTransferAdmissionController.java:11 import java.util.OptionalDouble;
DefaultTransferAdmissionController.java:12 import java.util.concurrent.ConcurrentHashMap;
DefaultTransferAdmissionController.java:13 import java.util.concurrent.Semaphore;
DefaultTransferAdmissionController.java:14 import java.util.concurrent.atomic.AtomicBoolean;
DefaultTransferAdmissionController.java:15
DefaultTransferAdmissionController.java:16 /**
DefaultTransferAdmissionController.java:17 * In-process admission control backed by bounded semaphores.
DefaultTransferAdmissionController.java:18 *
DefaultTransferAdmissionController.java:19 * <p>The failure vocabulary is deliberately distinct so a client can tell a permanent policy denial
DefaultTransferAdmissionController.java:20 * from a transient one: an exhausted pool is {@code STORAGE_FULL}, a scope over its ceiling is
DefaultTransferAdmissionController.java:21 * {@code QUOTA_EXCEEDED}, and momentary permit exhaustion is a retryable admission rejection.
DefaultTransferAdmissionController.java:22 */
DefaultTransferAdmissionController.java:23 public final class DefaultTransferAdmissionController implements TransferAdmissionController {
DefaultTransferAdmissionController.java:24
DefaultTransferAdmissionController.java:25 private final TransferAdmissionProperties properties;
DefaultTransferAdmissionController.java:26 private final StorageUsageProbe usageProbe;
DefaultTransferAdmissionController.java:27 private final long maximumFileSize;
DefaultTransferAdmissionController.java:28 private final Semaphore instanceUploads;
DefaultTransferAdmissionController.java:29 private final Semaphore directDownloads;
DefaultTransferAdmissionController.java:30 private final Map<String, Semaphore> scopeUploads = new ConcurrentHashMap<>();
# 승인이 실제로 검사하는 것
DefaultTransferAdmissionController.java:44 @Override
DefaultTransferAdmissionController.java:45 public TransferPermit acquireUpload(QuotaScope scope, long requestedBytes) {
DefaultTransferAdmissionController.java:46 requireWithinFileSizePolicy(requestedBytes);
DefaultTransferAdmissionController.java:47 requireBelowHardHighWater();
DefaultTransferAdmissionController.java:48 Semaphore scopePermits = scopePermits(scope);
DefaultTransferAdmissionController.java:49 if (!scopePermits.tryAcquire()) {
DefaultTransferAdmissionController.java:50 throw new QuotaExceededException(
DefaultTransferAdmissionController.java:51 "scope upload concurrency is exhausted",
DefaultTransferAdmissionController.java:52 FileserverFailureContext.of(FileserverErrorCode.QUOTA_EXCEEDED, true));
DefaultTransferAdmissionController.java:53 }
DefaultTransferAdmissionController.java:54 if (!instanceUploads.tryAcquire()) {
DefaultTransferAdmissionController.java:55 scopePermits.release();
DefaultTransferAdmissionController.java:56 throw new TransferAdmissionRejectedException(
DefaultTransferAdmissionController.java:57 "instance upload permits are exhausted",
DefaultTransferAdmissionController.java:58 FileserverFailureContext.of(FileserverErrorCode.TRANSFER_ADMISSION_REJECTED, true));
DefaultTransferAdmissionController.java:59 }
DefaultTransferAdmissionController.java:60 return new SemaphorePermit(scopePermits, instanceUploads);
DefaultTransferAdmissionController.java:61 }
DefaultTransferAdmissionController.java:62
# 예약이 저장되는 자리
JpaFileQuotaService.java:36
JpaFileQuotaService.java:37 @Override
JpaFileQuotaService.java:38 public QuotaReservation reserve(QuotaScope scope, long expectedBytes, Duration ttl) {
JpaFileQuotaService.java:39 if (expectedBytes < 0) {
JpaFileQuotaService.java:40 throw new IllegalArgumentException("expectedBytes must not be negative");
JpaFileQuotaService.java:41 }
JpaFileQuotaService.java:42 Instant now = clock.instant();
JpaFileQuotaService.java:43 QuotaReservationEntity entity =
JpaFileQuotaService.java:44 new QuotaReservationEntity(
JpaFileQuotaService.java:45 UUID.randomUUID(),
JpaFileQuotaService.java:46 scope.type(),
JpaFileQuotaService.java:47 scope.value(),
JpaFileQuotaService.java:48 expectedBytes,
JpaFileQuotaService.java:49 now.plus(ttl),
JpaFileQuotaService.java:50 QuotaReservationStatus.RESERVED.name(),
JpaFileQuotaService.java:51 now);
JpaFileQuotaService.java:52 return FileEntityMapper.toReservation(reservations.save(entity));
JpaFileQuotaService.java:53 }
# 범위별 바이트 집계를 읽는 자리
adapter/outbound/persistence-jpa · main · JpaFileQuotaService.java:84 public long reservedBytes(QuotaScope scope) {
adapter/outbound/persistence-jpa · main · JpaFileQuotaService.java:89 public long committedBytes(QuotaScope scope) {
adapter/outbound/persistence-jpa · postgresqlIntegrationTest · PostgreSqlFileserverMetadataStoreIntegrationTest.java:303 assertThat(quota.committedBytes(scope)).isEqualTo(600);
adapter/outbound/persistence-jpa · postgresqlIntegrationTest · PostgreSqlFileserverMetadataStoreIntegrationTest.java:304 assertThat(quota.reservedBytes(scope)).isZero();
adapter/outbound/persistence-jpa · postgresqlIntegrationTest · PostgreSqlFileserverMetadataStoreIntegrationTest.java:321 assertThat(quota.reservedBytes(scope)).isZero();
adapter/outbound/persistence-jpa · postgresqlIntegrationTest · PostgreSqlFileserverReclamationIntegrationTest.java:291 assertThat(inTransaction(() -> quota.reservedBytes(scope))).isZero();
adapter/outbound/persistence-jpa · postgresqlIntegrationTest · PostgreSqlFileserverReclamationIntegrationTest.java:292 assertThat(inTransaction(() -> quota.committedBytes(scope))).isEqualTo(600);
adapter/outbound/persistence-jpa · postgresqlIntegrationTest · PostgreSqlFileserverReclamationIntegrationTest.java:307 assertThat(inTransaction(() -> quota.committedBytes(scope))).isEqualTo(450);
adapter/outbound/persistence-jpa · postgresqlIntegrationTest · PostgreSqlFileserverReclamationIntegrationTest.java:323 assertThat(inTransaction(() -> quota.reservedBytes(scope))).isZero();
adapter/outbound/persistence-jpa · postgresqlIntegrationTest · PostgreSqlFileserverReclamationIntegrationTest.java:324 assertThat(inTransaction(() -> quota.committedBytes(scope))).isZero();
adapter/outbound/persistence-jpa · postgresqlIntegrationTest · PostgreSqlFileserverReclamationIntegrationTest.java:345 assertThat(inTransaction(() -> quota.committedBytes(scope))).isEqualTo(200);
adapter/outbound/persistence-jpa · postgresqlIntegrationTest · PostgreSqlFileserverReclamationIntegrationTest.java:365 assertThat(inTransaction(() -> quota.committedBytes(scope))).isZero();
application-core · test · FakeFileQuotaService.java:59 public List<Long> committedBytes() {
application-core · test · FakeQuotaCommitGateway.java:24 public long committedBytes() {
application-core · test · FinalizeUploadServiceTest.java:104 assertThat(quota.committedBytes()).isEqualTo(10);
그중 정의 파일 밖의 main 호출자 : 0 줄
[대조] acquireUpload 를 부르는 main 줄 : 2 줄
# 설정에 바이트 상한이 있는가
application.yml:810 fileserver:
application.yml:811 enabled: ${APP_FILESERVER_ENABLED:false}
application.yml:812 destinations:
application.yml:813 local-export:
application.yml:814 provider-ref: local-primary
application.yml:815 required-publication: unique-atomic-create
application.yml:816 required-durability: file-and-directory-sync
application.yml:817 maximum-rows: 1000000
application.yml:818 maximum-encoded-bytes: 1073741824
application.yml:819 providers:
application.yml:820 local-primary:
application.yml:821 # local-persistent is the only implemented/qualified R2 provider.
application.yml:822 # shared-mounted/NFS and SFTP settings must not be added before their providers exist.
application.yml:823 type: local-persistent
application.yml:824 root-directory: ${APP_FILESERVER_LOCAL_ROOT:}
application.yml:825 auto-create: false
application.yml:826 strict-path-security: true
application.yml:827 expected-file-store-name: ${APP_FILESERVER_LOCAL_EXPECTED_FILE_STORE_NAME:}
application.yml:828 expected-file-store-type: ${APP_FILESERVER_LOCAL_EXPECTED_FILE_STORE_TYPE:}
application.yml:829 mount-sentinel-name: .ca-fileserver-volume
application.yml:830 mount-sentinel-sha256: ${APP_FILESERVER_LOCAL_MOUNT_SENTINEL_SHA256:}
application.yml:831 expected-owner: ${APP_FILESERVER_LOCAL_EXPECTED_OWNER:}
application.yml:832 maximum-root-mode: "0750"
범위별 바이트 상한 이름이 나오는 자리 :
fileserver 안에서 그 이름을 쓰는 자리 : 0 개
승인이 읽는 프로퍼티 :
:40 this.instanceUploads = new Semaphore(properties.instanceUploadPermits());
:41 this.directDownloads = new Semaphore(properties.directDownloadPermits());
:80 return used.isPresent() && used.getAsDouble() >= properties.softHighWater();
:93 if (used.isPresent() && used.getAsDouble() >= properties.hardHighWater()) {
:102 scope.canonicalKey(), ignored -> new Semaphore(properties.scopeUploadPermits()));