package com.ftptool.sync.service; import com.ftptool.sync.entity.ProdPullAckRecord; import com.ftptool.sync.model.ProdPullAckStatus; import com.ftptool.sync.model.ProdPullResult; import com.ftptool.sync.repository.ProdPullAckRecordRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** * Stores pullConfig ack states and retry plans. */ @Service public class ProdPullAckService { private static final int RETRY_DELAY_BASE_SECONDS = 30; private final ProdPullAckRecordRepository prodPullAckRecordRepository; public ProdPullAckService(ProdPullAckRecordRepository prodPullAckRecordRepository) { this.prodPullAckRecordRepository = prodPullAckRecordRepository; } @Transactional(readOnly = true) public PendingAckSummary getPendingAckSummary() { List successIds = toRemoteIds( prodPullAckRecordRepository.findByAckStatusAndReportedFalseOrderByUpdatedAtAsc(ProdPullAckStatus.SUCCESS) ); List failedIds = toRemoteIds( prodPullAckRecordRepository.findByAckStatusAndReportedFalseOrderByUpdatedAtAsc(ProdPullAckStatus.FAILED) ); return new PendingAckSummary(successIds, failedIds); } @Transactional public void recordAckSuccess(Collection pulledConfigs) { if (pulledConfigs == null) { return; } for (ProdPullResult.PulledConfigRef pulledConfig : pulledConfigs) { if (!hasRemoteId(pulledConfig)) { continue; } ProdPullAckRecord record = prodPullAckRecordRepository.findByRemoteConfigId(pulledConfig.getRemoteConfigId()) .orElseGet(ProdPullAckRecord::new); record.setRemoteConfigId(pulledConfig.getRemoteConfigId()); record.setAckStatus(ProdPullAckStatus.SUCCESS); record.setReported(Boolean.FALSE); applyPullMetadata(record, pulledConfig); record.setRetryCount(0); record.setNextRetryAt(null); record.setLastErrorMsg(null); prodPullAckRecordRepository.save(record); } } @Transactional public void recordPullFailure( Collection pulledConfigs, String errorMsg, boolean retryAttempt ) { if (pulledConfigs == null) { return; } for (ProdPullResult.PulledConfigRef pulledConfig : pulledConfigs) { if (!hasRemoteId(pulledConfig)) { continue; } ProdPullAckRecord record = prodPullAckRecordRepository.findByRemoteConfigId(pulledConfig.getRemoteConfigId()) .orElseGet(ProdPullAckRecord::new); record.setRemoteConfigId(pulledConfig.getRemoteConfigId()); record.setAckStatus(ProdPullAckStatus.FAILED); record.setReported(Boolean.FALSE); applyPullMetadata(record, pulledConfig); record.setLastErrorMsg(errorMsg); if (retryAttempt) { int retryCount = safeRetryCount(record) + 1; record.setRetryCount(retryCount); record.setNextRetryAt(calculateNextRetryAt(retryCount)); } else { record.setRetryCount(0); record.setNextRetryAt(LocalDateTime.now()); } prodPullAckRecordRepository.save(record); } } @Transactional public void markRetryAttemptFailed(RetryPullRequest retryPullRequest, String errorMsg) { if (retryPullRequest == null || retryPullRequest.getRemoteConfigIds().isEmpty()) { return; } for (String remoteConfigId : retryPullRequest.getRemoteConfigIds()) { prodPullAckRecordRepository.findByRemoteConfigId(remoteConfigId).ifPresent(record -> { int retryCount = safeRetryCount(record) + 1; record.setRetryCount(retryCount); record.setNextRetryAt(calculateNextRetryAt(retryCount)); record.setLastErrorMsg(errorMsg); record.setAckStatus(ProdPullAckStatus.FAILED); prodPullAckRecordRepository.save(record); }); } } @Transactional(readOnly = true) public List getRetryPullRequests(int maxRetryCount) { List failedRecords = prodPullAckRecordRepository.findByAckStatusOrderByUpdatedAtAsc(ProdPullAckStatus.FAILED); LocalDateTime now = LocalDateTime.now(); Map retryPullRequests = new LinkedHashMap(); for (ProdPullAckRecord failedRecord : failedRecords) { if (!hasRetryMetadata(failedRecord)) { continue; } if (safeRetryCount(failedRecord) >= maxRetryCount) { continue; } if (failedRecord.getNextRetryAt() != null && failedRecord.getNextRetryAt().isAfter(now)) { continue; } String groupKey = buildRetryGroupKey(failedRecord); RetryPullRequest retryPullRequest = retryPullRequests.get(groupKey); if (retryPullRequest == null) { retryPullRequest = new RetryPullRequest( failedRecord.getSourceVersion(), failedRecord.getAirportId(), failedRecord.getAppName(), failedRecord.getFileName() ); retryPullRequests.put(groupKey, retryPullRequest); } retryPullRequest.addRemoteConfigId(failedRecord.getRemoteConfigId()); } return new ArrayList(retryPullRequests.values()); } @Transactional public void markPendingAsReported() { List records = prodPullAckRecordRepository.findByReportedFalseOrderByUpdatedAtAsc(); for (ProdPullAckRecord record : records) { record.setReported(Boolean.TRUE); prodPullAckRecordRepository.save(record); } } private void applyPullMetadata(ProdPullAckRecord record, ProdPullResult.PulledConfigRef pulledConfig) { if (pulledConfig == null) { return; } if (StringUtils.hasText(pulledConfig.getConfigVersion())) { record.setSourceVersion(pulledConfig.getConfigVersion().trim()); } if (StringUtils.hasText(pulledConfig.getAirportId())) { record.setAirportId(pulledConfig.getAirportId().trim()); } if (StringUtils.hasText(pulledConfig.getAppName())) { record.setAppName(pulledConfig.getAppName().trim()); } if (StringUtils.hasText(pulledConfig.getFileName())) { record.setFileName(pulledConfig.getFileName().trim()); } } private boolean hasRemoteId(ProdPullResult.PulledConfigRef pulledConfig) { return pulledConfig != null && StringUtils.hasText(pulledConfig.getRemoteConfigId()); } private boolean hasRetryMetadata(ProdPullAckRecord failedRecord) { return StringUtils.hasText(failedRecord.getSourceVersion()) && StringUtils.hasText(failedRecord.getAirportId()) && StringUtils.hasText(failedRecord.getAppName()) && StringUtils.hasText(failedRecord.getFileName()); } private int safeRetryCount(ProdPullAckRecord record) { return record.getRetryCount() == null ? 0 : record.getRetryCount().intValue(); } private LocalDateTime calculateNextRetryAt(int retryCount) { long delaySeconds = RETRY_DELAY_BASE_SECONDS * (1L << Math.max(0, retryCount - 1)); return LocalDateTime.now().plusSeconds(delaySeconds); } private String buildRetryGroupKey(ProdPullAckRecord failedRecord) { return failedRecord.getSourceVersion() + "|" + failedRecord.getAirportId() + "|" + failedRecord.getAppName() + "|" + failedRecord.getFileName(); } private List toRemoteIds(List records) { List ids = new ArrayList(); for (ProdPullAckRecord record : records) { ids.add(record.getRemoteConfigId()); } return ids; } public static class PendingAckSummary { private final List ackSucIds; private final List ackFailIds; public PendingAckSummary(List ackSucIds, List ackFailIds) { this.ackSucIds = ackSucIds; this.ackFailIds = ackFailIds; } public List getAckSucIds() { return ackSucIds; } public List getAckFailIds() { return ackFailIds; } public boolean hasPendingAck() { return !(ackSucIds == null || ackSucIds.isEmpty()) || !(ackFailIds == null || ackFailIds.isEmpty()); } } public static class RetryPullRequest { private final String sourceVersion; private final String airportId; private final String appName; private final String fileName; private final List remoteConfigIds = new ArrayList(); public RetryPullRequest(String sourceVersion, String airportId, String appName, String fileName) { this.sourceVersion = sourceVersion; this.airportId = airportId; this.appName = appName; this.fileName = fileName; } public String getSourceVersion() { return sourceVersion; } public String getAirportId() { return airportId; } public String getAppName() { return appName; } public String getFileName() { return fileName; } public List getRemoteConfigIds() { return remoteConfigIds; } private void addRemoteConfigId(String remoteConfigId) { this.remoteConfigIds.add(remoteConfigId); } } }