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