# 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 은 이렇게 적는다
 * <p>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);
