task-12: extend coze result buffering beyond poll path

- bufferCozeRowsOrMerge: unified DONE result persist entry point; buffers to
  transient storage when toggle on, falls back to immediate chunk merge on
  failure (result never lost), no-op on empty input
- submit/retry synchronous immediate DONE results now buffer too (previously
  always merged immediately); submit path persists a DONE scope state row to
  carry the buffer pointer (persistImmediateCozeDoneState)
- poll path drops the batchTotal>1 gate: single-batch tasks also buffer
- CozeCandidate/CozeBatchContext records package-private for test access
This commit is contained in:
2026-08-29 16:46:29 +08:00
parent 59ee3c8154
commit 4d91146256
2 changed files with 552 additions and 36 deletions
@@ -2616,6 +2616,17 @@ public class SimilarAsinTaskService {
if (!emptyResultMessage.isBlank()) {
throw new IllegalStateException(emptyResultMessage);
}
// Task 12同步 immediate DONE 结果也走缓冲原立即 merge finalize/assemble
// 一次性 flush 合并到 chunk减少 chunk payload 频繁读写先落一条 DONE state 承载
// 缓冲 pointer缓冲关闭/失败/重复时回退立即 merge结果不丢失
if (isCozeResultBufferEnabled()) {
TaskScopeStateEntity doneState = persistImmediateCozeDoneState(task, result, job, batchRows,
batchScopeKey, batchScopeHash, batchIndex, batchTotal, submit.credentialName());
if (doneState != null) {
bufferCozeRowsOrMerge(doneState, readCozeBatchContext(doneState), cozeRows, task, allRowsByBaseId);
return false;
}
}
mergeCozeRowsIntoChunk(task, null, null, cozeRows, allRowsByBaseId);
return false;
}
@@ -2851,31 +2862,23 @@ public class SimilarAsinTaskService {
if (!failureMessage.isBlank()) {
cozeRows = cozeClient.markRowsFailed(batchRows, failureMessage);
}
// P0-3"DONE 且 batchTotal>1 且 feature toggle 开启"时缓冲 cozeRows transient storage
// finalize 阶段一次性合并到 chunk失败 batch / batch 任务保留原立即 merge 路径
boolean buffered = false;
if (failureMessage.isBlank()
&& isCozeResultBufferEnabled()
&& context.batchTotal() != null && context.batchTotal() > 1
&& cozeRows != null && !cozeRows.isEmpty()) {
CozeBatchContext bufferedContext = bufferCozeResultForFlush(state, context, cozeRows);
if (bufferedContext != null
&& bufferedContext.resultPayloadPointer() != null
&& !bufferedContext.resultPayloadPointer().isBlank()) {
context = bufferedContext;
buffered = true;
// Task 12DONE 结果统一走 bufferCozeRowsOrMerge P0-3 poll batchTotal>1 缓冲
// 现单 batchsubmit/retry 同步 immediate 结果也缓冲缓冲失败回退立即 merge
// flush finalize/assemble 前一次性合并到 chunk结果不丢失
// 失败行markRowsFailed保持立即 merge 语义不变
if (failureMessage.isBlank()) {
if (cozeRows != null && !cozeRows.isEmpty()) {
FileTaskEntity pollTask = taskForPoll(state.getTaskId());
bufferCozeRowsOrMerge(state, context, cozeRows, pollTask,
pollTask == null ? Map.of() : allRowsByBaseIdForPoll(pollTask));
}
}
if (!buffered) {
FileTaskEntity task = taskForPoll(state.getTaskId());
if (task != null) {
Map<String, List<SimilarAsinParsedRowVo>> allRowsByBaseId = allRowsByBaseIdForPoll(task);
} else {
FileTaskEntity pollTask = taskForPoll(state.getTaskId());
if (pollTask != null) {
Map<String, List<SimilarAsinParsedRowVo>> allRowsByBaseId = allRowsByBaseIdForPoll(pollTask);
try {
mergeCozeRowsIntoChunk(task, context.chunkScopeHash(), context.chunkIndex(), cozeRows, allRowsByBaseId);
mergeCozeRowsIntoChunk(pollTask, context.chunkScopeHash(), context.chunkIndex(), cozeRows, allRowsByBaseId);
} catch (Exception mergeEx) {
if (failureMessage.isBlank()) {
throw mergeEx;
}
log.warn("[similar-asin] coze failed result merge failed, mark state terminal anyway taskId={} stateId={} executeId={} err={}",
state.getTaskId(), state.getId(), state.getCozeExecuteId(),
firstNonBlank(mergeEx.getMessage(), mergeEx.getClass().getSimpleName()));
@@ -3406,7 +3409,9 @@ public class SimilarAsinTaskService {
if (!emptyResultMessage.isBlank()) {
throw new IllegalStateException(emptyResultMessage);
}
mergeCozeRowsIntoChunk(task, null, null, cozeRows, allRowsByBaseId);
// Task 12retry 同步 immediate DONE 结果也走缓冲原立即 merge
// 缓冲失败回退立即 mergeflush finalize 时一次性完成
bufferCozeRowsOrMerge(state, context, cozeRows, task, allRowsByBaseId);
markCozeStateTerminal(state, COZE_STATUS_DONE, null);
maybeFinalizeCozeJobLocked(state.getTaskId(), context);
return;
@@ -4048,6 +4053,86 @@ public class SimilarAsinTaskService {
return properties.isCozeResultBufferEnabled();
}
/**
* Task 12统一 Coze DONE 结果落库入口
* 缓冲开关开启时把 cozeRows 写入 transient storagepointer 存进 state.stateJson
* flushBufferedCozeResults finalize/assemble 前一次性合并到 chunk
* 缓冲失败存储异常 / state 更新失败 / 开关关闭回退立即 merge结果不丢失
* rows / state / context 直接返回不产生任何写入
*/
private void bufferCozeRowsOrMerge(TaskScopeStateEntity state,
CozeBatchContext context,
List<SimilarAsinResultRowDto> cozeRows,
FileTaskEntity task,
Map<String, List<SimilarAsinParsedRowVo>> allRowsByBaseId) {
if (state == null || context == null || cozeRows == null || cozeRows.isEmpty()) {
return;
}
if (!isCozeResultBufferEnabled()) {
mergeCozeRowsIntoChunk(task, context.chunkScopeHash(), context.chunkIndex(), cozeRows, allRowsByBaseId);
return;
}
CozeBatchContext bufferedContext = bufferCozeResultForFlush(state, context, cozeRows);
if (bufferedContext == null
|| bufferedContext.resultPayloadPointer() == null
|| bufferedContext.resultPayloadPointer().isBlank()) {
// 缓冲失败回退立即 merge避免结果悬挂在 transient storage 之外
mergeCozeRowsIntoChunk(task, context.chunkScopeHash(), context.chunkIndex(), cozeRows, allRowsByBaseId);
}
}
/**
* Task 12 submit 同步 immediate DONE 结果落一条 DONE state 承载缓冲 pointer
* saveCozeBatchStateSUBMITTED 异步不同 state 直接以 DONE 终态插入
* 不会被 countPendingCozeStates 扫描缓冲失败/重复插入时返回 null调用方回退立即 merge
*/
private TaskScopeStateEntity persistImmediateCozeDoneState(FileTaskEntity task,
FileResultEntity result,
TaskFileJobEntity job,
List<SimilarAsinResultRowDto> batchRows,
String batchScopeKey,
String batchScopeHash,
int batchIndex,
int batchTotal,
String credentialName) {
LocalDateTime now = LocalDateTime.now();
CozeBatchContext context = new CozeBatchContext(
job.getId(),
result.getId(),
null,
null,
batchIndex,
batchTotal,
currentInstanceId(),
0,
credentialName,
null
);
TaskScopeStateEntity state = new TaskScopeStateEntity();
state.setTaskId(task.getId());
state.setModuleType(MODULE_TYPE);
state.setScopeKey(batchScopeKey);
state.setScopeHash(batchScopeHash);
state.setStateJson(writeJson(context, "serialize immediate coze done state context failed"));
state.setCozeStatus(COZE_STATUS_DONE);
state.setCozeSubmittedAt(now);
state.setCozeCompletedAt(now);
state.setCozeAttemptCount(0);
state.setChunkTotal(batchTotal);
state.setReceivedChunkCount(batchIndex);
state.setCompleted(1);
state.setCreatedAt(now);
state.setUpdatedAt(now);
try {
taskScopeStateMapper.insert(state);
return state;
} catch (DuplicateKeyException ex) {
log.info("[similar-asin] duplicate immediate done coze state ignored taskId={} scope={}",
task.getId(), batchScopeKey);
return null;
}
}
/**
* P0-3 finalize 之前一次性把所有 DONE/FAILED state 上缓冲的 cozeRows
* chunkScopeHash 分组合并到 chunk把每个 batch "loadSubmittedChunks +
@@ -4291,9 +4376,9 @@ public class SimilarAsinTaskService {
return "coze:task:" + taskId + ":rows:" + DigestUtil.sha256Hex(rowKeys.toString());
}
private record CozeCandidate(String chunkScopeHash,
Integer chunkIndex,
SimilarAsinResultRowDto row) {
record CozeCandidate(String chunkScopeHash,
Integer chunkIndex,
SimilarAsinResultRowDto row) {
}
private record PythonUploadProgress(int current, int total, String unit) {
@@ -6789,16 +6874,16 @@ public class SimilarAsinTaskService {
boolean terminal) {
}
private record CozeBatchContext(Long jobId,
Long resultId,
String chunkScopeHash,
Integer chunkIndex,
Integer batchIndex,
Integer batchTotal,
String ownerInstanceId,
Integer submitRetryCount,
String credentialName,
String resultPayloadPointer) {
record CozeBatchContext(Long jobId,
Long resultId,
String chunkScopeHash,
Integer chunkIndex,
Integer batchIndex,
Integer batchTotal,
String ownerInstanceId,
Integer submitRetryCount,
String credentialName,
String resultPayloadPointer) {
}
private static class SourceRowsBuilder {