task-36
This commit is contained in:
+13
@@ -0,0 +1,13 @@
|
||||
package com.nanri.aiimage.modules.shopdatacrawl.service;
|
||||
|
||||
/**
|
||||
* Task 36:每日累计文件归档提交阶段的版本 CAS 冲突信号。
|
||||
* 由提交阶段捕获并触发整次归档重试(释放店铺级锁后按最新状态重新组装),
|
||||
* 区别于需要调用方直接失败的运行时异常。
|
||||
*/
|
||||
class DailyStateConflictException extends RuntimeException {
|
||||
|
||||
DailyStateConflictException() {
|
||||
super("daily file state conflict");
|
||||
}
|
||||
}
|
||||
+66
-33
@@ -83,6 +83,8 @@ public class ShopDataCrawlTaskService {
|
||||
private static final int RESULT_SUCCESS = 1;
|
||||
/** 批量 IN 查询单批上限。 */
|
||||
private static final int ID_BATCH_SIZE = 500;
|
||||
/** Task 36:每日累计归档版本 CAS 冲突的最大重试次数(每次重试都释放店铺级锁)。 */
|
||||
private static final int MAX_DAILY_AGGREGATION_ATTEMPTS = 3;
|
||||
private static final String INTERRUPTED_MESSAGE = "Python 在该店铺结果提交完成前中断";
|
||||
private static final String PARTIAL_RESULT_MESSAGE = "Python 中断,已保留已回传的部分数据";
|
||||
private static final String RESULT_CHUNK_SCOPE_PREFIX = "result-chunks:";
|
||||
@@ -1742,46 +1744,73 @@ public class ShopDataCrawlTaskService {
|
||||
if (userId == null || shopKeyHash == null) {
|
||||
throw new BusinessException("店铺累计文件归属信息不完整");
|
||||
}
|
||||
TaskDistributedLockService.LockHandle lock = dailyFileService.acquireLock(userId, shopKey);
|
||||
if (lock == null) {
|
||||
throw new BusinessException("店铺当天累计文件正在处理中,请稍后重试");
|
||||
}
|
||||
// Task 36:版本号/CAS 短临界区。店铺级锁只覆盖“准备/提交”两个毫秒级短事务
|
||||
// (各自独立取锁/释放),整表 Excel 组装与 OSS 上传在两次取锁之间于锁外执行;
|
||||
// 提交阶段按 daily_file.version CAS,冲突时释放锁、清理本次上传对象并按最新状态重试(最多 3 次)。
|
||||
DailyAggregationResult persistedResult = null;
|
||||
try {
|
||||
DailyAggregationPreparation preparation = executeShortTransaction(
|
||||
() -> prepareDailyAggregation(userId, shopKeyHash, businessDate, row));
|
||||
DailyWorkbookArtifact artifact = null;
|
||||
for (int attempt = 1; attempt <= MAX_DAILY_AGGREGATION_ATTEMPTS; attempt++) {
|
||||
DailyAggregationPreparation preparation;
|
||||
TaskDistributedLockService.LockHandle prepareLock = dailyFileService.acquireLock(userId, shopKey);
|
||||
if (prepareLock == null) {
|
||||
throw new BusinessException("店铺当天累计文件正在处理中,请稍后重试");
|
||||
}
|
||||
try {
|
||||
preparation = executeShortTransaction(
|
||||
() -> prepareDailyAggregation(userId, shopKeyHash, businessDate, row));
|
||||
} finally {
|
||||
prepareLock.close();
|
||||
}
|
||||
if (preparation.alreadyArchived()) {
|
||||
return new DailyAggregationResult(List.of(), false);
|
||||
}
|
||||
|
||||
int addedRowCount = excelAssemblyService.countRows(List.of(snapshot));
|
||||
ShopDataCrawlDailyFileEntity baseDailyFile = resolveBaseDailyFile(
|
||||
ShopDataCrawlDailyFileEntity baseForAttempt = resolveBaseDailyFile(
|
||||
preparation.dailyFile(), userId, shopKeyHash, businessDate);
|
||||
DailyWorkbookArtifact artifact = assembleDailyWorkbook(
|
||||
task, snapshot, baseDailyFile, addedRowCount);
|
||||
try {
|
||||
persistedResult = executeShortTransaction(() -> persistDailyAggregation(
|
||||
task, row, userId, shopKey, shopKeyHash, businessDate,
|
||||
preparation, artifact, snapshot));
|
||||
if (persistedResult.discardUploadedObject() && artifact.uploaded()) {
|
||||
deleteObjectQuietly(artifact.objectKey());
|
||||
}
|
||||
return persistedResult;
|
||||
} catch (RuntimeException ex) {
|
||||
if (artifact.uploaded()) {
|
||||
deleteObjectQuietly(artifact.objectKey());
|
||||
}
|
||||
throw ex;
|
||||
// 组装在锁外执行。每次尝试都用本次准备阶段读到的最新 base 组装
|
||||
// (冲突重试时 base 已变化,复用过期的组装结果会把并发写入的行丢在对象外);
|
||||
// 零行引用对象不重复上传。
|
||||
if (artifact == null || !artifact.uploaded()) {
|
||||
artifact = assembleDailyWorkbook(task, snapshot, baseForAttempt,
|
||||
excelAssemblyService.countRows(List.of(snapshot)));
|
||||
}
|
||||
TaskDistributedLockService.LockHandle commitLock = dailyFileService.acquireLock(userId, shopKey);
|
||||
if (commitLock == null) {
|
||||
throw new BusinessException("店铺当天累计文件正在处理中,请稍后重试");
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
lock.close();
|
||||
} finally {
|
||||
if (persistedResult != null) {
|
||||
persistedResult.obsoleteObjectKeys().forEach(this::deleteObjectQuietly);
|
||||
DailyAggregationPreparation preparationForAttempt = preparation;
|
||||
DailyWorkbookArtifact artifactForAttempt = artifact;
|
||||
try {
|
||||
persistedResult = executeShortTransaction(() -> persistDailyAggregation(
|
||||
task, row, userId, shopKey, shopKeyHash, businessDate,
|
||||
preparationForAttempt, artifactForAttempt, snapshot,
|
||||
preparationForAttempt.dailyFile()));
|
||||
if (persistedResult.discardUploadedObject() && artifactForAttempt.uploaded()) {
|
||||
deleteObjectQuietly(artifactForAttempt.objectKey());
|
||||
} else {
|
||||
persistedResult.obsoleteObjectKeys().forEach(this::deleteObjectQuietly);
|
||||
}
|
||||
return persistedResult;
|
||||
} catch (DailyStateConflictException conflictEx) {
|
||||
// 冲突:本次上传对象作废并释放,按最新状态重新组装再试。
|
||||
if (artifact.uploaded()) {
|
||||
deleteObjectQuietly(artifact.objectKey());
|
||||
artifact = null;
|
||||
}
|
||||
if (attempt >= MAX_DAILY_AGGREGATION_ATTEMPTS) {
|
||||
throw new BusinessException("店铺累计文件并发更新冲突,请稍后重试");
|
||||
}
|
||||
} catch (RuntimeException ex) {
|
||||
if (artifact != null && artifact.uploaded()) {
|
||||
deleteObjectQuietly(artifact.objectKey());
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
} finally {
|
||||
commitLock.close();
|
||||
}
|
||||
}
|
||||
throw new BusinessException("店铺累计文件并发更新冲突,请稍后重试");
|
||||
}
|
||||
|
||||
private DailyAggregationPreparation prepareDailyAggregation(Long userId,
|
||||
@@ -1937,14 +1966,18 @@ public class ShopDataCrawlTaskService {
|
||||
LocalDate businessDate,
|
||||
DailyAggregationPreparation preparation,
|
||||
DailyWorkbookArtifact artifact,
|
||||
ShopDataCrawlResultItemVo snapshot) {
|
||||
ShopDataCrawlResultItemVo snapshot,
|
||||
ShopDataCrawlDailyFileEntity expectedBase) {
|
||||
ShopDataCrawlDailyFileEntity dailyFile = dailyFileService.findForUpdate(
|
||||
userId, shopKeyHash, businessDate);
|
||||
if (handleExistingDailyMembership(row, dailyFile)) {
|
||||
return new DailyAggregationResult(List.of(), true);
|
||||
}
|
||||
if (!sameDailyFileState(preparation.dailyFile(), dailyFile)) {
|
||||
throw new BusinessException("当天累计文件状态已变化,请重试文件任务");
|
||||
if (!Objects.equals(dailyFile == null ? null : dailyFile.getId(),
|
||||
expectedBase == null ? null : expectedBase.getId())
|
||||
|| !Objects.equals(dailyFile == null ? null : dailyFile.getVersion(),
|
||||
expectedBase == null ? null : expectedBase.getVersion())) {
|
||||
throw new DailyStateConflictException();
|
||||
}
|
||||
|
||||
List<ShopDataCrawlDailyFileEntity> olderFiles = dailyFileService.findOlder(
|
||||
|
||||
Reference in New Issue
Block a user