data-download-open-issues.md 21 KB

DataDownload 待解决问题

建立日期:2026-09-19
状态:持续维护
文档导航:DataDownload README;目标需求见 requirements.md;技术设计见 technical-design.md;当前实现见 implementation-status.md。 范围:DataDownload 首版实现差异、可靠性风险,以及当前阻断全仓测试的既有源码问题。

1. 使用规则

  • 状态取值:open、in_progress、blocked、resolved、wont_fix。
  • 优先级取值:
    • P0:可能造成数据错误、并发覆盖、返回旧内容或任务永久异常,发布前必须解决。
    • P1:可能造成资源泄漏、内存故障、重要功能缺失或运维风险。
    • P2:产品能力、展示或工程质量未完整对齐设计。
    • P3:仓库既有问题,不直接影响 DataDownload 主链路,但会阻断全仓验证。
  • 关闭问题时应填写解决提交、验证方式和关闭日期,不直接删除历史条目。
  • 产品目标以 requirements.md 为准,技术约束以 technical-design.md 为准,当前事实以源码、测试和 implementation-status.md 为准。

2. 汇总

ID 优先级 状态 问题
DD-001 P0 open Redis 锁没有续租和 fencing token(观测优先:第 1 层已落地,第 2/3 层待触发)
DD-002 P0 resolved PostgreSQL、Mongo Document、RenderSlot 提交不具备崩溃一致性(方向 A:先失效后提交;读侧竞态转 DD-008)
DD-003 P0 resolved FileRequestService 未按 RequestID/延迟等待模型实现
DD-004 P0 resolved plan handler 重试不幂等
DD-005 P0 resolved RequestKey 缺少影响正文身份的字段
DD-006 P0 resolved FaultRecord 文档粒度和存储模型错误
DD-007 P1 open Artifact 发布缺少 generation/token CAS 校验
DD-008 P1 open 缓存命中未校验 PostgreSQL 权威 Generation
DD-009 P1 open 二进制切换失败会产生孤儿对象
DD-010 P1 open 大文件读取与 ZIP 构建存在内存放大
DD-011 P1 open PostgreSQL migration 多实例并发不安全
DD-012 P1 open 任务删除不是事务或原子状态变更
DD-013 P1 resolved PackageJob PurgeAt 计算基准错误
DD-014 P1 open MinIO Repository 没有传播 context
DD-015 P1 open 归档内部文件读取没有大小上限
DD-016 P2 open PostgreSQL 强类型结构仍处于过渡态
DD-017 P2 open Renderer 输出未完整对齐设计
DD-018 P2 open 前端真实预览与归档浏览未实现
DD-019 P2 open 前端批量操作和部分文件选择未实现
DD-020 P2 open 任务、Package 和管理员页面过滤/权限展示不完整
DD-021 P2 open MCP 能力和幂等创建未完整实现
DD-022 P2 open MongoDB 索引与设计索引表不完全一致
DD-023 P2 open 自动化测试覆盖不足
REPO-001~007 P3 open 仓库既有编译和旧测试签名问题

3. P0:数据正确性与并发问题

DD-001 Redis 锁没有续租和 fencing token

  • 状态:open(当前采用「观测优先」策略:先补充可观测性,待真实样本再决定 CAS/TTL 方案)
  • 位置:service/cache/redisx/lockx.go、service/services/dataDownloadService.go、dataDownloadBinaryService.go
  • 当前行为:FileRequest 租约和流式读取锁已经支持 token 校验续租;普通 content 锁和 RenderArtifact 发布仍没有完整 fencing/CAS。
  • 影响:大型 Renderer 或正文提交超过 content lock TTL 后仍有租约过期窗口;Artifact 发布也仍可能被旧执行者覆盖。
  • 处理建议:把 content/render 锁统一为可续租租约,并在 Artifact/正文发布处加入 generation/token CAS。
  • 验收条件:覆盖锁续租、租约丢失、旧持有者晚提交、worker 崩溃后恢复的并发测试。

决策:观测优先,暂不实施 CAS

该问题「理论存在、实际频率未知」,真实行数量级与 PG/MinIO 实际延迟均为估算。generation CAS 改造牵连 DD-002/007/008,投入较大。因此先补充观测日志,让问题从「悄悄写坏」变为「事后可判定、可定位」,攒到真实的耗时分布与 owner 丢失样本后,再决定是上 CAS 还是仅调 TTL / 改批量写入。本条在样本充分前保持 open,不因加了日志而视为已修复。

观测手段进度

  • 第 1 层(已落地,2026-09-20):service/cache/redisx/lockx.go 的 RedisLock 内建观测,一处覆盖全部锁(content / package / request lease,cache.RedisLock 为其类型别名,调用方透明):

    • 新增 acquired(加锁时刻,单调计时)、label(从 key 前两段推断,如 data-download:content)。
    • SafeRelease 分三档记录:正常释放但持锁 > TTL×70% 记 Warn;释放时发现 not owner(锁被抢走、可能已并发覆盖)记 Error(关键铁证,带 label/key/token/held/ttl);其他释放失败记 Error。
    • 引入哨兵错误 errLockNotOwner,统一 Release/LockRelease/Extend/SetIfOwner 的 not-owner 返回值,供 errors.Is 精确判定。
    • 验证:go build ./cache/...、go test ./services ./cache/redisx ./database 通过。
  • 第 2 层(待补,出问题后再加):写入临界区分段耗时 + 数据规模。

    • 位置:ExecuteStructuredDataDownloadItem / ExecuteBinaryDataDownloadItem 拿锁之后。
    • 记录字段:documentId、taskId、kind、rowCount(结构化)/objectBytes(二进制)、分段耗时(replaceRows/minioPut/mongoWrite)、total、ttl。
    • 日志策略(定稿):正常路径打 Debug(生产默认 Warn 级别,不输出,零成本;需要时可临时调级看全量);total ≥ TTL×50% 打 Warn 带完整分段。默认零噪音,与第 1 层职责不重叠(第 1 层管「有没有出事/接近出事」,第 2 层管「出事时慢在哪一步」)。
  • 第 3 层(待补,视第 2 层结果再加):ReplaceDataDownloadRows 内部 pg_advisory_xact_lock 获取耗时。

    • 目的:区分「逐行 insert 慢」与「advisory 锁等待慢(写-写争用,正是 DD-001 的并发场景)」。
    • 触发条件:第 2 层日志显示 replaceRows 大但 rowCount 不高时,再补此层定位。
    • 记录:advisory 锁获取耗时,> 若干秒打 Warn。

DD-002 PostgreSQL、Mongo Document、RenderSlot 崩溃一致性

  • 状态:resolved(方向 A,2026-09-20)
  • 位置:service/services/dataDownloadService.go(ExecuteStructuredDataDownloadItem 提交段)
  • 原行为:先提交 PostgreSQL,再更新 Mongo Document,最后断开 RenderSlot。「PG 已提到 N、Mongo Document 仍为 N-1」的窗口内,currentArtifact 因 slot.ContentGeneration == document.ContentGeneration(都为 N-1)判定命中,静默返回旧代缓存且不自愈。
  • 解决方式(方向 A:先失效、后提权威、再回填):将 DetachDataDownloadRenderSlots 从「PG + Upsert 之后」提前到 ReplaceDataDownloadRows 之前。断开 RenderSlot 后 currentArtifact 会因 slot.ArtifactID==nil 判定 miss(GetDataDownloadRenderSlot 无 slot 时返回 ErrNoDocuments,调用方仅在 err==nil 时命中,其余一律重渲染),强制后续读者从 PG 当前 generation 重新渲染。
    • 逐窗口验证(初始三者均为 N-1,目标 N):
    • Detach 后 / PG 前崩溃:slot 断开、PG=N-1 → 读 miss 重渲染读 PG(N-1),正确。
    • PG 后 / Upsert Document 前崩溃(原危险窗口):slot 断开、PG=N、Doc=N-1 → 读 miss 重渲染读 PG(N),正确,不再命中旧代缓存。
    • 全部成功:slot 断开、PG=N、Doc=N → 读 miss 重渲染读 PG(N),正确。
    • 反向风险:Detach 提前后若 PG 提交失败返回,仅表现为缓存被清、下次读重渲染同一代内容,无数据错误。
  • 影响范围:仅结构化路径(PowerFlow/MqttLog/EventAlarm/UpdateLog)。二进制路径(AlarmSnapshot/EmsLog/FaultRecord)走 BinaryObjectKey 直读、无 RenderSlot,不涉及本窗口。
  • 验证:go build ./services/...、go vet ./services/、go test ./services ./repositories/pgsql ./database 通过;崩溃注入的端到端恢复测试仍归 DD-023。
  • 残留(转由其他条目跟踪):
    • DD-008:非崩溃的正常并发下,读取仍只比较 Mongo 侧 generation、未回读 PG 权威 generation。方向 A 消除了「崩溃后命中旧缓存」,但「读取信任 Mongo 副本而非 PG 权威」的读侧竞态需由 DD-008(缓存命中回读 PG generation)闭合。
  • 说明:本系统尚未上线、数据库无历史 DataDownload 数据,不存在旧提交顺序遗留的不一致文档,因此无需存量对账/修复任务。方向 A 自首条写入起即生效。

DD-003 FileRequestService 请求协调缺失

  • 状态:resolved
  • 解决日期:2026-09-19
  • 解决方式:新增独立 data-download:request:{DocumentID} 租约,RequestID 使用租约 token;等待者携带 AwaitRequestID 按 15/30/60 秒退避重新入队,保持 pending,不占用 worker。租约自动续期,提交前校验 owner token,完成标记通过 Lua 在仍持有租约时原子写入。Mutable 同轮等待者复用对应 RequestID 的结果;Dynamic 完成等待后重新竞争下一轮租约。
  • 验证:DataDownload 后端相关包编译和单元测试通过;跨进程 Redis/Asynq 集成场景仍归 DD-023。

DD-004 plan handler 重试不幂等

  • 状态:resolved
  • 位置:service/task/queue/dataDownloadQueue/dataDownloadQueue.go:132-195
  • 解决日期:2026-09-19
  • 解决方式:item 写入改为基于 (taskId,itemKey) 的无序 BulkWrite upsert,并回填已存在 item 的 _id;稳定 Asynq TaskID 冲突按“已经调度”处理。plan 和 site_validation 重规划均可重复执行。
  • 验证:相关包编译通过;MongoDB/Asynq 故障注入测试仍归 DD-023。

DD-005 RequestKey 维度不足

  • 状态:resolved
  • 位置:service/services/dataDownloadService.go:168-170
  • 解决日期:2026-09-21
  • 解决方式:新增独立的规范化 RequestKey 构造器,按数据类型选择实际影响正文身份的字段;UpdateLog 不带日期和时区,MQTT/EMS 按需包含 SubType,按本地日期查询或解析的类型包含实际时区,FaultRecord 使用 DeviceSN/DevType 设备级身份。每种类型使用独立 content_version 合并管理 query/parser/schema 语义版本,RendererVersion 不进入 RequestKey。
  • 链路:DocumentID、Redis 请求租约和 Mongo Document 均使用同一个规范化 RequestKey;任务内部 ItemKey 不再充当文档身份。
  • 验证:增加所有数据类型的确定性、字段隔离、转义和非法身份单元测试;DataDownload 服务、队列及 Mongo/PostgreSQL Repository 相关测试通过。

DD-006 FaultRecord 模型错误

  • 状态:resolved
  • 解决日期:2026-09-21
  • 解决方式:规划阶段按设备拆分 DeviceSN + DevType + DataDate item;执行阶段建立 DataDownloadFaultRecordEntry 多文件目录与任务级 DataDownloadFileAttempt,每个录波文件独立上传、发布和重试,不再创建每日 ZIP。TaskDocument 路径包含设备和日期,任务详情返回文件目录及 Attempt,并提供校验 task owner/admin 的任务入口单文件下载接口。
  • 新增决策:FaultRecord 同样执行 D+2,Frozen 作用于 DeviceSN + DevType + DataDate 文档单元;成功文件立即可用,文件级失败使 item/task 为 partial,旧当前文件在刷新失败时继续服务但不能计作新任务成功;上游后续未返回的历史文件继续保留;首版一键重试全部失败文件,不提供单文件人工重试。
  • 访问语义:底层 Document/File 可跨任务复用,普通用户只能通过自己的 TaskDocument 入口访问;删除任务即失去入口但不删除共享文件。MCP 使用非管理员特殊用户 USER_AI,AI 以数据可用性为中心查询,AI任务仅管理员可见。
  • 验收条件:支持一个日文档关联零到多个原始对象;空结果可固化;目录按设备和原始文件展示;保存文件级 Attempt;partial ZIP 包含成功文件及 DataDownloadReport.json;D+2 后完整查询且无失败条目才能 Frozen。
  • 验证:FaultRecord 空结果和完整结果按 D+2 规则写入 Mutable/Frozen;失败文件不切换旧 Entry,成功文件立即可读;一键重试只会再次处理未成功文件;PackageJob 收集独立对象,并在存在失败时生成含任务汇总、设备、日期、失败阶段和原因的 DataDownloadReport.json。相关 Service、Queue、Controller、Route、Mongo/PostgreSQL 包测试通过。

4. P1:可靠性、资源和运维问题

DD-007 Artifact 发布缺少 CAS

  • 当前行为:PublishDataDownloadArtifact 直接按 (documentId,format) upsert Slot。
  • 处理建议:增加 RendererVersion、CacheKey、Slot.Generation、渲染 token 和 Artifact 状态;发布时原子校验 PG Generation 与 Slot generation。

DD-008 缓存命中未校验 PostgreSQL Generation

  • 当前行为:只比较 Mongo Document 和 Slot 的 Generation。
  • 影响:DD-002 的失败窗口中可能继续返回旧 Artifact。
  • 处理建议:在文档锁内读取 PG content_state,只有三者一致才允许命中。

DD-009 二进制上传孤儿对象

  • 位置:service/services/dataDownloadBinaryService.go:129-160
  • 当前行为:上传成功后 Mongo Upsert 失败会直接返回。
  • 处理建议:失败时尽力删除新对象;删除失败则登记 orphan/retiring 记录供清理任务重试。

DD-010 大对象内存放大

  • 位置:Renderer、BinaryService、PackageService、Controller。
  • 当前行为:HTTP 的 Package、二进制文档和缓存 RenderArtifact 已改为流式响应;Renderer 新生成内容、BinaryService 上游文件和 ZIP 构建仍使用 []byte/bytes.Buffer。
  • 影响:多站点、600 天或大录波任务可能导致 OOM。
  • 处理建议:使用受控临时文件或流式管道;限制单条预览大小;MinIO 上传支持流和取消。

DD-011 migration 多实例并发不安全

  • 位置:service/database/data_download_schema.go
  • 当前行为:自制 migration table,没有 migration advisory lock,也没有 goose/embed。
  • 影响:两个实例首次启动可能同时判断版本缺失,其中一个因版本主键冲突启动失败。
  • 处理建议:采用 goose/embed,或至少在迁移事务前获取固定 PostgreSQL advisory lock。

DD-012 任务删除非事务化

  • 位置:service/repositories/mongo/dataDownloadRepo.go 的 DeleteDataDownloadTask。
  • 当前行为:依次删除 TaskDocument、Item、Task。
  • 影响:中途失败形成部分删除;状态检查与删除之间也存在竞态。
  • 处理建议:Mongo transaction,或使用 deleting 状态和可重入清理流程;删除主记录时带终态条件。

DD-013 PackageJob PurgeAt 计算错误

  • 状态:resolved
  • 位置:service/services/dataDownloadCleanupService.go:27
  • 解决日期:2026-09-19
  • 解决方式:清理时统一写入 PurgeAt = ExpiresAt + 30天,不再以清理实际执行时间为基准;下载与清理使用同一个 Package 锁,避免流式响应期间删除对象。

DD-014 MinIO context 未传播

  • 位置:service/repositories/minio/dataDownloadRepo.go 和现有 utils/minio。
  • 当前行为:Repository 接收 context,但底层 helper 不使用它。
  • 影响:用户取消、任务超时或服务关闭后,对象操作不能及时中断。

DD-015 归档读取缺少大小上限

  • 位置:service/services/dataDownloadArchiveService.go。
  • 当前行为:内部文件按 tar header.Size 读入内存。
  • 影响:异常或恶意归档可能消耗大量内存。
  • 处理建议:配置预览上限;超限只允许流式下载;验证 header.Size 和实际读取量。

5. P2:功能和设计完整性问题

DD-016 PostgreSQL 结构仍是过渡态

  • 强类型列已经加入,但仍保留早期 payload jsonb、通用 UpdateLog 表和冗余字段。
  • UpdateLog device version 尚未展开为完整 Gate/子设备版本行。
  • EventAlarm 的可解析开始/结束时间列不完整。
  • 应制定新 migration,清理或明确保留兼容列,避免继续在同一个迁移版本追加语句。

DD-017 Renderer 未完整对齐

  • PowerFlow HTML 当前输出 JSON <pre>,未生成 ECharts。
  • UpdateLog XLSX 当前是单列 payload,未生成 SystemUpdate、aHubUpdate 两个 Sheet。
  • JSON 输出没有文档元信息和结构化 envelope。
  • MQTT/EventAlarm 固定列已经实现,但仍需使用真实数据回归旧导出格式。

DD-018 前端真实预览与归档浏览缺失

  • 当前任务和管理员页面只有下载按钮。
  • 尚无 html/csv/xls/xlsx/log/txt/json 真实预览。
  • 后端已有 tar.gz 列表/内部读取 API,但前端没有目录浏览和内部文件预览。

DD-019 批量操作和部分文件选择缺失

  • 后端 Package API 可以接受多个 TaskID,但页面没有任务多选。
  • 没有批量删除、目录树部分文件选择及权限不符项静默跳过。
  • 没有“解析后一个实际文件直接下载,两个以上创建异步 ZIP”的选择逻辑。

DD-020 页面过滤和权限展示不完整

  • 任务创建时间过滤后端已支持,但前端未接入。
  • 描述修改 API 已有,但前端未接入。
  • 查看“所有任务”时,非 owner 仍能看到取消/重试/删除按钮,只在后端拒绝。
  • Package 页面缺少状态和创建时间过滤。
  • 管理员页面缺少 Kind、SN、日期、SubType、状态过滤和完整对象清单。

DD-021 MCP 能力不完整

  • 当前只有任务列表、创建任务和文档分页列表。
  • 缺少文档过滤/详情/读取、tar.gz 浏览、存在性判断、AI 活跃任务覆盖判断。
  • “检查已有文档/任务后创建”没有规范化指纹或 Redis 锁,重复请求会创建重复任务。

DD-022 MongoDB 索引与设计不完全一致

  • 缺少 Document {state,updatedAt}。
  • Task 状态索引使用 updatedAt,设计为 createdAt。
  • TaskDocument 唯一键不是设计中的 {taskId,logicalPath}。
  • Artifact 没有唯一 CacheKey 索引。
  • Package 缺少 {status,createdAt} 和 expiresAt 索引。
  • 所有索引尚未设置固定名称,不利于显式迁移和审计。

DD-023 自动化测试不足

至少需要增加:

  • IANA 时区、固定半小时偏移、DST 跨越和 D+2 边界测试;
  • Mutable 同轮合并、后续刷新、Frozen 复用、Dynamic 串行测试;
  • 锁续租、租约丢失、旧 worker 晚提交和崩溃恢复测试;
  • PostgreSQL 全量替换、Generation、空正文和回滚测试;
  • RenderSlot 失效/发布竞态与 3 天过期测试;
  • 取消、删除、人工重试、Package 7/30 天生命周期和权限测试;
  • MongoDB/PostgreSQL/Redis/MinIO 集成测试。

6. P3:阻断全仓测试的既有源码问题

这些问题不是 DataDownload 引入,但当前会使 go test ./... 失败。

REPO-001 PostgreSQL model 语法错误

  • 文件:service/models/model_pgsql/siteOriginalData.go:54
  • 问题:Go 语法错误,编译器报告 unexpected keyword type。

REPO-002 FWH 代码生成命令引用失效配置

  • 文件:service/cmd/gen/fwh_pro.go
  • 问题:引用不存在的 cfg.MySQLDSN。

REPO-003 PostgreSQL 代码生成命令引用失效配置

  • 文件:service/cmd/gen/pgsql/alarm_snapshot.go
  • 问题:引用不存在的 cfg.PostgresDSN。

REPO-004 多个旧命令/测试调用旧 logger.Init 签名

  • 范围:service/cmd/test_plane_v2、service/repositories/mqtt/fwh_mqtt_test/test、service/test/shutdownStatistics、service/test/snapTest、service/test/getSnapTest、service/test/temp 等。
  • 问题:仍只传一个参数;当前签名要求环境和日志级别两个参数。

REPO-005 getApowerSn 对指针切片直接 range

  • 文件:service/test/getApowerSn/getApowerSn.go
  • 问题:res.Result 为 *[]domain.FwhSiteInfo,需要先判空并解引用。

REPO-006 getSnapTest 调用已删除函数

  • 文件:service/test/getSnapTest/getAlarmSnapshotTest.go
  • 问题:调用不存在的 jobs.GetAlarmSnapshot。

REPO-007 temp 测试使用过期函数签名

  • 文件:service/test/temp/runtimeSort.go
  • 问题:GetSiteAnalysisManageList 的调用参数数量与当前签名不一致。

7. 建议处理批次

  1. 并发正确性批次:DD-001、DD-007、DD-008;DD-002、DD-003、DD-004、DD-005 已解决。
  2. 存储模型批次:DD-006、DD-009、DD-011、DD-016、DD-022。
  3. 资源安全批次:DD-010、DD-014、DD-015;DD-013 已解决。
  4. 产品补全批次:DD-017~DD-021。
  5. 测试与仓库健康批次:DD-023、REPO-001~REPO-007。