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>
133 lines
11 KiB
Plaintext
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()));
|