# 설계 문서가 쿼터를 무엇으로 정의하는가
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 *
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 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 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()));