Skip to main content

def allocate_next_id(namespace: str, area: str, category: str) -> str:

  1. def allocate_next_id(namespace: str, area: str, category: str) -> str:
        """
        原子分配下一个可用 ID
        使用 IMMEDIATE 事务模式 + UNIQUE 约束确保并发安全
        """
        max_retries = 3
        for attempt in range(max_retries):
            try:
                # ⚠️ 关键:使用 IMMEDIATE 模式,事务开始时立即获取写锁
                # 这会阻塞其他写事务,避免空 Category 的竞态条件
                cursor.execute("BEGIN IMMEDIATE")
                
                # 1. 查询当前 Category 下最大 ID
                # 同时查询 id_reservations 表(可能有预占但未使用的 ID)
                cursor.execute("""
                    SELECT MAX(id_num) FROM (
                        SELECT CAST(SUBSTR(jd_number, -4) AS INTEGER) as id_num
                        FROM resources 
                        WHERE namespace = ? AND jd_number LIKE ?
                        UNION ALL
                        SELECT CAST(SUBSTR(jd_number, -4) AS INTEGER) as id_num
                        FROM id_reservations
                        WHERE namespace = ? AND jd_number LIKE ?
                    )
                """, (namespace, f"{area}.{category}.%", 
                      namespace, f"{area}.{category}.%"))
                
                row = cursor.fetchone()
                max_id = (row[0] if row else None) or 0
                next_id = max_id + 1
                
                if next_id > 9999:
                    cursor.execute("ROLLBACK")
                    raise OverflowError(f"Category {area}.{category} ID exhausted (max 9999)")
                
                # 2. 预占 ID(UNIQUE 约束是最后一道防线)
                new_jd = f"{area}.{category}.{next_id:04d}"
                cursor.execute("""
                    INSERT INTO id_reservations (jd_number, namespace, reserved_at)
                    VALUES (?, ?, datetime('now'))
                """, (new_jd, namespace))
                
                cursor.execute("COMMIT")
                return new_jd
                
            except sqlite3.IntegrityError:
                # UNIQUE 约束冲突,说明有并发分配,重试
                cursor.execute("ROLLBACK")
                if attempt == max_retries - 1:
                    raise AllocationError(f"Failed to allocate ID after {max_retries} retries")
                continue  # 重试
    
    def release_reservation(jd_number: str):
        """创建失败时释放预占的 ID"""
        cursor.execute("DELETE FROM id_reservations WHERE jd_number = ?", (jd_number,))
    


    **id_reservations 表结构**:

    
    CREATE TABLE id_reservations (
        jd_number TEXT PRIMARY KEY,   -- ⚠️ UNIQUE 约束,防止重复分配
        namespace TEXT NOT NULL,
        reserved_at TEXT NOT NULL,
        INDEX idx_namespace_jd (namespace, jd_number)
    );
    
    -- 定期清理过期预占(超过 1 小时未使用)
    DELETE FROM id_reservations 
    WHERE reserved_at < datetime('now', '-1 hour');
    


    **并发控制机制**:

    | 层级 | 机制 | 作用 |
    |------|------|------|
    | 1 | BEGIN IMMEDIATE | 事务级写锁,阻塞其他写事务 |
    | 2 | 查询含 id_reservations | 感知其他进程的预占 |
    | 3 | jd_number PRIMARY KEY | UNIQUE 约束,最后防线 |
    | 4 | 重试机制 | 冲突时自动重试 |
    
    
    **批量导入优化**:
    
    ```bash
    # 批量模式:一次性预占 N 个 ID,减少锁竞争
    syn import --batch /path/to/files/ --count 100
    
    # 内部实现:单次事务分配 100 个连续 ID
    


    **ID 耗尽处理**:
    - 每个 Category 最多 9999 个资源
    - 接近上限(>9000)时 CLI/TUI 显示警告
    - 建议用户创建新 Category 或清理归档资源

    ### 3.2 短 ID 支持

    为降低 CLI 输入摩擦,支持 Git 风格的短 ID:

    | 输入方式 | 示例 | 匹配规则 |
    |----------|------|----------|
    | 完整 ID | 10.001.0015 | 精确匹配 |
    | 短 ID | 10.1.15 | 自动补零 → 10.001.0015 |
    | 最短 ID | 15 | 当前 Namespace 内唯一匹配 |
    | 标题模糊 | 设计文档 | Fuzzy Finder 模糊搜索 |

    
    syn show 15           # 当前 Namespace 内唯一,直接匹配
    syn show 10.1.15      # 自动补零
    syn show 设计         # Fuzzy Finder
    


    **歧义处理**:当短 ID 匹配到多个结果时:
    - **CLI**:报错并列出候选项,用户需输入更精确的 ID
    - **TUI**:弹窗让用户选择

    
    $ syn show 15
    Error: Ambiguous ID '15'. Did you mean:
      [1] 10.001.0015 - 完成项目设计 (task)
      [2] 20.001.0015 - 会议纪要 (meeting)
    Use full ID or choose: syn show 10.001.0015
    


    **Shell 自动补全**:CLI 必须实现 Tab completion(支持 Bash/Zsh/Fish)。

    ### 3.3 目录结构(双层分桶)