- Git -> PROD 改为按 branch 作为 configVersion - 按 airportId/appName/fileName 目录结构解析 pushConfig 参数 - PROD -> Git 改为写入 snapshot-branch/<configVersion> 动态分支 - pullConfig 支持 configVersion/fileName 可选过滤 - 抽出 ConfigCryptoService,统一收口加解密扩展点 - ackFail 落库增加重试上下文,支持按 airportId/appName/configVersion/fileName 定向重拉 - 同步更新测试、接口文档和 current.md
278 lines
10 KiB
Java
278 lines
10 KiB
Java
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<String> successIds = toRemoteIds(
|
|
prodPullAckRecordRepository.findByAckStatusAndReportedFalseOrderByUpdatedAtAsc(ProdPullAckStatus.SUCCESS)
|
|
);
|
|
List<String> failedIds = toRemoteIds(
|
|
prodPullAckRecordRepository.findByAckStatusAndReportedFalseOrderByUpdatedAtAsc(ProdPullAckStatus.FAILED)
|
|
);
|
|
return new PendingAckSummary(successIds, failedIds);
|
|
}
|
|
|
|
@Transactional
|
|
public void recordAckSuccess(Collection<ProdPullResult.PulledConfigRef> 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<ProdPullResult.PulledConfigRef> 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<RetryPullRequest> getRetryPullRequests(int maxRetryCount) {
|
|
List<ProdPullAckRecord> failedRecords = prodPullAckRecordRepository.findByAckStatusOrderByUpdatedAtAsc(ProdPullAckStatus.FAILED);
|
|
LocalDateTime now = LocalDateTime.now();
|
|
Map<String, RetryPullRequest> retryPullRequests = new LinkedHashMap<String, RetryPullRequest>();
|
|
|
|
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<RetryPullRequest>(retryPullRequests.values());
|
|
}
|
|
|
|
@Transactional
|
|
public void markPendingAsReported() {
|
|
List<ProdPullAckRecord> 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<String> toRemoteIds(List<ProdPullAckRecord> records) {
|
|
List<String> ids = new ArrayList<String>();
|
|
for (ProdPullAckRecord record : records) {
|
|
ids.add(record.getRemoteConfigId());
|
|
}
|
|
return ids;
|
|
}
|
|
|
|
public static class PendingAckSummary {
|
|
|
|
private final List<String> ackSucIds;
|
|
private final List<String> ackFailIds;
|
|
|
|
public PendingAckSummary(List<String> ackSucIds, List<String> ackFailIds) {
|
|
this.ackSucIds = ackSucIds;
|
|
this.ackFailIds = ackFailIds;
|
|
}
|
|
|
|
public List<String> getAckSucIds() {
|
|
return ackSucIds;
|
|
}
|
|
|
|
public List<String> 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<String> remoteConfigIds = new ArrayList<String>();
|
|
|
|
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<String> getRemoteConfigIds() {
|
|
return remoteConfigIds;
|
|
}
|
|
|
|
private void addRemoteConfigId(String remoteConfigId) {
|
|
this.remoteConfigIds.add(remoteConfigId);
|
|
}
|
|
}
|
|
}
|