# 회수 메서드가 자기 javadoc 에 적어 둔 것 108: * because {@code recordAttempt} matches on it too. 109: * 110: *

The item comes back as FAILED rather than PENDING, and its attempt counter advances. An item 111: * whose worker dies every time is then bounded by the same retry budget as one that fails 112: * outright, instead of being reclaimed forever. 113: * 114: * @return 1 when this caller reclaimed it, 0 when someone else already had 115: */ 116: @Modifying(clearAutomatically = true, flushAutomatically = true) 117: @Query( 118: """ 119: update CleanupItemEntity c 120: set c.status = 'FAILED', 121: c.attempt = c.attempt + 1, 122: c.nextAttemptAt = :now, 123: c.lastErrorCode = 'CLAIM_LEASE_EXPIRED', 124: c.claimOwner = null, 125: c.claimToken = null, 126: c.leaseUntil = null, 127: c.updatedAt = :now 128: where c.cleanupId = :cleanupId 129: and c.claimToken = :token 130: and c.status = 'IN_PROGRESS' 131: """) # 저장소 전체에서 attempt 를 비교하는 줄: 2 # 저장소가 attempt 에 하는 일 전부 57: * Records the outcome of an attempt, for the worker that still holds the claim. 68: c.attempt = c.attempt + 1, 110: *

The item comes back as FAILED rather than PENDING, and its attempt counter advances. An item 121: c.attempt = c.attempt + 1, # 예산을 끊는 곳은 코드베이스에 한 군데다 JpaCleanupQueue.java:45: private static final String STATUS_ABANDONED = "ABANDONED"; JpaCleanupQueue.java:150: exhausted ? STATUS_ABANDONED : STATUS_FAILED, CleanupType.java:16: ABANDONED_LEASE, 145: public void markFailed(CleanupItem item, String reasonCode, Instant nextAttemptAt) { 146- Instant now = clock.instant(); 147- boolean exhausted = item.attempt() + 1 >= MAXIMUM_ATTEMPTS; 148- settle( 149- item.cleanupId(), 150- exhausted ? STATUS_ABANDONED : STATUS_FAILED, 151- nextAttemptAt, 152- reasonCode, 153- now); 154- } # 회수 대상을 고르는 조회 — 만료된 것만, 시도 한계 없음 93: @Query( 94: """ 95: select c from CleanupItemEntity c 96: where c.status = 'IN_PROGRESS' 97: and c.leaseUntil is not null 98: and c.leaseUntil < :now 99: order by c.leaseUntil 100: """) 101: List findExpiredClaims(@Param("now") Instant now, Limit limit); # 다시 청구되는 조건 — 상태만이 아니라 다음 시도 시각도 본다 16: @Query( 17: """ 18: select c from CleanupItemEntity c 19: where c.status in ('PENDING', 'FAILED') 20: and c.nextAttemptAt <= :now 21: order by c.nextAttemptAt asc 22: """) 23: List findDue(@Param("now") Instant now, Limit limit); # 회수 래퍼가 next_attempt_at 에 넣는 시각 122: public int reclaimExpiredClaims(Instant now, int limit) { 123- if (limit < 1) { 124- throw new IllegalArgumentException("limit must be positive"); 125- } 126- int reclaimed = 0; 127- for (CleanupItemEntity abandoned : items.findExpiredClaims(now, Limit.of(limit))) { 128- // Matched on the token that was read, so the reaper that loses the race changes nothing — 129- // and the worker that eventually wakes up finds its own token gone and settles nothing. 130- reclaimed += 131- items.reclaimExpiredClaim( 132- abandoned.getCleanupId(), abandoned.getClaimToken(), clock.instant()); 133- heldClaims.remove(abandoned.getCleanupId()); 134- } 135- return reclaimed; 136- } # 배치는 시각을 한 번 잡아 회수와 청구에 같이 쓴다 69: Instant now = clock.instant(); 70- // Abandoned claims come back first. A claim takes an item out of the due set, so an item whose 71- // worker performed the physical delete and then died is invisible to claimDue — nothing ever 72- // reclaimed it, and the file's quota and lifecycle stayed unsettled for good. Reclaiming before 73- // claiming means the recovered items are eligible in this same pass. 74- transactions.inWrite(() -> queue.reclaimExpiredClaims(now, maxItems)); 75- List due = transactions.inWrite(() -> queue.claimDue(now, maxItems)); 76- 77- int deleted = 0; 78- int skippedActiveLease = 0; 79- int skippedStateChanged = 0; 71: private static final java.time.Duration LEASE = java.time.Duration.ofMinutes(10); # 정상 실패 경로는 실제로 불린다 87: markFailed(item, "BATCH_BYTE_BUDGET_EXHAUSTED", now); 123: markFailed(item, "CLEANUP_ATTEMPT_FAILED", now); 140: markFailed(item, "ACTIVE_WRITER_LEASE", now); 209: queue.markFailed(item, reasonCode, now.plus(retryBackoff)); 121- return Outcome.of(OutcomeKind.SKIPPED_STATE_CHANGED, 0); 122- } catch (RuntimeException failure) { 123: markFailed(item, "CLEANUP_ATTEMPT_FAILED", now); 124- return Outcome.of(OutcomeKind.FAILED, 0); 125- } 126- } # 도달성 51: return registrar -> registrar.addFixedDelayTask(worker::runBatch, interval); 854: enabled: ${APP_FILESERVER_PLATFORM_ENABLED:false} 914: enabled: ${APP_FILESERVER_PLATFORM_CLEANUP_ENABLED:false}