DaZuiZui commented on issue #18428:
URL: https://github.com/apache/iotdb/issues/18428#issuecomment-5251673548

   ## 实现方案补充:以 Issue 验收为准的正确性与兼容性约束
   
   本评论是对原实现方案的规范性补充:
   
   https://github.com/apache/iotdb/issues/18428#issuecomment-5235636695
   
   最终用户可见行为以 issue 正文为唯一验收标准。如果本补充与原方案存在冲突,以本补充为准。
   
   本补充不推翻原方案的核心设计,以下思路继续保留:
   
   - 使用结构化 calendar duration。
   - 每次从原始 `BOUNDARY` 按 `B + n × E` 计算 occurrence。
   - RANGE 端点从原始 `BOUNDARY` 在 duration-vector 空间计算。
   - 持久化 CQ `ZoneId`。
   - 旧 CQ 保持 legacy fixed-duration 行为,不进行静默迁移。
   - DataNode 最终仍只接收具体的 `startTime` 和 `endTime`。
   
   下面补充为防止静默语义降级、DST 漂移、混合版本错误以及恢复后调度漂移所必须满足的约束。
   
   ### 1. 支持范围以 issue 为准
   
   CQ 的 `EVERY` 和 `RANGE` 应支持:
   
   - 月:`mo`、`month`
   - 年:`y`、`year`
   - 单位大小写不敏感
   - `1y` 等价于 12 个自然月
   - 不新增 issue 未要求的复数形式 `months`、`years`
   
   如果当前共享的 `DURATION_LITERAL` 无法只为 CQ 增加完整单位别名,则实现时需要选择以下方式之一:
   
   1. 扩展共享 duration grammar,并回归测试其他使用 duration 的 Tree SQL 功能。
   2. 为 CQ 增加专用的 duration parser rule。
   
   不能因为当前 grammar 只支持缩写,就在本 issue 中将 `month/year` 排除在最终验收范围之外。
   
   ### 2. Calendar duration 不得压平
   
   CQ 使用结构化 duration:
   
   ```text
   CQDuration {
     long monthPart
     long fixedPart
   }
   ```
   
   其中:
   
   - `monthPart` 表示自然月数。
   - `fixedPart` 表示当前 timestamp precision 下的固定 ticks。
   - `1y` 解析为 `monthPart = 12`。
   - `d` 和 `w` 继续表示固定的 24 小时和 7×24 小时。
   - Calendar month 必须先应用,fixed ticks 随后作为 elapsed duration 应用。
   - 文本 component 顺序不改变归一化结果。
   
   在解析、RPC、持久化、恢复和调度流程中,禁止将一个自然月转换为固定 30 天,或将一个自然年转换为固定 365 天。
   
   所有 duration 运算必须检查溢出,包括:
   
   - 月份累加
   - `y × 12`
   - occurrence index 与 duration 相乘
   - duration-vector 加减
   - timestamp 加减
   
   实现中应使用 `Math.addExact`、`Math.subtractExact` 和 
`Math.multiplyExact`,不能直接依赖可能发生静默溢出的 duration 乘法。
   
   ### 3. BOUNDARY 必须保持准确 instant
   
   BOUNDARY 应持久化为:
   
   ```text
   boundaryInstant
   zoneId
   explicitBoundary
   ```
   
   Calendar arithmetic 必须满足:
   
   ```text
   calendarApply(B, zero, Z) == B
   ```
   
   推荐的计算方式为:
   
   ```text
   calendarApply(B, D, Z):
   
     anchor = instant(B).atZone(Z)
     monthApplied = anchor.plusMonths(D.monthPart)
     result = ticks(monthApplied.toInstant()) + D.fixedPart
   ```
   
   禁止先把 BOUNDARY 转换成不带 offset 的 `LocalDateTime`,再调用 `atZone()` 转回 instant。该方式会在 
DST overlap 中丢失原始 offset,可能导致 `calendarApply(B, 0) != B`。
   
   DST 解析规则明确如下:
   
   - 计算产生的本地时间落入 DST gap 时,向后移动 gap 长度,使用第一个有效时间。
   - 计算产生的本地时间落入 DST overlap 时,优先保留原始 BOUNDARY 的 offset;该 offset 无效时选择 earlier 
offset。
   - 带显式 offset 的 BOUNDARY 表示准确 instant。
   - 无 offset 的 BOUNDARY 必须根据目标日期的 ZoneRules 解析,禁止使用当前墙钟时间对应的 offset。
   - 无 offset 的 BOUNDARY 如果落入 gap 或 overlap,应返回明确的语义错误,要求用户提供显式 offset。
   
   Calendar EVERY 省略 BOUNDARY 时,使用 CQ ZoneId 中的本地 `1970-01-01 00:00:00`,按 
ZoneRules 解析一次并持久化为准确 instant。
   
   显式 `BOUNDARY 0` 仍然表示 Unix epoch instant,不能与省略 BOUNDARY 混淆。
   
   ### 4. 调度与 RANGE 必须始终从原始锚点计算
   
   第 n 个 occurrence 定义为:
   
   ```text
   executionTime(n) =
       calendarApply(B, multiplyExact(E, n), Z)
   ```
   
   禁止:
   
   ```text
   executionTime(n + 1) =
       executionTime(n) + everyInterval
   ```
   
   因为上一次 occurrence 可能已经发生月末钳制,继续递推会造成日期漂移。
   
   调度序列必须满足:
   
   ```text
   executionTime(n + 1) > executionTime(n)
   ```
   
   RANGE 端点保持 boundary-anchored duration-vector 语义:
   
   ```text
   startTime(n) =
       calendarApply(B, n × E - startOffset, Z)
   
   endTime(n) =
       calendarApply(B, n × E - endOffset, Z)
   ```
   
   内部 duration-vector 应支持计算产生的负 month/fixed component,但用户输入的 duration component 
仍然必须为非负数。
   
   当 `RANGE == EVERY` 时必须满足:
   
   ```text
   startTime(n) == executionTime(n - 1)
   endTime(n) == executionTime(n)
   ```
   
   从而保证月末、闰日和 DST 场景下相邻窗口仍然连续。
   
   ### 5. occurrence index 是 calendar CQ 的权威调度进度
   
   结构化 calendar CQ 的持久化状态至少包括:
   
   ```text
   anchor
   zoneId
   everyDuration
   startOffset
   endOffset
   explicitBoundary
   nextOccurrenceIndex
   ```
   
   创建 CQ 时:
   
   ```text
   firstIndex = findFirstOccurrenceNotBefore(now)
   nextOccurrenceIndex = firstIndex
   ```
   
   调度时:
   
   ```text
   executionTime =
       calendarApply(
           anchor,
           everyDuration × nextOccurrenceIndex,
           zoneId)
   ```
   
   执行成功后:
   
   ```text
   BLOCKED:
       newNextIndex = currentIndex + 1
   
   DISCARD:
       newNextIndex =
           max(
               currentIndex + 1,
               findFirstOccurrenceNotBefore(currentTime))
   ```
   
   Calendar occurrence 查找应使用估算加修正或有界二分搜索,不能从 1970 年开始逐月遍历。
   
   建议新增或替换为以下共识更新:
   
   ```text
   UpdateCQProgressPlan {
     cqId
     cqToken
     expectedCurrentIndex
     newNextOccurrenceIndex
   }
   ```
   
   共识更新只有在以下条件全部成立时才成功:
   
   - `cqToken` 匹配。
   - 当前持久化 index 等于 `expectedCurrentIndex`。
   - 新 index 严格大于当前 index。
   
   这样可以阻止旧 leader 或旧调度任务覆盖新 leader 已经写入的进度。
   
   以下链路必须同步改造:
   
   - `AddCQPlan`
   - `CreateCQProcedure`
   - `CQEntry`
   - `CQScheduleTask`
   - `UpdateCQLastExecTimePlan`
   - 相关 Plan/Procedure 序列化
   - CQ snapshot
   
   ConfigNode 重启、leader 切换或 procedure 恢复时,必须直接从 `nextOccurrenceIndex` 恢复。
   
   Calendar CQ 禁止继续使用:
   
   ```text
   firstExecutionTime - everyInterval
   lastExecutionTime + everyInterval
   ```
   
   旧 CQEntry 没有 occurrence index 时,继续走 legacy fixed-duration 调度路径,不得自动迁移成 
calendar CQ。
   
   ### 6. 混合版本必须 fail closed
   
   新增集群能力标志:
   
   ```text
   CQ_CALENDAR_DURATION_V1
   ```
   
   只有所有已注册 ConfigNode 和 DataNode 都支持结构化 CQ duration 后,才能启用该能力。
   
   DataNode 和 ConfigNode 必须进行双重校验:
   
   - DataNode 在接受 calendar CQ 前检查 capability。
   - ConfigNode 在持久化 calendar CQ 前再次检查 capability。
   - capability 未启用时明确拒绝 calendar CQ。
   - 禁止回退成 30/365 天后继续创建。
   
   `TCreateCQReq` 增加:
   
   ```text
   durationFormatVersion
   structuredEveryDuration
   structuredStartOffset
   structuredEndOffset
   explicitBoundary
   ```
   
   新 DataNode 创建 CQ 时,应始终发送 `durationFormatVersion` 和结构化 duration,包括 fixed-only 
CQ。
   
   原有 required i64 字段可以保留用于 wire compatibility,但必须遵守:
   
   ```text
   durationFormatVersion 存在时,结构化字段是唯一权威数据。
   结构化字段缺失、不完整或非法时必须拒绝请求,
   不能退回 legacy i64 字段继续创建。
   ```
   
   对于实时创建请求:
   
   - 混合版本升级阶段不允许创建 calendar CQ。
   - 新 ConfigNode 不得把缺少 structured duration 的实时 calendar 请求当成 fixed CQ。
   - 如果无法可靠提供 capability handshake,则混合版本阶段应禁止 `CREATE CQ`,或者明确要求所有 ConfigNode 和 
DataNode 完成升级后才能使用该功能。
   - 不宣称 calendar CQ 与旧 DataNode 或旧 ConfigNode 兼容。
   
   对于历史 CQ:
   
   - 没有 structured duration 的旧 CQEntry 按 legacy fixed CQ 加载。
   - 即使旧 SQL 中包含 `mo/y`,也不得重新解析或自动迁移。
   - 用户通过 DROP 并重新 CREATE CQ 显式启用 calendar 语义。
   
   Snapshot 采用两阶段策略:
   
   - 新版本必须能读取 v1 和 v2 snapshot。
   - ConfigNode 混合版本阶段继续写 v1,并禁止创建 structured calendar CQ。
   - 所有 ConfigNode 完成升级后才允许写 v2。
   - v2 保存 structured duration、anchor、ZoneId、explicitBoundary 和 occurrence 
index。
   
   ### 7. Duration 比较不得使用固定天数近似
   
   Duration 比较函数应返回:
   
   ```text
   LESS
   EQUAL
   GREATER
   AMBIGUOUS
   ```
   
   规则:
   
   - 归一化结构完全相同:`EQUAL`
   - 相同 `monthPart`:比较 `fixedPart`
   - 两个 pure-fixed duration:比较 ticks
   - 两个 pure-calendar duration:比较月份数量
   - 能够对所有相关日期、时区和 DST 证明顺序时,可以比较
   - 无法证明时返回 `AMBIGUOUS`,并返回明确的语义错误
   
   例如:
   
   ```text
   1mo3d > 1mo
   1mo == 1mo
   1mo versus 30d -> AMBIGUOUS
   ```
   
   以下校验必须使用同一套比较语义:
   
   - `EVERY > 0`
   - `startOffset > 0`
   - `endOffset >= 0`
   - `startOffset > endOffset`
   - `startOffset >= EVERY`
   - `EVERY >= continuous_query_minimum_every_interval`
   
   禁止在校验阶段把月份压成固定 30 天。
   
   ### 8. TExecuteCQ timeout 必须转换为毫秒
   
   Calendar occurrence 的真实间隔首先以 timestamp ticks 计算:
   
   ```text
   deltaTicks =
       executionTime(n + 1) - executionTime(n)
   ```
   
   发送给 `TExecuteCQ` 前必须转换为毫秒并向上取整:
   
   ```text
   timeoutMs =
       deltaTicks / ticksPerMillisecond
       + (deltaTicks % ticksPerMillisecond == 0 ? 0 : 1)
   ```
   
   必须保证:
   
   ```text
   timeoutMs >= 1
   ```
   
   并处理:
   
   - ms/us/ns precision
   - 除法舍入
   - timestamp 差值溢出
   - 月份长度及 DST 导致的实际 occurrence 间隔变化
   
   ### 9. 必须满足的验收不变量
   
   实现和测试必须验证:
   
   ```text
   1. calendarApply(B, zero, Z) == B
   
   2. executionTime(n + 1) > executionTime(n)
   
   3. RANGE == EVERY 时:
      startTime(n) == executionTime(n - 1)
   
   4. 重启、leader 切换和 procedure 恢复前后:
      nextOccurrenceIndex 及其对应的 executionTime 完全一致
   
   5. 一个 calendar CQ 只能:
      - 保持结构化 calendar 语义成功创建;或者
      - 返回明确错误
   
      绝不能静默转换成 30/365 天 CQ
   ```
   
   ### 10. 补充测试要求
   
   测试除原计划外,还必须覆盖:
   
   - `mo/month`、`y/year` 的 parser 和端到端行为。
   - issue 中省略 BOUNDARY 的自然月聚合示例。
   - `calendarApply(B, 0) == B`。
   - Jan-31、Feb-29 以及 28/29/30/31 天月份。
   - America/New_York DST gap。
   - DST overlap 的 earlier/later 两个 offset。
   - 无 offset BOUNDARY 使用目标日期 offset,而不是当前日期 offset。
   - fixed-only duration 回归。
   - calendar EVERY 从 `GROUP BY(1mo/1y)` 继承。
   - BLOCKED 和 DISCARD。
   - DISCARD 恰好落在 occurrence boundary。
   - ConfigNode 重启和 leader 切换。
   - occurrence-index procedure/plan serde。
   - legacy v1 snapshot fixture 和 v2 snapshot round trip。
   - 旧 CQ 保持 legacy fixed-duration 行为。
   - capability 未启用时明确拒绝 calendar CQ。
   - ms/us/ns precision 下 timeout 单位正确。
   - checked duration arithmetic 和 timestamp overflow。
   - 一个使用 near-future BOUNDARY 的端到端 IT,验证完整链路:
   
   ```text
   SQL parser
   → structured TCreateCQReq
   → ConfigNode persistence
   → calendar scheduler
   → concrete startTime/endTime
   → TExecuteCQ
   → DataNode query
   → result written by SELECT INTO
   ```
   
   完成上述约束后,原方案可以继续作为实现基线,同时确保最终行为以 issue 为准,并避免 calendar CQ 再次出现“语法接受但实际静默按固定 
30/365 天执行”的问题。
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to