Files
railwaysystem/docs/development/dms-r3/07_retry_idempotency_and_compensation.md
T

14 KiB
Raw Blame History

07 异步轮询、失败重试、本地幂等与换房补偿设计 (Retry, Idempotency and Compensation)

可靠性第一原则:DMS 外部设备故障绝不允许导致本地住宿安排事实发生回滚 (DMS Unavailable Principle)。 住宿事实分配成功后,外部设备权限下发即使遭遇网络分区、网关断电或服务不可用,也只在门禁层体现为 PENDING 或 ISSUE_FAILED,供后台调度补偿或人工排障。


1. 异步轮询调度策略 (Polling Strategy)

1.1 严禁主线程阻塞等待

  • 官方文档明示:“门锁的实际执行结果,务必在调用相关操作后 10 秒查询”。
  • 严重红线:绝对禁止在 Web 请求线程(Controller/Service)中执行 Thread.sleep(10000)!这会迅速耗尽 Tomcat 工作线程池,引发系统雪崩。
  • 轮询架构设计:
    Web 请求 (如确认入住)
           │
           ▼
    本地持久化 Accommodation (已入住) 与 DoorAuthorization (ISSUING)
    提交本地数据库事务 (Commit Local DB)
           │
           ▼
    异步派发调用 DMS 创建钥匙 -> 返回 operationId (耗时 ~200ms)
    本地记录 ExternalOperationRecord (status = ACCEPTED, next_poll_time = now + 10s)
    快速向前端返回 HTTP 200 (附带提示:“住宿已分配,门锁授权下发中”)
           │
           ▼
    后台调度引擎 (Quartz / Spring Scheduled ThreadPool - 专用IO线程池)
    扫描 next_poll_time <= NOW() 的任务 -> 调用 /doorlock/query-operation-status
    更新操作记录与授权记录状态 -> 若为 success 则推为 ACTIVE
    

1.2 门锁 vs 网关轮询参数隔离

官方文档中存在两套截然不同的轮询规约,严禁混淆:

  • 门锁操作轮询(密码/卡片/远程开锁):
    • INITIAL_DELAY: 10 秒(门锁低功耗唤醒通常需 5~8 秒)。
    • POLL_INTERVAL: 5 秒。
    • MAX_POLL_ATTEMPTS: 3 次(总超时窗口约 25 秒)。
  • 网关在线状态轮询(网关刷新):
    • INITIAL_DELAY: 3 秒。
    • POLL_INTERVAL: 1 秒。
    • MAX_POLL_ATTEMPTS: 5 次。

2. 失败分类与重试策略 (Retry Strategy)

严禁只要发生错误就盲目重新调用 create 接口,这会导致同一学员在门锁中堆积多张相同密码或卡片,占满锁具内存。必须对故障进行严格分类:

flowchart TD
    FAIL[发生异常 / 收到非成功结果] --> CHECK{判断故障类型}

    CHECK -->|A. 请求明确未发送\n(连接超时/本地校验失败)| RETRY_A[安全重试创建: 指数退避\n重试间隔 2s, 4s, 8s, 上限3次]

    CHECK -->|B. DMS明确返回业务拒绝\n(400/Invalid Room/Invalid Lock)| FAIL_B[不可恢复业务失败\n标记 ISSUE_FAILED, 记录错误日志\n停止重试, 提示管理员核对房源]

    CHECK -->|C. 成功获得 operationId\n但轮询返回 undone| WAIT_C[设备执行中: 继续等待下一次轮询\n未超上限前保持 PENDING]

    CHECK -->|D. 网络读取超时(Read Timeout)\n无法确认DMS是否已受理| RECONCILE[状态未决 (UNKNOWN)\n禁止立即重新创建!\n先执行对账查询或主动撤销后新建]

2.1 未决状态对账策略 (UNKNOWN / RECONCILE)

  • 当本地向 DMS 发送 POST /online-password-key/create 时遭遇 SocketTimeoutException(网络读取超时):
    • 风险:DMS 服务端可能已经处理成功并把钥匙下发到了锁端,只是响应包丢失。若直接重发创建请求,门锁内将存在两套密码。
    • 对账方案:
      1. 标记当前操作为 RECONCILE_PENDING。
      2. 调度器调用开门钥匙列表或按房间查询当前已有钥匙。
      3. 若能识别出刚才持有人及手机号生成的有效钥匙,则绑定该钥匙 ID 并恢复轮询。
      4. 若确认未成功生成,方可发起二次 create。

3. 本地业务幂等设计 (Idempotency Strategy)

  • 外部约束:DMS API 原生并不支持 Idempotency-Key 请求头。
  • 本地防护机制:
    • 幂等联合主键定义:IdempotencyKey = Hash(accommodation_id + credential_type + grant_version)。
    • 在 train_door_authorization 表中建立唯一约束或在 Service 缓存层加分布式互斥锁。
    • 业务效果:无论操作员在前端如何频繁、多次点击“下发权限”按钮,对于同一个住宿安排的同一个版本,有且仅有一条处于 ISSUING 或 ACTIVE 状态的授权记录,绝不产生重复钥匙。

4. 业务触发与撤权决策比选 (Business Triggers)

4.1 门禁授权下发触发时机比选 (ACCESS_GRANT_TRIGGER_RECOMMENDATION)

候选方案 业务流程说明 优势 风险与缺点 推荐结论
方案 A:住宿分配后立即下发 管理员排宿完成即刻向门锁下发限时密码 提前就绪,报到现场零等待 若学员临时取消参训,需频繁撤权;提前泄露密码可能导致未报到人员提前进入 不推荐
方案 B:报到核验确认后触发 (首选) 学员现场在《报到管理》通过人脸/刷卡核验后,异步触发门禁下发 安全严密:确保人已到校才给钥匙;与现场报到事件紧密联动 报到高峰期对 DMS 带来并发请求压力,需队列平滑削峰 (推荐) 提交甲方确认
方案 C:临近住宿开始时间下发 开班前 2 小时定时扫描下发 自动化高 无法过滤未报到/缺勤学员 备选

4.2 授权有效时间窗口缓冲设计 (Grant Validity Buffer)

  • 严禁将门锁密码生效时间与培训班作息严格卡死到分钟(例如培训班 9:00 开始,密码 9:00:00 才生效)。
  • 缓冲机制 (Buffer):
    • accessStartOffset(提早进房时间):建议可配置提前 2~4 小时(允许学员提早入住休整)。
    • accessEndOffset(退宿宽限时间):建议培训班结束当天可配置延后 2~4 小时(允许学员打包行李离开)。
    • 有效期计算公式: \text{validStart} = \text{Accommodation.assignedStart} - \text{accessStartOffset} \text{validUntil} = \text{Accommodation.assignedEnd} + \text{accessEndOffset}

4.3 培训结训与学员退训撤权策略

  1. 培训计划结训 (Plan Completion):
    • 主动撤权为主:
      • 流程:Plan Complete → 对仍处于 ACTIVE 状态的 Door Authorization 发起主动 Revoke → 派发 DMS delete 指令 → 保存 operationId → 后台异步轮询确认设备执行结果。
    • validUntil 有效期边界兜底:
      • DMS Credential 的 validUntil 提供授权有效期边界;培训结束时 RailwaySystem 仍坚决主动发起撤权。
      • 设备离线情况下的本地过期机制、RTC 硬件时钟行为和离线保证级别需供应商确认(禁止假设设备端 RTC 机制已完成实物验证)。
  2. 学员中途退训 (Withdrawal):
    • 强制触发主动撤销 (WITHDRAW_REVOKE_RECOMMENDATION = IMMEDIATE_REVOKE)。学员一旦退训,立即将其名下处于 ACTIVE 的门锁授权状态置为 REVOKING 并派发删除指令。
  3. 学员请假离校 (Leave):
    • LEAVE_REVOKE_RECOMMENDATION = NEEDS_BUSINESS_CONFIRMATION。请假期间是否收回房间门禁权限,取决于公寓管理条例(是否保留个人物品及床位),需交由业务部门决策。

5. 调房换房补偿策略 (TRANSFER_COMPENSATION_STRATEGY)

在学员需要从原宿舍 A 调整至新宿舍 B 的业务场景下,涉及两把不同门锁的设备指令,绝不可塞进一个本地数据库事务中同步执行。

sequenceDiagram
    autonumber
    actor Admin as 管理员/宿管
    participant Service as 住宿调度服务
    participant DB as 本地数据库
    participant Gateway as 门禁网关
    participant DMS as DMS 平台

    Admin->>Service: 发起换房 (学员 X: 房间 A -> 房间 B)
    Service->>DB: 开启本地事务
    Service->>DB: 更新原住宿记录 A (status=已退宿/调出)
    Service->>DB: 创建新住宿记录 B (status=已入住/调入)
    Service->>DB: 提交本地事务 (业务事实先落盘)

    Note over Service,Gateway: 阶段一:先发新房权限 (保障学员有地方住)
    Service->>Gateway: grantAccess(新房 B, 学员 X)
    Gateway->>DMS: POST /online-password-key/create (锁 B)
    DMS-->>Gateway: 返回 accepted (新钥匙 ID_B)

    alt 新房权限下发成功
        Note over Service,Gateway: 阶段二:安全撤销旧房权限
        Service->>Gateway: revokeAccess(旧房 A, 旧钥匙 ID_A)
        Gateway->>DMS: POST /online-password/delete (锁 A)
        DMS-->>Gateway: 返回 accepted (撤销流水)
        Service-->>Admin: 换房完成,新房密码已生效,旧房权限已注销
    else 新房权限下发遭遇不可恢复故障 (如锁B损坏/网络中断)
        Note over Service: 触发补偿流程 (Compensation)
        Service->>DB: 记录调房门禁异常报警
        Service->>Service: 绝不撤销旧房权限 (保留原钥匙 A,学员不流落走廊)
        Service-->>Admin: 换房业务记录已建立,但新宿舍门锁下发失败,请人工介入处理
    end

5.1 换房核心准则总结

  • “先发新、后撤旧”原则:绝不能先删除了旧房权限再去发新房。若新房门锁出现硬件故障或网络不通,学员将两头落空无法进门。
  • 降级容错策略:新房授权失败时,保留旧房权限,并在前端控制台突出显示“门禁换房未同步”,由管理员一键重试或安排备用钥匙。

6. Wave 1 Implementation Result (Wave 1 implemented)

  • 阶段状态:Wave 1 implemented。
  • 本地幂等模型落地 (IDEMPOTENCY_MODEL):
    • 物理约束:train_external_operation 设立 uq_ext_op_idempotency 唯一索引,数据库级别杜绝重复写入。
    • 服务防重:ExternalOperationServiceImpl.createOperation 在插入前先查幂等键,命中已有记录直接返回已有实体,防止重复派发异步调用。
  • 重试审计与不可变性:
    • 终态记录不可覆写:每次重试调用通过 createRetryOperation 独立创建自增 attempt_count 的新记录。
    • 保留完整排障轨迹:last_error_code 与 last_error_message 详细保留每次失败现场,满足段级安全审计与对账需求。

7. Wave 2 Implementation Result (Wave 2 implemented against Mock DMS only)

  • 阶段状态:Wave 2 implemented against Mock DMS only。
  • 重试与幂等安全落地:
    • AMBIGUOUS_MUTATION_AUTO_RETRY = NO:写操作(如密码创建)发生网络读取超时(Read Timeout)时,底层 DmsHttpClient 坚决抑制自动重试,passwordCreatePostCounter = 1,避免在门锁端产生重复凭据。
    • 轮询指数退避:DmsOperationPoller 在遭遇网络抖动时将状态置为 RETRY_WAIT,基于 Math.pow(2, attemptCount) * 5 计算下次轮询时间并递增 attempt_count,防止高频风暴轮询。
    • 401 集中重试:仅在明确认证失败且为安全查询时才发起 1 次 Token 刷新与重试;写操作 401 则集中失效并向外抛错,由上层流水对账接管。
  • 迁移标记:
    • 008_RUNTIME_FIRST_RUN = NOT_EXECUTED,008_RUNTIME_SECOND_RUN = NOT_EXECUTED。
    • RUNTIME_MIGRATION_GATE_BEFORE_PRODUCTION = REQUIRED。

8. Wave 3A Implementation Result (Wave 3A implemented against Mock DMS only)

  • 阶段状态:Wave 3A Access Authorization Orchestration Dry-Run implemented against Mock DMS only。
  • 状态驱动对账模型 (RECONCILIATION_MODEL):
    • 核心编排组件:AccessAuthorizationOrchestrator.reconcile(enrollmentId)。
    • 统一检查业务事实并决策输出:NO_ACTION、WAITING_FOR_PREREQUISITE、GRANT_REQUIRED、REVOKE_REQUIRED。
    • 彻底解耦业务事件发生时序:
      • Scenario A(先分配住宿,后报到确认):报到确认后触发对账 -> GRANT -> 轮询成功 -> ACTIVE。
      • Scenario B(先报到确认,后分配住宿):初次对账 WAITING -> 分配住宿后触发对账 -> GRANT -> 轮询成功 -> ACTIVE。
  • 业务主事务完全解耦与独立性 (LONG_DB_TRANSACTION_AROUND_HTTP = NO):
    • 严禁长事务包住远程 HTTP 调用:本地写授权记录与写操作流水为短事务,随后进行外部 Gateway 调用,后续再短事务更新受理结果。
    • REPORT_CONFIRM_EXTERNAL_FAILURE_ROLLBACK = NO:DMS 网络超时或故障时,报到确认(status 0 -> 1)、退宿(status 1 -> 2)、结训归档(status 1 -> 4)等本地主业务事务绝对不会回滚,外部门禁自动转入待重试/挂起态。
  • 对账幂等与防重机制 (RECONCILE_IDEMPOTENT):
    • 连续多次触发对账(如学员重复点击或短时多事件触发),业务事实未变化时返回 NO_ACTION,绝不重复创建业务授权、绝不重复调用 DMS 发放密码凭据、绝不创建重复操作流水。
  • 最少特权门锁选择策略 (LOCK_SELECTION_POLICY = PRIMARY_LOCK_ONLY):
    • 严格限制自动发码范围为房间的唯一定义主门锁:
      • 0 主锁:阻断发码,返回 WAITING_FOR_PREREQUISITE。
      • 1 主锁:正常放行下发凭据。
      • 1 主锁:配置歧义错误阻断,保护硬件资源。

      • 次锁:不额外自动发码,保留未来扩展能力。
  • 调房安全策略 (TRANSFER_POLICY = GRANT_NEW_THEN_REVOKE_OLD):
    • 先为新宿舍发起凭据下发并等待轮询确认生效(ACTIVE)。
    • 只有在新宿舍确认生效后,才对旧宿舍发起撤销调用。
    • TRANSFER_FAILURE_PRESERVES_OLD_ACCESS = YES:若新宿舍凭据下发失败,旧宿舍授权绝对保持 ACTIVE,杜绝学员两头落空进不了门。
    • 审计确认存在短暂合理的 dual-access window,优先保障可用性。