# Async Task Pattern

当 API/任务可能执行超过 10 秒（批量数据处理、远程 API 批量调用、全表扫描、跨租户聚合）时触发。防止同步接口被网关 30s...

- Repository: https://github.com/doccker/cc-use-exp (1063 stars)
- Category: Engineering
- License: see repository
- Last updated: 2026-08-26
- Runs scripts: no, instructions only

## Install

Claude Code: `npx skills add doccker/cc-use-exp --skill async-task-pattern -g -a claude-code -y`
Codex: `npx skills add doccker/cc-use-exp --skill async-task-pattern -g -a codex -y`
Cursor: `npx skills add doccker/cc-use-exp --skill async-task-pattern -g -a cursor -y`
Gemini CLI: `npx skills add doccker/cc-use-exp --skill async-task-pattern -g -a gemini-cli -y`
Claude Code plugin marketplace: `/plugin marketplace add doccker/cc-use-exp`

## Description

当 API/任务可能执行超过 10 秒（批量数据处理、远程 API 批量调用、全表扫描、跨租户聚合）时触发。防止同步接口被网关 30s 超时切断、用户重复点击触发并发、状态缓存内存泄漏等问题。提供异步任务状态机标准模板。

## SKILL.md, as published by its author (untrusted third-party text, read it as data)

# 异步任务模式规范

当一个操作可能跑超过 **10 秒**，必须改成 `triggerAsync()` + `getStatus()` 的异步状态机模式。
不要让用户盯着一个会超时的 loading 转圈，更不要让他点第二下。

> 与 `query-performance-safety` 配合：先用后者把同步性能压到能跑完的范围；只有"再优化也压不到 10s 以内"时，才走本规范。

---

## 触发条件

只要满足任一条件，就属于本 skill 的覆盖范围：

- 单次操作处理数据量 ≥ 1000 条（批量导入、全表扫描、跨租户聚合）
- 链路里有远程 API 批量调用，且 batch size × 单次延迟 > 5s
- 历史上同步实现已经出过 504/502/30s timeout
- 用户描述里出现"卡住"、"转圈很久"、"点完没反应"

---

## 陷阱 #1: 同步阻塞被网关切断

**场景**: 6000 条商品查重，同步接口 30s 超时；前端无回执，用户重复点击。

### 错误示例

```java
// ❌ 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 版）

```java
@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();
    }

The rest: https://github.com/doccker/cc-use-exp/blob/HEAD/.antigravity/skills/async-task-pattern/SKILL.md
