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

   ## 中文对照:针对评审阻塞问题的修订补充说明
   
   感谢你结合当前实现详细审阅这份方案,并指出其中的问题。你的意见非常有帮助,我同意你提出的这些问题,也同意前三项属于正确性和兼容性阻塞问题。
   
   如果本修订与此前的方案或补充说明存在冲突,以本修订为准;此前方案中未发生冲突的部分保持不变。
   
   ### 1. CQ 与 `GROUP BY TIME` 使用完全相同的日历及 DST 解析规则
   
   CQ 实现将不再引入此前提出的“DST overlap 时优先使用原始 anchor offset”的规则。
   
   CQ occurrence 和 RANGE 的计算将复用当前 `GROUP BY TIME` 自然月路径使用的日历转换逻辑,包括以下行为:
   
   ```java
   LocalDateTime.plusMonths(...).atZone(zoneId)
   ```
   
   实现将复用 `DateTimeUtils.calcPositiveIntervalByMonth` 及其底层的 
`TimeDuration.calcPositiveIntervalByMonth` 行为,或者将该行为提取为 CQ 和 `GROUP BY TIME` 
共同使用的 helper。
   
   最终规则如下:
   
   - DST overlap 使用较早的有效 offset,与标准 `atZone` 规则保持一致。
   - DST gap 按照 zone transition 规则向前调整。
   - 解析后续 overlap 时不优先使用原始 anchor offset。
   - 先应用日历月分量。
   - 随后将固定时长分量作为 elapsed ticks 添加。
   - 仅包含固定时长的 duration 继续使用经过检查的 elapsed-tick 加法。
   - 不使用宿主机默认时区。
   
   可以扩展公共 helper,以支持 RANGE 计算需要的溢出检查和内部有符号 duration vector,但不能改变现有的本地时间解析行为。
   
   对于评审中给出的示例:
   
   ```text
   Zone = America/New_York
   B = 2024-01-03 01:30 -05:00
   E = 10mo
   目标本地时间 = 2024-11-03 01:30
   ```
   
   CQ 和 `GROUP BY TIME` 都选择 overlap 中较早的 offset:
   
   ```text
   2024-11-03 01:30 -04:00 = 05:30Z
   ```
   
   测试将覆盖普通日期、28/29/30/31 天月份转换、月末钳制、闰日 anchor、DST gap 和 DST overlap,并断言 CQ 
生成的边界 instant 与 `GROUP BY TIME` 生成的边界 instant 完全相同。
   
   本节取代此前补充说明中的 preferred-`anchorOffset` 规则。
   
   ### 2. 使结构化 wire format 和混合版本拒绝机制可以被强制执行
   
   请求将同时携带显式的 duration 编码版本和结构化 duration。例如:
   
   ```thrift
   struct TCQDuration {
     1: required i64 monthPart
     2: required i64 nonMonthDuration
   }
   
   struct TCreateCQReq {
     // 现有字段 1-10 保持不变。
   
     11: optional i16 durationEncodingVersion
     12: optional TCQDuration everyDuration
     13: optional TCQDuration startOffsetDuration
     14: optional TCQDuration endOffsetDuration
     15: optional bool boundaryExplicit
   }
   ```
   
   当 `durationEncodingVersion = 1` 时:
   
   - `everyDuration`、`startOffsetDuration`、`endOffsetDuration` 和 
`boundaryExplicit` 必须全部存在。
   - 只提供部分结构化字段的请求必须被拒绝。
   - 未知的编码版本必须被拒绝。
   - 结构化 duration 是权威值。
   - 所有月份值、固定值、算术操作和窄化转换都必须检查溢出与越界。
   - 新 DataNode 创建任何新 CQ 时都设置编码版本和全部结构化字段,包括 fixed-only CQ。
   
   没有 `durationEncodingVersion` 的请求视为 legacy protocol 请求。
   
   新 ConfigNode 会在 CREATE 入口拒绝没有 marker 的请求,而不是自动将其解释为 fixed-only 请求。这样可以阻止旧 
DataNode 把已经压平为 legacy `i64` 的日历 duration 发送给新 ConfigNode。
   
   因此在滚动升级期间,旧 DataNode 不能通过新 ConfigNode 创建 CQ。已经持久化的 CQ 不受影响,继续按照原有语义执行。
   
   加载已有 procedure、plan、metadata 或 snapshot 时遇到的 legacy 表示仍然受支持,并归一化为 legacy 
fixed-duration CQ。上述拒绝只适用于 RPC 入口的新 CQ 创建,不适用于已有状态的恢复。
   
   包含日历分量的 version-1 请求还必须通过强制能力屏障:
   
   1. 转发请求之前,接收 SQL 的 DataNode 检查所有已注册 ConfigNode,以及所有能够接收客户端 SQL 的已注册 
DataNode,确认它们都支持 duration encoding version 1。
   2. 接收请求的 ConfigNode 在接受请求之前,基于权威集群节点信息再次执行相同检查。
   3. 任意节点版本过旧、未知或不受支持,都必须拒绝请求并返回清晰的兼容性错误。
   4. 只要旧 ConfigNode 仍可能反序列化或应用该请求,就不得提交包含日历 duration 的请求。
   5. 在生成新的 consensus plan、procedure 序列化或 CQ snapshot 表示之前,也必须满足同一能力屏障。
   
   现有必填 legacy `i64` 字段保持不变,并按以下规则赋值:
   
   - 当三个结构化 duration 的 `monthPart` 都为 `0` 时,legacy 字段填写完全相同的固定时长值。
   - ConfigNode 验证 legacy 值与 structured fixed 值完全相同。
   - 如果任意最终生效的 duration 具有非零 `monthPart`,则不存在合法的 legacy 表示。
   - 此时三个 legacy duration 字段都填写无效哨兵值 `0`。
   - 新 reader 在验证 version-1 结构化表示后不使用这些哨兵字段。
   - “所有 ConfigNode 均支持新格式”的能力屏障保证旧 reader 不会接收到这种日历请求。
   - 哨兵值不能代替 reader barrier,也不能作为兼容性表示使用。
   - 日历 duration 绝不能在 legacy 字段中近似为 28、30 或 365 天。
   
   以下请求必须被拒绝:
   
   - 存在 marker,但没有提供全部结构化字段;
   - 存在结构化字段,但没有受支持的 marker;
   - 编码版本未知;
   - fixed legacy 值与 structured 值冲突;
   - 包含日历 duration,但所需节点能力不可用;
   - 新 ConfigNode 收到用于新建 CQ、但没有 marker 的请求。
   
   这样可以堵住旧 DataNode 到新 ConfigNode 的请求入口漏洞,同时明确必填 legacy 字段的值和强制 reader barrier。
   
   ### 3. 使用精确的 consensus CAS 和 fencing token 推进 `nextOccurrenceIndex`
   
   `nextOccurrenceIndex` 定义为第一个尚未被持久化标记为完成或丢弃的 occurrence。
   
   进度推进使用一个至少包含以下字段的 consensus plan:
   
   ```text
   cqId
   cqToken
   expectedIndex
   targetIndex
   ```
   
   CQ token 同时作为 CQ generation 和 fencing token。状态机原子地应用以下转换:
   
   ```text
   if storedToken == cqToken
      and nextOccurrenceIndex == expectedIndex:
       nextOccurrenceIndex = targetIndex
   ```
   
   结果定义如下:
   
   - CQ 不存在时,callback 停止。
   - `cqToken` 不匹配时,callback 被 fencing 并停止。
   - 已存储 index 等于 `expectedIndex` 时,状态机将其更新为 `targetIndex` 并返回 `ADVANCED`。
   - 已存储 index 大于 `expectedIndex` 时,callback 已经过期或重复。状态机返回 `STALE`,该 callback 
不得再调度任务。
   - 已存储 index 小于 `expectedIndex` 时,状态机返回一致性错误,该 callback 不得再调度任务。
   
   对于 `BLOCKED`:
   
   ```text
   expectedIndex = n
   targetIndex = n + 1
   ```
   
   对于 `DISCARD`,在 occurrence `n` 成功执行后,只捕获一次 callback time:
   
   ```text
   callbackTime = 只捕获一次
   lowerBound(t) = min { k >= 0 | executionTime(k) >= t }
   
   expectedIndex = n
   targetIndex = max(n + 1, lowerBound(callbackTime))
   ```
   
   该定义覆盖 callback time 恰好等于某个 occurrence、callback 延迟、错过多个 
occurrence、系统时钟回拨,以及前任 leader 遗留的过期 callback。
   
   callback 只有在自己的精确 CAS 返回 `ADVANCED` 后,才允许调度目标 occurrence。在进度写入失败、token 
不匹配、返回 `STALE` 或出现一致性错误后,不得递增内存中的 occurrence index,也不得调度其他任务。
   
   如果 consensus write 失败或提交结果不明确,callback 使用相同的 `cqToken`、`expectedIndex` 和 
`targetIndex` 重试同一个转换。它不能计算另一个 target,也不能根据本地状态重新建立转换基准。
   
   如果第一次写入实际已经提交,只是 acknowledgement 丢失,那么重试会发现已存储 index 已大于 `expectedIndex`,收到 
`STALE` 后停止,不会创建竞争调度链。
   
   leader 恢复和 scheduler reconciliation 始终读取持久化的 `(cqToken, 
nextOccurrenceIndex)`,并且最多为该持久化 pair 安装一个任务。过期 callback 永远不能根据自己的本地 execution 
time 派生新调度链。
   
   恢复流程如下:
   
   ```text
   持久化的 boundary + 持久化的 durations
   + 持久化的 zone + 持久化的 nextOccurrenceIndex
   -> 计算准确 occurrence
   -> 安装一个由 (cqToken, occurrenceIndex) 标识的任务
   ```
   
   执行语义仍然是 at-least-once。如果查询已经成功执行,但 CAS 在故障前没有被持久提交,恢复后可能再次执行同一个 
occurrence。该设计用于防止竞争调度链,不引入 exactly-once 查询执行协议。
   
   测试将覆盖:旧、新 leader callback 对同一个 index 的竞争;重复 callback;consensus 
响应丢失或结果不明确;使用同一 CQ ID 执行 DROP 后重新 CREATE 引发的 token 不匹配;使用相同 expected index 的 
CAS 重试;`DISCARD` 跳过多个 occurrence;相等和时钟回拨情况;以及无 off-by-one 偏移的恢复。
   
   ### 4. 定义确定且可以直接实现的 duration 偏序
   
   归一化后,用户提供的 duration 表示为:
   
   ```text
   D = (M, F)
   ```
   
   其中 `M >= 0` 是日历月分量,`F >= 0` 是固定 tick 分量;当所在 clause 要求 duration 
为正时,至少一个分量必须为正。
   
   duration 之间的校验使用分量支配关系:
   
   ```text
   D1 >= D2 iff M1 >= M2 and F1 >= F2
   D1 > D2  iff D1 >= D2 and D1 != D2
   ```
   
   示例:
   
   ```text
   1mo3d > 1mo       // 接受
   2mo >= 1mo        // 接受
   12mo == 1y        // 归一化后相等
   1mo vs 30d        // 不可比较
   30d vs 1mo        // 不可比较
   2mo vs 1mo40d     // 不可比较
   ```
   
   如果某项约束要求确定的顺序,而两个 duration 不可比较,则拒绝语句并返回清晰的语义错误。该偏序用于以下 duration-to-duration 
约束:
   
   ```text
   startOffset > endOffset
   startOffset >= EVERY
   ```
   
   这些比较不使用任何“月转天”的近似值。
   
   针对固定配置项 `continuous_query_minimum_every_interval` 的校验使用一套独立且确定的 elapsed-time 
下界。
   
   对于 EVERY duration `(M, F)`:
   
   ```text
   if M == 0:
       elapsedLowerBound = F
   else:
       elapsedLowerBound = M * 28d - 36h + F
   ```
   
   该计算在配置的 timestamp precision 下使用 checked arithmetic。下界基于以下平台级限制:
   
   - 向前移动 `M` 个正 Gregorian 日历月,在本地日期时间上至少跨越 `M * 28d`。
   - Java `ZoneOffset` 的范围是 `-18:00` 到 `+18:00`。
   - 因此两个解析后端点的 offset 差异最多使 elapsed time 减少 36 小时。
   - 随后将固定分量作为 elapsed ticks 添加。
   
   只有满足以下条件时才接受 CQ:
   
   ```text
   elapsedLowerBound >= continuous_query_minimum_every_interval
   ```
   
   否则因为无法证明满足最小间隔而保守拒绝。
   
   该规则不会把“每月 28 天”直接当成 elapsed duration 使用;额外的 36 小时 offset 界限覆盖 DST 以及 Java 
time model 能表示的更大历史 offset transition。因此 CREATE 的接受结果不依赖宿主机默认时区,也不依赖特定 TZDB 版本的 
transition 数据。计算下界时发生溢出属于语义错误。
   
   测试将覆盖:相等 duration vector;严格和非严格的分量支配;不可比较的日历与固定 duration;复合 
duration;minimum-EVERY 下界临界值;以及 `ms`、`us`、`ns` precision 下的 checked overflow。
   
   ### 5. CQ 与现有 Tree SQL duration 别名保持一致
   
   本 issue 不会为 CQ 引入私有的 `month` 或 `year` 别名。
   
   支持的日历单位仅限现有 Tree SQL 缩写:
   
   ```text
   mo
   y
   ```
   
   单位匹配仍然大小写不敏感,`y` 归一化为十二个日历月。
   
   以下别名不属于本 issue 的范围:
   
   ```text
   month
   months
   year
   years
   ```
   
   如果将来增加完整单位别名,应通过单独的 Tree SQL grammar 改动,在所有 duration 位置一致地引入,包括 `GROUP BY 
TIME`、日期运算、`FILL`、`SESSION` 和 CQ 的 `EVERY`/`RANGE`。
   
   因此一条 CQ 将统一写成:
   
   ```sql
   RESAMPLE EVERY 1mo RANGE 1mo
   ...
   GROUP BY(1mo)
   ```
   
   本节取代此前补充说明中提出 CQ 私有 `month` 和 `year` 别名的部分。
   
   ### 保留的实现决定
   
   此前方案中的以下内容保持不变:
   
   - duration 使用结构化 `(monthPart, nonMonthDuration)` 表示。
   - `1y` 归一化为 `12mo`。
   - 每个 occurrence 都从原始 boundary 推导:
   
   ```text
   executionTime(n) = calendarApply(B, n * E, Z)
   ```
   
   - 不通过向上一个已被钳制的 occurrence 反复添加 duration 来生成 occurrence。
   - RANGE 端点在 duration-vector 空间中从原始 boundary 推导:
   
   ```text
   startTime(n) = calendarApply(B, n * E - startOffset, Z)
   endTime(n)   = calendarApply(B, n * E - endOffset, Z)
   ```
   
   - 持久化 boundary、`boundaryExplicit`、zone、归一化 durations、CQ token 和 occurrence 
index。
   - 已持久化 CQ 保持原有 legacy fixed-duration 行为。
   - 已被压平的 `mo`/`y` CQ 不会被静默迁移;重新创建 CQ 是明确选择日历语义的方式。
   - DataNode 接收具体的查询开始和结束时间戳。
   - `TExecuteCQ.timeout` 根据相邻两个实际 occurrence instant 计算:
   
   ```text
   deltaTicks = executionTime(n + 1) - executionTime(n)
   timeoutMs = ceil(deltaTicks / ticksPerMillisecond)
   ```
   
   - 重启、leader 恢复和 snapshot 恢复从持久化的 boundary-relative occurrence index 继续。
   - 所有 duration 乘法、vector 算术、timestamp 转换和 timeout 转换都使用 checked arithmetic。
   
   ### 更新后的定向测试计划
   
   实现测试将包括:
   
   1. `EVERY 1mo`、`EVERY 1y`,以及从 `GROUP BY(1mo)`/`GROUP BY(1y)` 继承 EVERY。
   2. 拒绝 `month`、`months`、`year` 和 `years`。
   3. 普通日期、月末、闰日、DST gap 和 DST overlap 下 CQ 与 `GROUP BY TIME` 的 instant 完全相同。
   4. version-1 结构化请求 round trip。
   5. 拒绝部分字段、冲突字段、未知版本以及用于新建 CQ 的 markerless 请求。
   6. 在 DataNode ingress 和 ConfigNode acceptance 两处拒绝混合版本下的日历 CQ CREATE。
   7. 必填 legacy 字段的 sentinel 行为和强制 ConfigNode reader barrier。
   8. legacy fixed-duration request、plan、procedure 和 snapshot 恢复。
   9. 分量式 duration 比较和不可比较 duration 的拒绝行为。
   10. fixed minimum-EVERY 下界校验。
   11. CAS 推进、重复 callback、leader 切换、token fencing 和结果不明确的 consensus write。
   12. `BLOCKED` 和 `DISCARD` 恢复,包括相等和时钟回拨情况。
   13. boundary-relative 复合 schedule 和 RANGE 计算。
   14. checked overflow 以及 `ms`/`us`/`ns` timeout 转换。
   
   完成这些修改后,DST 规则对齐、混合版本请求入口保护、精确进度转换、确定性比较规则以及 Tree SQL 别名一致性都属于实现要求。
   


-- 
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