# V4 가 더한 상태 컬럼과 제약, 그리고 부분 인덱스 ALTER TABLE fs_upload_session ADD COLUMN IF NOT EXISTS lifecycle_state varchar(16) NOT NULL DEFAULT 'ACTIVE'; ALTER TABLE fs_upload_session ADD CONSTRAINT ck_fs_upload_lifecycle_state CHECK (lifecycle_state IN ('ACTIVE', 'TERMINAL')); -- Cleanup's claim: terminal sessions whose lease has lapsed. CREATE INDEX IF NOT EXISTS ix_fs_upload_session_terminal ON fs_upload_session (lease_until) WHERE lifecycle_state = 'TERMINAL'; # 이 저장소의 쓰기·전이 문장 여섯 40: int acquireLease( 60: int renewLease( 79: int commitOffset( 98: int releaseLease( 119: int terminate(@Param("uploadId") UUID uploadId, @Param("now") Instant now); 150: int claimForCleanup(@Param("uploadId") UUID uploadId, @Param("now") Instant now); # 그중 활성 상태를 조건으로 거는 줄 37: and s.lifecycleState = 'ACTIVE' 58: and s.lifecycleState = 'ACTIVE' 76: and s.lifecycleState = 'ACTIVE' 117: and s.lifecycleState = 'ACTIVE' # 클래스 javadoc 은 이렇게 적는다 *

Every writer statement also requires the session to be {@code ACTIVE}. Without that clause a * cancelled upload still handed out leases: acquire looked at the upload's expiry and the held # 그러나 리스 반납 문장의 조건은 이것뿐이다 where s.uploadId = :uploadId and s.leaseToken = :token # 최종화와 정리 청구가 요구하는 상태 set s.lifecycleState = 'TERMINAL', and s.lifecycleState = 'ACTIVE' and s.lifecycleState = 'TERMINAL' and (s.leaseUntil is null or s.leaseUntil <= :now) # 취소 경로 — 트랜잭션 시작부터 끝까지 연속 transactions.inWrite( () -> { // Logical first: the record must stop being reachable before any physical work is // scheduled. Both writes commit together, so no cancel can leave a file unreachable with // nothing queued to reclaim it. metadataStore.markDeleting(record.fileId(), record.version()); // Terminal in the same transaction that queues the cleanup. Queuing alone left the // session ACTIVE, so a writer could still take a lease on the very bytes the cleanup was // about to delete and the two raced for the same object. sessionStore.terminate(session.uploadId()); cleanupQueue.enqueue( CleanupRequest.forStaging( CleanupType.CANCELLED_STAGING, record.fileId(), session.uploadId())); }); # 검증 실패 경로 — 같은 람다의 시작과 세 쓰기와 끝 transactions.inWrite( () -> { FileRecord moved = metadataStore.transition( verifying.fileId(), ...... sessionStore.terminate(session.uploadId()); cleanupQueue.enqueue( CleanupRequest.forStaging( CleanupType.FAILED_VERIFICATION_CONTENT, moved.fileId(), session.uploadId())); return moved; }); # 정리는 세션을 먼저 읽고, 있을 때만 청구한다 if (sessionStore.find(uploadId).isPresent() && !sessionStore.claimForCleanup(uploadId, now)) { markFailed(item, "ACTIVE_WRITER_LEASE", now); return Outcome.of(OutcomeKind.SKIPPED_ACTIVE_LEASE, 0); } contentGateway.discardStaging(uploadId);