ChatAttachmentCleanupService.java
package com.ecommerce.chat.application.service;
import com.ecommerce.chat.application.port.ChatAttachmentStorage;
import com.ecommerce.chat.infrastructure.persistence.entity.ChatAttachmentUploadEntity;
import com.ecommerce.chat.infrastructure.persistence.mapper.ChatAttachmentUploadMapper;
import com.ecommerce.chat.infrastructure.storage.ChatAttachmentStorageProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;
import java.time.Instant;
import java.util.List;
@Service
public class ChatAttachmentCleanupService {
private static final Logger log = LoggerFactory.getLogger(ChatAttachmentCleanupService.class);
private final ChatAttachmentUploadMapper uploadMapper;
private final ChatAttachmentStorage storage;
private final ChatAttachmentStorageProperties properties;
private final TransactionTemplate transactions;
public ChatAttachmentCleanupService(
ChatAttachmentUploadMapper uploadMapper,
ChatAttachmentStorage storage,
ChatAttachmentStorageProperties properties,
TransactionTemplate transactions) {
this.uploadMapper = uploadMapper;
this.storage = storage;
this.properties = properties;
this.transactions = transactions;
}
public int cleanupBatch() {
Instant now = uploadMapper.currentTime();
Instant recoverBefore = now.minus(properties.cleanupRecoveryAge());
List<ChatAttachmentUploadEntity> candidates = uploadMapper.selectCleanupCandidates(
now,
recoverBefore,
properties.cleanupBatchSize());
int cleaned = 0;
for (ChatAttachmentUploadEntity candidate : candidates) {
int claimedAttempt = candidate.getCleanupAttempts() + 1;
Boolean claimed = transactions.execute(status ->
uploadMapper.claimCleanup(
candidate.getId(),
candidate.getCleanupAttempts(),
uploadMapper.currentTime(),
recoverBefore) == 1);
if (!Boolean.TRUE.equals(claimed)) {
continue;
}
try {
storage.remove(properties.bucket(), candidate.getObjectKey());
Boolean deleted = transactions.execute(status ->
uploadMapper.markDeleted(
candidate.getId(),
claimedAttempt,
uploadMapper.currentTime()) == 1);
if (!Boolean.TRUE.equals(deleted)) {
throw new IllegalStateException(
"Attachment cleanup state changed before completion");
}
cleaned++;
} catch (RuntimeException exception) {
String error = conciseError(exception);
transactions.executeWithoutResult(status ->
uploadMapper.markCleanupPending(
candidate.getId(),
claimedAttempt,
error,
uploadMapper.currentTime()));
log.warn("Expired chat attachment cleanup failed and remains retryable: uploadId={}, error={}",
candidate.getId(), error);
}
}
return cleaned;
}
public int cleanupQuarantineBatch() {
Instant now = uploadMapper.currentTime();
Instant recoverBefore = now.minus(properties.cleanupRecoveryAge());
List<ChatAttachmentUploadEntity> candidates =
uploadMapper.selectQuarantineCleanupCandidates(
recoverBefore,
properties.cleanupBatchSize());
int cleaned = 0;
for (ChatAttachmentUploadEntity candidate : candidates) {
String objectKey = candidate.getQuarantineObjectKey();
int claimedAttempt = candidate.getQuarantineCleanupAttempts() + 1;
Instant claimedAt = uploadMapper.currentTime();
Boolean claimed = transactions.execute(status ->
uploadMapper.claimQuarantineCleanup(
candidate.getId(),
objectKey,
candidate.getQuarantineCleanupAttempts(),
claimedAt,
recoverBefore) == 1);
if (!Boolean.TRUE.equals(claimed)) {
continue;
}
try {
storage.remove(properties.bucket(), objectKey);
Boolean updated = transactions.execute(status ->
uploadMapper.markClaimedQuarantineCleaned(
candidate.getId(),
objectKey,
claimedAttempt,
uploadMapper.currentTime()) == 1);
if (Boolean.TRUE.equals(updated)) {
cleaned++;
}
} catch (RuntimeException exception) {
String error = conciseError(exception);
transactions.executeWithoutResult(status ->
uploadMapper.markClaimedQuarantineCleanupFailed(
candidate.getId(),
objectKey,
claimedAttempt,
error,
uploadMapper.currentTime()));
log.warn(
"Quarantine chat attachment cleanup failed and remains retryable: "
+ "uploadId={}, error={}",
candidate.getId(),
error);
}
}
return cleaned;
}
private String conciseError(RuntimeException exception) {
String message = exception.getClass().getSimpleName() + ": " + exception.getMessage();
return message.length() <= 500 ? message : message.substring(0, 500);
}
}