feat(sync): 将 pullConfig 的 ACK 确认拆分为独立请求
- 正常 pullConfig 请求不再夹带 ackSuc/ackFail,只保留业务过滤参数 - 在本轮生产数据处理完成后,新增一次 ACK-only 的 pullConfig 确认调用 - ACK 确认请求仅回传 ackSuc/ackFail,不再携带 airportId/appName/configVersion/fileName - 保持本地 ackFail 落库与定向重拉机制不变,确认成功后再标记 reported - 更新相关 HTTP/集成测试,并同步 current.md 与 prod-api-v1.md 文档
This commit is contained in:
parent
00b81bf7ef
commit
c22eff8950
62
current.md
62
current.md
@ -11,6 +11,7 @@ pushConfig:POST + JSON数组
|
|||||||
pullConfig:GET + JSON响应
|
pullConfig:GET + JSON响应
|
||||||
login:已支持 token 获取与缓存
|
login:已支持 token 获取与缓存
|
||||||
ackSuc/ackFail:已接入回传与本地落库
|
ackSuc/ackFail:已接入回传与本地落库
|
||||||
|
ackSuc/ackFail:已改为 pull 数据处理完成后,再补发一次 ACK-only pullConfig 确认请求(只带 ackSuc/ackFail)
|
||||||
ConfigCryptoService:已抽出,当前默认透传实现,后续只需替换该服务内算法
|
ConfigCryptoService:已抽出,当前默认透传实现,后续只需替换该服务内算法
|
||||||
Git -> PROD:已支持最小增量推送,删除场景自动回退全量
|
Git -> PROD:已支持最小增量推送,删除场景自动回退全量
|
||||||
Git -> PROD:已改为先按 git.repo.scan-branch-pattern 批量扫描版本分支,再逐个按“版本分支 + 机场目录 + 模块目录”解析参数
|
Git -> PROD:已改为先按 git.repo.scan-branch-pattern 批量扫描版本分支,再逐个按“版本分支 + 机场目录 + 模块目录”解析参数
|
||||||
@ -45,6 +46,7 @@ PROD -> Git:
|
|||||||
- 当前提交目标分支为:git.repo.snapshot-branch/<configVersion>
|
- 当前提交目标分支为:git.repo.snapshot-branch/<configVersion>
|
||||||
- 例如:config-prod-snapshot/R_XXX_V3.0.3_XXX
|
- 例如:config-prod-snapshot/R_XXX_V3.0.3_XXX
|
||||||
- 若 pullConfig 返回缺少统一版本号,则按当前 result 的 sourceVersion 退化生成动态分支名
|
- 若 pullConfig 返回缺少统一版本号,则按当前 result 的 sourceVersion 退化生成动态分支名
|
||||||
|
- pull 数据处理完成后,需要再次调用一次 pullConfig 做 ACK 确认,请求只传 ackSuc/ackFail
|
||||||
|
|
||||||
关键文件
|
关键文件
|
||||||
|
|
||||||
@ -64,6 +66,66 @@ ftp-sync-tool-detail-design.md
|
|||||||
|
|
||||||
configContent 推送前加密
|
configContent 推送前加密
|
||||||
configContent 拉取后解密
|
configContent 拉取后解密
|
||||||
|
|
||||||
|
本轮评估结论(2026-04-29)
|
||||||
|
|
||||||
|
目标补充
|
||||||
|
|
||||||
|
最终目标希望 Git 仓库内容与生产接口库保持一致。
|
||||||
|
|
||||||
|
当前结论
|
||||||
|
|
||||||
|
- 当前正式模式不变:
|
||||||
|
- Git -> PROD:版本分支直推生产
|
||||||
|
- PROD -> Git:回写到 git.repo.snapshot-branch/<configVersion>
|
||||||
|
- 当前不建议直接把生产回流写回对应版本分支。
|
||||||
|
- 原因不是“代码不能写”,而是“当前 pullConfig 语义还不够安全”:
|
||||||
|
- pullConfig 不带过滤时,返回的是“未同步到 Git 的已审核配置”
|
||||||
|
- 这不是某个 configVersion 的全量快照
|
||||||
|
- 但当前 Git 回写语义是“整目录覆盖目标分支”
|
||||||
|
- 如果直接覆盖版本分支,可能误删未出现在本次 pull 结果里的文件
|
||||||
|
- 当前 snapshot 分支仍然有必要保留,作为生产回流的隔离区和审计区。
|
||||||
|
|
||||||
|
保守方案(推荐)
|
||||||
|
|
||||||
|
1. 保持当前双分支职责不变
|
||||||
|
- 版本分支:作为 Git -> PROD 的发布源
|
||||||
|
- snapshot 动态分支:作为 PROD -> Git 的生产回流镜像分支
|
||||||
|
|
||||||
|
2. 把 snapshot 分支定义为“生产待对账视图”
|
||||||
|
- 生产回流继续先写到 git.repo.snapshot-branch/<configVersion>
|
||||||
|
- 不直接改动 R_XXX_* 等版本分支
|
||||||
|
|
||||||
|
3. 增加“版本分支 vs snapshot 分支”自动对账能力
|
||||||
|
- 对比同一 configVersion 下两边目录差异
|
||||||
|
- 差异至少要区分:新增、修改、删除
|
||||||
|
- 输出可读结果,供人工确认是否需要把生产变更回收进版本分支
|
||||||
|
|
||||||
|
4. 增加“受控回收”动作,而不是自动覆盖
|
||||||
|
- 建议后续增加管理入口或命令:
|
||||||
|
- promoteSnapshotToVersion(configVersion)
|
||||||
|
- 行为:
|
||||||
|
- 读取 snapshot 分支
|
||||||
|
- 与对应版本分支做差异确认
|
||||||
|
- 经人工确认后再回写版本分支
|
||||||
|
- 这样可以避免生产临时变更直接污染发布分支
|
||||||
|
|
||||||
|
5. 明确回环控制
|
||||||
|
- Git -> PROD 只扫描版本分支
|
||||||
|
- snapshot 前缀分支不得进入 git.repo.scan-branch-pattern
|
||||||
|
- 避免生产回流提交再次被推回生产形成闭环
|
||||||
|
|
||||||
|
6. 后续如果要升级成“版本分支直接等于生产态”,前提是:
|
||||||
|
- 生产接口能按 configVersion 提供全量数据
|
||||||
|
- 或者返回结果具备明确删除语义
|
||||||
|
- 在此前,不建议取消 snapshot 隔离层
|
||||||
|
|
||||||
|
下一步建议
|
||||||
|
|
||||||
|
如果继续推进,最稳妥的下一步是:
|
||||||
|
|
||||||
|
- 先实现“snapshot 分支 与 对应版本分支”的差异对账能力
|
||||||
|
- 再决定是否补一个人工确认后的 promote 流程
|
||||||
验证状态
|
验证状态
|
||||||
|
|
||||||
mvn -s build-support/maven-settings.xml test 已通过
|
mvn -s build-support/maven-settings.xml test 已通过
|
||||||
|
|||||||
@ -197,25 +197,36 @@ PEK/monitor/jobs/sync-job.json
|
|||||||
说明:
|
说明:
|
||||||
|
|
||||||
- 由于接口是 `GET`,当前实现按 query string 传参
|
- 由于接口是 `GET`,当前实现按 query string 传参
|
||||||
- 当前实现已经支持:
|
- 正常拉取请求当前已经支持:
|
||||||
- `airportId` 可选过滤
|
- `airportId` 可选过滤
|
||||||
- `appName` 可选过滤
|
- `appName` 可选过滤
|
||||||
- `ackSuc` 自动回传
|
|
||||||
- `ackFail` 自动回传
|
|
||||||
- 当前已经支持:
|
|
||||||
- `configVersion` 可选过滤
|
- `configVersion` 可选过滤
|
||||||
- `fileName` 可选过滤
|
- `fileName` 可选过滤
|
||||||
|
- ACK 确认请求当前已经支持:
|
||||||
|
- `ackSuc` 自动回传
|
||||||
|
- `ackFail` 自动回传
|
||||||
|
|
||||||
### 5.3 当前请求行为
|
### 5.3 当前请求行为
|
||||||
|
|
||||||
当前代码只有在配置了过滤条件时才会发送:
|
当前代码发起两类 `pullConfig` 请求:
|
||||||
|
|
||||||
|
1. 正常拉取请求:
|
||||||
|
|
||||||
|
- 只有在配置了过滤条件时才会发送:
|
||||||
- `prod.api.airport-id`
|
- `prod.api.airport-id`
|
||||||
- `prod.api.app-name`
|
- `prod.api.app-name`
|
||||||
- `prod.api.pull-config-version`
|
- `prod.api.pull-config-version`
|
||||||
- `prod.api.pull-file-name`
|
- `prod.api.pull-file-name`
|
||||||
|
|
||||||
如果这两个配置为空,则不会在请求中携带它们,此时依赖生产端返回所有已审核且未同步的配置。
|
如果这些配置为空,则不会在请求中携带它们,此时依赖生产端返回所有已审核且未同步的配置。
|
||||||
|
|
||||||
|
2. ACK 确认请求:
|
||||||
|
|
||||||
|
- 在本轮 pull 数据处理完成后额外发起一次
|
||||||
|
- 请求只传:
|
||||||
|
- `ackSuc`
|
||||||
|
- `ackFail`
|
||||||
|
- 不再夹带 `airportId/appName/configVersion/fileName` 过滤参数
|
||||||
|
|
||||||
### 5.4 响应体
|
### 5.4 响应体
|
||||||
|
|
||||||
@ -253,6 +264,7 @@ PEK/monitor/jobs/sync-job.json
|
|||||||
- 如果某组内所有项的 `configVersion` 相同,则该值作为该组的 `sourceVersion`
|
- 如果某组内所有项的 `configVersion` 相同,则该值作为该组的 `sourceVersion`
|
||||||
- 如果某组缺少统一版本号,则该组退回为内容哈希作为 `sourceVersion`
|
- 如果某组缺少统一版本号,则该组退回为内容哈希作为 `sourceVersion`
|
||||||
- `configContent` 当前已统一经过 `ConfigCryptoService`,默认实现仍为透传
|
- `configContent` 当前已统一经过 `ConfigCryptoService`,默认实现仍为透传
|
||||||
|
- pull 结果本地处理完成后,会再补发一次 ACK-only `pullConfig` 请求,只带 `ackSuc/ackFail`
|
||||||
- 当本地处理失败时,会把失败项写入 ACK 重试表,并在下次按 `airportId/appName/configVersion/fileName` 定向重拉
|
- 当本地处理失败时,会把失败项写入 ACK 重试表,并在下次按 `airportId/appName/configVersion/fileName` 定向重拉
|
||||||
|
|
||||||
## 6. 接口三:获取 token
|
## 6. 接口三:获取 token
|
||||||
@ -302,7 +314,7 @@ PEK/monitor/jobs/sync-job.json
|
|||||||
- `pushConfig` 使用 `POST + JSON 数组`
|
- `pushConfig` 使用 `POST + JSON 数组`
|
||||||
- `pullConfig` 使用 `GET + JSON 响应`
|
- `pullConfig` 使用 `GET + JSON 响应`
|
||||||
- `login` 使用 `POST + JSON`
|
- `login` 使用 `POST + JSON`
|
||||||
- `ackSuc/ackFail` 已接入请求回传和本地状态更新
|
- `ackSuc/ackFail` 已接入 ACK-only 二次确认请求和本地状态更新
|
||||||
- `pushConfig` 参数已按 `airportId/appName/fileName` 目录结构解析
|
- `pushConfig` 参数已按 `airportId/appName/fileName` 目录结构解析
|
||||||
- `pullConfig` 响应已按 `airportId/appName/fileName` 结构恢复到本地目录
|
- `pullConfig` 响应已按 `airportId/appName/fileName` 结构恢复到本地目录
|
||||||
|
|
||||||
|
|||||||
@ -129,6 +129,7 @@ public class ProdSyncCoordinator {
|
|||||||
gitRepoProperties.getSnapshotBranch()
|
gitRepoProperties.getSnapshotBranch()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
retryFailedProdPulls();
|
retryFailedProdPulls();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -139,6 +140,9 @@ public class ProdSyncCoordinator {
|
|||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("PROD prod->git sync failed before new snapshot groups were processed", e);
|
log.error("PROD prod->git sync failed before new snapshot groups were processed", e);
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
confirmPendingPullAck();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean shouldSkip(Optional<SyncTask> existing) {
|
private boolean shouldSkip(Optional<SyncTask> existing) {
|
||||||
@ -301,6 +305,14 @@ public class ProdSyncCoordinator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void confirmPendingPullAck() {
|
||||||
|
try {
|
||||||
|
prodConfigApiService.confirmPendingPullAck();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("PROD prod->git ACK confirm failed", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 计算本轮 Git -> PROD 实际要推送的目录:
|
* 计算本轮 Git -> PROD 实际要推送的目录:
|
||||||
* 1. 首次同步走全量
|
* 1. 首次同步走全量
|
||||||
|
|||||||
@ -115,7 +115,6 @@ public class ProdConfigApiService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 按指定过滤条件拉取生产配置。
|
* 按指定过滤条件拉取生产配置。
|
||||||
* 这里会把 ACK 待回传参数一并带上。
|
|
||||||
*/
|
*/
|
||||||
public List<ProdPullResult> pullConfigSnapshots(
|
public List<ProdPullResult> pullConfigSnapshots(
|
||||||
String airportIdFilter,
|
String airportIdFilter,
|
||||||
@ -123,45 +122,15 @@ public class ProdConfigApiService {
|
|||||||
String configVersionFilter,
|
String configVersionFilter,
|
||||||
String fileNameFilter
|
String fileNameFilter
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
String url = buildUrl(prodApiProperties.getPullPath());
|
|
||||||
HttpHeaders headers = defaultJsonHeaders();
|
|
||||||
|
|
||||||
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url);
|
|
||||||
ProdPullAckService.PendingAckSummary pendingAckSummary = prodPullAckService.getPendingAckSummary();
|
|
||||||
|
|
||||||
// 过滤条件为空时,依赖生产端返回“已审核且未同步”的全量数据。
|
// 过滤条件为空时,依赖生产端返回“已审核且未同步”的全量数据。
|
||||||
if (StringUtils.hasText(airportIdFilter) && !isPlaceholder(airportIdFilter)) {
|
ProdApiResponse<List<ProdPulledConfigItem>> body = executePullConfigRequest(
|
||||||
builder.queryParam("airportId", airportIdFilter.trim());
|
airportIdFilter,
|
||||||
}
|
appNameFilter,
|
||||||
if (StringUtils.hasText(appNameFilter) && !isPlaceholder(appNameFilter)) {
|
configVersionFilter,
|
||||||
builder.queryParam("appName", appNameFilter.trim());
|
fileNameFilter,
|
||||||
}
|
null
|
||||||
if (StringUtils.hasText(configVersionFilter) && !isPlaceholder(configVersionFilter)) {
|
|
||||||
builder.queryParam("configVersion", configVersionFilter.trim());
|
|
||||||
}
|
|
||||||
if (StringUtils.hasText(fileNameFilter) && !isPlaceholder(fileNameFilter)) {
|
|
||||||
builder.queryParam("fileName", fileNameFilter.trim());
|
|
||||||
}
|
|
||||||
if (pendingAckSummary.getAckSucIds() != null && !pendingAckSummary.getAckSucIds().isEmpty()) {
|
|
||||||
builder.queryParam("ackSuc", StringUtils.collectionToCommaDelimitedString(pendingAckSummary.getAckSucIds()));
|
|
||||||
}
|
|
||||||
if (pendingAckSummary.getAckFailIds() != null && !pendingAckSummary.getAckFailIds().isEmpty()) {
|
|
||||||
builder.queryParam("ackFail", StringUtils.collectionToCommaDelimitedString(pendingAckSummary.getAckFailIds()));
|
|
||||||
}
|
|
||||||
|
|
||||||
ResponseEntity<ProdApiResponse<List<ProdPulledConfigItem>>> response = restTemplate.exchange(
|
|
||||||
builder.build(true).toUri(),
|
|
||||||
HttpMethod.GET,
|
|
||||||
new HttpEntity<Void>(headers),
|
|
||||||
new ParameterizedTypeReference<ProdApiResponse<List<ProdPulledConfigItem>>>() {
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
ProdApiResponse<List<ProdPulledConfigItem>> body = response.getBody();
|
|
||||||
validateSuccess(body, "Production pullConfig call failed");
|
validateSuccess(body, "Production pullConfig call failed");
|
||||||
if (pendingAckSummary.hasPendingAck()) {
|
|
||||||
prodPullAckService.markPendingAsReported();
|
|
||||||
}
|
|
||||||
|
|
||||||
List<ProdPulledConfigItem> items = body.getData();
|
List<ProdPulledConfigItem> items = body.getData();
|
||||||
if (items == null || items.isEmpty()) {
|
if (items == null || items.isEmpty()) {
|
||||||
@ -170,6 +139,32 @@ public class ProdConfigApiService {
|
|||||||
return buildPullResults(items);
|
return buildPullResults(items);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生产快照处理完成后,使用 ACK-only pull 请求向生产侧确认本轮处理结果。
|
||||||
|
* 当前约定只传 ackSuc / ackFail,不再夹带业务过滤参数。
|
||||||
|
*/
|
||||||
|
public void confirmPendingPullAck() throws IOException {
|
||||||
|
ProdPullAckService.PendingAckSummary pendingAckSummary = prodPullAckService.getPendingAckSummary();
|
||||||
|
if (!pendingAckSummary.hasPendingAck()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ProdApiResponse<List<ProdPulledConfigItem>> body = executePullConfigRequest(
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
pendingAckSummary
|
||||||
|
);
|
||||||
|
validateSuccess(body, "Production pullConfig ACK confirm failed");
|
||||||
|
prodPullAckService.markPendingAsReported();
|
||||||
|
log.info(
|
||||||
|
"Prod pullConfig ACK confirm finished. ackSucCount={}, ackFailCount={}",
|
||||||
|
pendingAckSummary.getAckSucIds() == null ? 0 : pendingAckSummary.getAckSucIds().size(),
|
||||||
|
pendingAckSummary.getAckFailIds() == null ? 0 : pendingAckSummary.getAckFailIds().size()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private HttpHeaders defaultJsonHeaders() {
|
private HttpHeaders defaultJsonHeaders() {
|
||||||
HttpHeaders headers = new HttpHeaders();
|
HttpHeaders headers = new HttpHeaders();
|
||||||
headers.setAccept(java.util.Collections.singletonList(MediaType.APPLICATION_JSON));
|
headers.setAccept(java.util.Collections.singletonList(MediaType.APPLICATION_JSON));
|
||||||
@ -231,6 +226,46 @@ public class ProdConfigApiService {
|
|||||||
return cachedToken;
|
return cachedToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ProdApiResponse<List<ProdPulledConfigItem>> executePullConfigRequest(
|
||||||
|
String airportIdFilter,
|
||||||
|
String appNameFilter,
|
||||||
|
String configVersionFilter,
|
||||||
|
String fileNameFilter,
|
||||||
|
ProdPullAckService.PendingAckSummary pendingAckSummary
|
||||||
|
) {
|
||||||
|
String url = buildUrl(prodApiProperties.getPullPath());
|
||||||
|
HttpHeaders headers = defaultJsonHeaders();
|
||||||
|
|
||||||
|
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url);
|
||||||
|
if (StringUtils.hasText(airportIdFilter) && !isPlaceholder(airportIdFilter)) {
|
||||||
|
builder.queryParam("airportId", airportIdFilter.trim());
|
||||||
|
}
|
||||||
|
if (StringUtils.hasText(appNameFilter) && !isPlaceholder(appNameFilter)) {
|
||||||
|
builder.queryParam("appName", appNameFilter.trim());
|
||||||
|
}
|
||||||
|
if (StringUtils.hasText(configVersionFilter) && !isPlaceholder(configVersionFilter)) {
|
||||||
|
builder.queryParam("configVersion", configVersionFilter.trim());
|
||||||
|
}
|
||||||
|
if (StringUtils.hasText(fileNameFilter) && !isPlaceholder(fileNameFilter)) {
|
||||||
|
builder.queryParam("fileName", fileNameFilter.trim());
|
||||||
|
}
|
||||||
|
if (pendingAckSummary != null && pendingAckSummary.getAckSucIds() != null && !pendingAckSummary.getAckSucIds().isEmpty()) {
|
||||||
|
builder.queryParam("ackSuc", StringUtils.collectionToCommaDelimitedString(pendingAckSummary.getAckSucIds()));
|
||||||
|
}
|
||||||
|
if (pendingAckSummary != null && pendingAckSummary.getAckFailIds() != null && !pendingAckSummary.getAckFailIds().isEmpty()) {
|
||||||
|
builder.queryParam("ackFail", StringUtils.collectionToCommaDelimitedString(pendingAckSummary.getAckFailIds()));
|
||||||
|
}
|
||||||
|
|
||||||
|
ResponseEntity<ProdApiResponse<List<ProdPulledConfigItem>>> response = restTemplate.exchange(
|
||||||
|
builder.build(true).toUri(),
|
||||||
|
HttpMethod.GET,
|
||||||
|
new HttpEntity<Void>(headers),
|
||||||
|
new ParameterizedTypeReference<ProdApiResponse<List<ProdPulledConfigItem>>>() {
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return response.getBody();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 把 Git 分支快照目录转换成 pushConfig 所需的 JSON 数组。
|
* 把 Git 分支快照目录转换成 pushConfig 所需的 JSON 数组。
|
||||||
* 当前目录约定必须是:airportId/appName/fileName
|
* 当前目录约定必须是:airportId/appName/fileName
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import com.ftptool.sync.repository.SyncTaskRepository;
|
|||||||
import com.ftptool.sync.service.GitClientService;
|
import com.ftptool.sync.service.GitClientService;
|
||||||
import com.ftptool.sync.service.PackageService;
|
import com.ftptool.sync.service.PackageService;
|
||||||
import com.ftptool.sync.service.ProdConfigApiService;
|
import com.ftptool.sync.service.ProdConfigApiService;
|
||||||
|
import com.ftptool.sync.service.ProdPullAckService;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@ -70,6 +71,9 @@ class ProdSyncCoordinatorIntegrationTest {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private ProdSyncCoordinator prodSyncCoordinator;
|
private ProdSyncCoordinator prodSyncCoordinator;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ProdPullAckService prodPullAckService;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private SyncTaskRepository syncTaskRepository;
|
private SyncTaskRepository syncTaskRepository;
|
||||||
|
|
||||||
@ -93,6 +97,10 @@ class ProdSyncCoordinatorIntegrationTest {
|
|||||||
syncTaskRepository.deleteAll();
|
syncTaskRepository.deleteAll();
|
||||||
syncCheckpointRepository.deleteAll();
|
syncCheckpointRepository.deleteAll();
|
||||||
prodPullAckRecordRepository.deleteAll();
|
prodPullAckRecordRepository.deleteAll();
|
||||||
|
doAnswer(invocation -> {
|
||||||
|
prodPullAckService.markPendingAsReported();
|
||||||
|
return null;
|
||||||
|
}).when(prodConfigApiService).confirmPendingPullAck();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -189,7 +197,7 @@ class ProdSyncCoordinatorIntegrationTest {
|
|||||||
assertEquals(2, ackRecords.size());
|
assertEquals(2, ackRecords.size());
|
||||||
for (ProdPullAckRecord ackRecord : ackRecords) {
|
for (ProdPullAckRecord ackRecord : ackRecords) {
|
||||||
assertEquals(ProdPullAckStatus.SUCCESS, ackRecord.getAckStatus());
|
assertEquals(ProdPullAckStatus.SUCCESS, ackRecord.getAckStatus());
|
||||||
assertFalse(ackRecord.getReported());
|
assertTrue(ackRecord.getReported());
|
||||||
}
|
}
|
||||||
|
|
||||||
verify(gitClientService, times(1)).syncDirectoryToBranch(
|
verify(gitClientService, times(1)).syncDirectoryToBranch(
|
||||||
@ -197,6 +205,7 @@ class ProdSyncCoordinatorIntegrationTest {
|
|||||||
eq("config-prod-snapshot/prod-v1"),
|
eq("config-prod-snapshot/prod-v1"),
|
||||||
contains("prod-v1")
|
contains("prod-v1")
|
||||||
);
|
);
|
||||||
|
verify(prodConfigApiService, times(2)).confirmPendingPullAck();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -255,7 +264,7 @@ class ProdSyncCoordinatorIntegrationTest {
|
|||||||
assertEquals(1, ackRecords.size());
|
assertEquals(1, ackRecords.size());
|
||||||
assertEquals("9", ackRecords.get(0).getRemoteConfigId());
|
assertEquals("9", ackRecords.get(0).getRemoteConfigId());
|
||||||
assertEquals(ProdPullAckStatus.FAILED, ackRecords.get(0).getAckStatus());
|
assertEquals(ProdPullAckStatus.FAILED, ackRecords.get(0).getAckStatus());
|
||||||
assertFalse(ackRecords.get(0).getReported());
|
assertTrue(ackRecords.get(0).getReported());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -306,7 +315,7 @@ class ProdSyncCoordinatorIntegrationTest {
|
|||||||
assertEquals(1, ackRecords.size());
|
assertEquals(1, ackRecords.size());
|
||||||
ProdPullAckRecord ackRecord = ackRecords.get(0);
|
ProdPullAckRecord ackRecord = ackRecords.get(0);
|
||||||
assertEquals(ProdPullAckStatus.SUCCESS, ackRecord.getAckStatus());
|
assertEquals(ProdPullAckStatus.SUCCESS, ackRecord.getAckStatus());
|
||||||
assertFalse(ackRecord.getReported());
|
assertTrue(ackRecord.getReported());
|
||||||
assertEquals(Integer.valueOf(0), ackRecord.getRetryCount());
|
assertEquals(Integer.valueOf(0), ackRecord.getRetryCount());
|
||||||
assertEquals("PEK", ackRecord.getAirportId());
|
assertEquals("PEK", ackRecord.getAirportId());
|
||||||
assertEquals("monitor", ackRecord.getAppName());
|
assertEquals("monitor", ackRecord.getAppName());
|
||||||
@ -314,6 +323,7 @@ class ProdSyncCoordinatorIntegrationTest {
|
|||||||
|
|
||||||
verify(prodConfigApiService, times(1))
|
verify(prodConfigApiService, times(1))
|
||||||
.pullConfigSnapshots("PEK", "monitor", "prod-v3", "jobs/sync-job.json");
|
.pullConfigSnapshots("PEK", "monitor", "prod-v3", "jobs/sync-job.json");
|
||||||
|
verify(prodConfigApiService, times(2)).confirmPendingPullAck();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@ -19,10 +19,12 @@ import java.nio.file.Path;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
import static org.springframework.test.web.client.ExpectedCount.once;
|
import static org.springframework.test.web.client.ExpectedCount.once;
|
||||||
@ -139,7 +141,7 @@ class ProdConfigApiServiceHttpTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldSendAckParamsWhenPullingConfig() throws Exception {
|
void shouldPullConfigWithoutAckParams() throws Exception {
|
||||||
RestTemplate restTemplate = new RestTemplate();
|
RestTemplate restTemplate = new RestTemplate();
|
||||||
MockRestServiceServer server = MockRestServiceServer.bindTo(restTemplate).build();
|
MockRestServiceServer server = MockRestServiceServer.bindTo(restTemplate).build();
|
||||||
|
|
||||||
@ -158,9 +160,6 @@ class ProdConfigApiServiceHttpTest {
|
|||||||
prodApiProperties.setPullFileName("a.txt");
|
prodApiProperties.setPullFileName("a.txt");
|
||||||
|
|
||||||
ProdPullAckService prodPullAckService = mock(ProdPullAckService.class);
|
ProdPullAckService prodPullAckService = mock(ProdPullAckService.class);
|
||||||
when(prodPullAckService.getPendingAckSummary()).thenReturn(
|
|
||||||
new ProdPullAckService.PendingAckSummary(Arrays.asList("1", "2"), Arrays.asList("9"))
|
|
||||||
);
|
|
||||||
ConfigCryptoService configCryptoService = mock(ConfigCryptoService.class);
|
ConfigCryptoService configCryptoService = mock(ConfigCryptoService.class);
|
||||||
when(configCryptoService.decryptAfterPull("test-airport", "test-app", "v1", "a.txt", "content"))
|
when(configCryptoService.decryptAfterPull("test-airport", "test-app", "v1", "a.txt", "content"))
|
||||||
.thenReturn("decoded-content");
|
.thenReturn("decoded-content");
|
||||||
@ -180,8 +179,8 @@ class ProdConfigApiServiceHttpTest {
|
|||||||
assertTrue(uri.contains("appName=test-app"));
|
assertTrue(uri.contains("appName=test-app"));
|
||||||
assertTrue(uri.contains("configVersion=v1"));
|
assertTrue(uri.contains("configVersion=v1"));
|
||||||
assertTrue(uri.contains("fileName=a.txt"));
|
assertTrue(uri.contains("fileName=a.txt"));
|
||||||
assertTrue(uri.contains("ackSuc=1,2") || uri.contains("ackSuc=1%2C2"));
|
assertFalse(uri.contains("ackSuc="));
|
||||||
assertTrue(uri.contains("ackFail=9"));
|
assertFalse(uri.contains("ackFail="));
|
||||||
})
|
})
|
||||||
.andExpect(method(HttpMethod.GET))
|
.andExpect(method(HttpMethod.GET))
|
||||||
.andExpect(header("token", "static-token"))
|
.andExpect(header("token", "static-token"))
|
||||||
@ -205,6 +204,63 @@ class ProdConfigApiServiceHttpTest {
|
|||||||
assertEquals("decoded-content", new String(Files.readAllBytes(restoredFile), StandardCharsets.UTF_8));
|
assertEquals("decoded-content", new String(Files.readAllBytes(restoredFile), StandardCharsets.UTF_8));
|
||||||
|
|
||||||
verify(configCryptoService).decryptAfterPull("test-airport", "test-app", "v1", "a.txt", "content");
|
verify(configCryptoService).decryptAfterPull("test-airport", "test-app", "v1", "a.txt", "content");
|
||||||
|
verify(prodPullAckService, times(0)).getPendingAckSummary();
|
||||||
|
verify(prodPullAckService, times(0)).markPendingAsReported();
|
||||||
|
server.verify();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldConfirmPendingAckByAckOnlyPullCall() throws Exception {
|
||||||
|
RestTemplate restTemplate = new RestTemplate();
|
||||||
|
MockRestServiceServer server = MockRestServiceServer.bindTo(restTemplate).build();
|
||||||
|
|
||||||
|
SyncProperties syncProperties = newSyncProperties("./target/http-test-work-pull-ack");
|
||||||
|
WorkDirectoryService workDirectoryService = new WorkDirectoryService(syncProperties);
|
||||||
|
workDirectoryService.initialize();
|
||||||
|
|
||||||
|
ProdApiProperties prodApiProperties = new ProdApiProperties();
|
||||||
|
prodApiProperties.setBaseUrl("https://prod.example.com");
|
||||||
|
prodApiProperties.setPullPath("/pic_bus_manage_monitor/configSync/pullConfig");
|
||||||
|
prodApiProperties.setToken("static-token");
|
||||||
|
prodApiProperties.setTokenHeaderName("token");
|
||||||
|
prodApiProperties.setAirportId("test-airport");
|
||||||
|
prodApiProperties.setAppName("test-app");
|
||||||
|
prodApiProperties.setPullConfigVersion("v1");
|
||||||
|
prodApiProperties.setPullFileName("a.txt");
|
||||||
|
|
||||||
|
ProdPullAckService prodPullAckService = mock(ProdPullAckService.class);
|
||||||
|
when(prodPullAckService.getPendingAckSummary()).thenReturn(
|
||||||
|
new ProdPullAckService.PendingAckSummary(Arrays.asList("1", "2"), Arrays.asList("9"))
|
||||||
|
);
|
||||||
|
|
||||||
|
ProdConfigApiService service = new ProdConfigApiService(
|
||||||
|
prodApiProperties,
|
||||||
|
syncProperties,
|
||||||
|
restTemplate,
|
||||||
|
workDirectoryService,
|
||||||
|
prodPullAckService,
|
||||||
|
new ConfigCryptoService()
|
||||||
|
);
|
||||||
|
|
||||||
|
server.expect(once(), request -> {
|
||||||
|
String uri = request.getURI().toString();
|
||||||
|
assertFalse(uri.contains("airportId="));
|
||||||
|
assertFalse(uri.contains("appName="));
|
||||||
|
assertFalse(uri.contains("configVersion="));
|
||||||
|
assertFalse(uri.contains("fileName="));
|
||||||
|
assertTrue(uri.contains("ackSuc=1,2") || uri.contains("ackSuc=1%2C2"));
|
||||||
|
assertTrue(uri.contains("ackFail=9"));
|
||||||
|
})
|
||||||
|
.andExpect(method(HttpMethod.GET))
|
||||||
|
.andExpect(header("token", "static-token"))
|
||||||
|
.andRespond(withSuccess(
|
||||||
|
"{\"code\":\"0\",\"data\":[],\"msg\":\"ok\"}",
|
||||||
|
MediaType.APPLICATION_JSON
|
||||||
|
));
|
||||||
|
|
||||||
|
service.confirmPendingPullAck();
|
||||||
|
|
||||||
|
verify(prodPullAckService).getPendingAckSummary();
|
||||||
verify(prodPullAckService).markPendingAsReported();
|
verify(prodPullAckService).markPendingAsReported();
|
||||||
server.verify();
|
server.verify();
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user