
Async Task Pattern
当 API/任务可能执行超过 10 秒(批量数据处理、远程 API 批量调用、全表扫描、跨租户聚合)时触发。防止同步接口被网关 30s...
Install
npx skills add doccker/cc-use-exp --skill async-task-pattern -g -a claude-code -y- Repository
- doccker/cc-use-exp1.1k
- Category
- Engineering
- Updated
- 2mo ago
- License
- See repository
- Context cost
- Mediumabout 1,666 tokens
- Runs scripts
- No, instructions only
What it does
当 API/任务可能执行超过 10 秒(批量数据处理、远程 API 批量调用、全表扫描、跨租户聚合)时触发。防止同步接口被网关 30s 超时切断、用户重复点击触发并发、状态缓存内存泄漏等问题。提供异步任务状态机标准模板。
异步任务模式规范
当一个操作可能跑超过 10 秒,必须改成 triggerAsync() + getStatus() 的异步状态机模式。
不要让用户盯着一个会超时的 loading 转圈,更不要让他点第二下。
与
query-performance-safety配合:先用后者把同步性能压到能跑完的范围;只有"再优化也压不到 10s 以内"时,才走本规范。
触发条件
只要满足任一条件,就属于本 skill 的覆盖范围:
- 单次操作处理数据量 ≥ 1000 条(批量导入、全表扫描、跨租户聚合)
- 链路里有远程 API 批量调用,且 batch size × 单次延迟 > 5s
- 历史上同步实现已经出过 504/502/30s timeout
- 用户描述里出现"卡住"、"转圈很久"、"点完没反应"
陷阱 #1: 同步阻塞被网关切断
场景: 6000 条商品查重,同步接口 30s 超时;前端无回执,用户重复点击。
错误示例
// ❌ HTTP 请求线程内串行跑 N 次远程调用 + JPA 写库
@PostMapping("/duplicates/check")
public ApiResponse<Map<String, List<ProductDTO>>> checkDuplicates() {
return ApiResponse.success(productService.findDuplicateProducts());
// 6000 条 × 4 次隐藏查询 = 24000 次 SQL,必然 > 30s
}
后果:
- Nginx/网关默认 30-60s 截断 → 用户看到 502/504
- 用户重试 → 后端线程池被占满 → 雪崩
- 前端 loading 状态没法跨刷新保持
正确模板(Spring 版)
@Service
public class DuplicateChecker {
private static final long FINISHED_STATE_TTL_MINUTES = 60;
private final Executor executor;
private final ProductService productService;
// 关键:状态缓存按租户隔离,永远不要让它无界增长
private final Map<Long, CheckState> tenantStates = new ConcurrentHashMap<>();
public CheckState triggerAsync() {
evictExpiredStates();
Long tenantId = TenantContext.getTenantId();
// 同租户已 RUNNING:返回当前状态,不重复启动
CheckState existing = tenantStates.get(tenantId);
if (existing != null && existing.getStatus() == Status.RUNNING) {
return existing.snapshot();
}
CheckState state = new CheckState();
state.setStatus(Status.RUNNING);
state.setStartedAt(LocalDateTime.now());
tenantStates.put(tenantId, state);
executor.execute(() -> {
try {
TenantContext.setTenantId(tenantId); // 子线程必须恢复上下文
run(tenantId, state);
} finally {
TenantContext.clear();
}
});
return state.snapshot();
}
