fix(fam-core): 修复 sync_identity_map 增量同步 1062 主键冲突导致游标卡死
Oracle→NAS 增量同步反复报 (1062, "Duplicate entry '1073-汤圆' for key 'uq_video_raw_uid'"),整批失败、游标永不推进,同步状态页卡在"同步中"。 根因:sync_identity_map 以 Oracle person_identity_map.id(代理自增键) 作 NAS 主键,但 Oracle 库重建/恢复后这个 id 会被复用——实测 Oracle 现在 的 id=744 对应 (1073,'汤圆'),NAS 上 id=744 还是旧的 (1302,'人物E')。 upsert 先按 PK id 命中旧行,UPDATE 成 (1073,'汤圆') 后与已存在的同名行 在 uq_video_raw_uid 上二次冲突。 修复:镜像表改为 NAS 本地自增 id 主键,业务键 (video_id, raw_uid) 唯一, Oracle 的 id 只落 oracle_id 列做溯源;upsert 的 UPDATE 子句不再改写 video_id/raw_uid 这两个键列,只更新 oracle_id/canonical_name/source 等。 ddl.sql 同步更新表定义。 已在 NAS 上完成现网迁移(ADD COLUMN oracle_id -> 回填 -> 主键改自增)并 验证:补拉同步成功恢复,游标正常推进,手动触发同步验证通过。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,3 +14,7 @@
|
|||||||
- 孤儿云库 sentinel_home_ai + famcore 用户已 DROP(新服务器)。
|
- 孤儿云库 sentinel_home_ai + famcore 用户已 DROP(新服务器)。
|
||||||
- SS Webhook:代码中为可选补充(motion_bp.py 文档明确),SS 未配行动规则故休眠,
|
- SS Webhook:代码中为可选补充(motion_bp.py 文档明确),SS 未配行动规则故休眠,
|
||||||
是否删除待用户拍板。
|
是否删除待用户拍板。
|
||||||
|
- **用户拍板:删除 webhook + NAS 前端托管,并已提交推送**(两个 commit 已 push 到 Gitea:
|
||||||
|
`7948f55` docs 迁移文档、`cf3d145` refactor 移除 webhook/static_app 共删 329 行)。
|
||||||
|
**待办:代码尚未部署到 NAS**——需同步 fam-core 到 NAS、删 NAS fam-ui 目录 + start_ui.sh、
|
||||||
|
重启 fam-core 验证 /api 与登录(云端 :80 前端不受影响)。
|
||||||
|
|||||||
37
.workbuddy/memory/2026-08-29.md
Normal file
37
.workbuddy/memory/2026-08-29.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# 2026-08-29 工作日志
|
||||||
|
|
||||||
|
## 修复 Oracle→NAS 增量同步 1062 (uq_video_raw_uid 重复键) — 已部署验证
|
||||||
|
|
||||||
|
### 现象
|
||||||
|
- 同步状态页:状态"同步中"、`最近 —`、`本次增量 —`、游标卡在 `2026-08-29 04:57:15`
|
||||||
|
- 报错:`(1062, "Duplicate entry '1073-汤圆' for key 'uq_video_raw_uid'")`,游标永不推进
|
||||||
|
|
||||||
|
### 根因(两层)
|
||||||
|
1. **镜像表主键设计缺陷(主因)**:`sync_identity_map` 以 Oracle `person_identity_map.id` 作主键。
|
||||||
|
Oracle 端 id 是 AUTOINCREMENT 代理键,但 Oracle 库重建/恢复后 id 会被复用——实测 Oracle
|
||||||
|
现在的 id=744 是 `(1073,'汤圆')`,而 NAS 上 id=744 还是旧的 `(1302,'人物E')`。旧 upsert 的
|
||||||
|
`ON DUPLICATE KEY UPDATE` 先按 **PK id** 命中旧行,再把它 UPDATE 成 `(1073,'汤圆')`,与
|
||||||
|
已有的 `(1073,'汤圆')` 行在 `uq_video_raw_uid` 上二次冲突 → 1062 → 整批失败 → 游标不推进
|
||||||
|
→ 每 30 分钟重复失败。
|
||||||
|
2. **陈旧 __pycache__(部署陷阱)**:NAS 上 `.pyc` 时间戳比 `.py` 新(部署时 `.py` 带旧 mtime 拷贝),
|
||||||
|
Python 信任 pyc 加载了旧逻辑;另曾有两个 gunicorn master 并存(8487/10914),只有一个绑 8000。
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
- **db_layer.py `upsert_sync_identity_map` v2**:镜像表改为 NAS 本地自增 `id` 主键,
|
||||||
|
业务键 `(video_id, raw_uid)` 唯一;Oracle id 只落 `oracle_id` 列溯源;UPDATE 子句不再改写
|
||||||
|
video_id/raw_uid(只更新 oracle_id/canonical_name/source/updated_at/synced_at)。
|
||||||
|
- **scripts/ddl.sql**:同步更新表定义(`id INT AUTO_INCREMENT PRIMARY KEY` + `oracle_id INT`)。
|
||||||
|
- **现网迁移**:`ADD COLUMN oracle_id` → 回填 `oracle_id=id` → `MODIFY id AUTO_INCREMENT`(无重复对,
|
||||||
|
迁移安全);修复早前测试误改的 3061 行 canonical_name。
|
||||||
|
- **部署**:db_layer.py 用 stdin 管道覆盖 NAS;**清空全部 __pycache__**;杀掉双 gunicorn,单实例重启。
|
||||||
|
|
||||||
|
### 验证(通过)
|
||||||
|
- 09:55 一次补拉成功:`videos+260 events+1456 people+37 model_calls+744 identity_map+285`,游标推进到 09:55:06
|
||||||
|
- `/api/status`:`last_error=null`、`last_sync_at` 有值;手动 `/api/sync/trigger` ok(拉 2videos/2events/1people/6calls/1identity)
|
||||||
|
- `(1073,'汤圆')` 行已自愈:`oracle_id=744`;NAS 旧 `(744→1302,'人物E')` 保留不冲突
|
||||||
|
|
||||||
|
### 待办/注意
|
||||||
|
- 本地 repo 有未提交改动:`fam-core/src/fam_core/db_layer.py`、`scripts/ddl.sql`(等用户拍板提交)
|
||||||
|
- 同类隐患:sync_videos/sync_events/sync_model_calls 同样以 Oracle id 为主键,但 Oracle 侧这些表
|
||||||
|
只插入不 UPDATE、id 稳定,风险低;若将来 Oracle 也重建,需同样改法
|
||||||
|
- NAS 部署后必须清 __pycache__(或 touch .py),否则旧 pyc 被加载(已踩坑)
|
||||||
@@ -27,6 +27,12 @@
|
|||||||
素材 `/opt/fam-edge/gdrive_videos`;logs 目录 ubuntu 可写。
|
素材 `/opt/fam-edge/gdrive_videos`;logs 目录 ubuntu 可写。
|
||||||
**前端亦托管于此机 Caddy :80**(fam-ui SPA + /api 反代回 NAS fam-core :8000 经 frp 隧道);旧机 129.146.203.203 已退役。
|
**前端亦托管于此机 Caddy :80**(fam-ui SPA + /api 反代回 NAS fam-core :8000 经 frp 隧道);旧机 129.146.203.203 已退役。
|
||||||
- 部署:本地 git 提交 push Gitea → tar 管道(NAS 直接解包;Oracle `--strip-components=1` 到临时目录再 cp,避免动 data/venv)。
|
- 部署:本地 git 提交 push Gitea → tar 管道(NAS 直接解包;Oracle `--strip-components=1` 到临时目录再 cp,避免动 data/venv)。
|
||||||
|
- **NAS 部署坑(2026-08-29 踩过)**:拷贝 `.py` 会带旧 mtime,若 `.pyc` 比 `.py` 新,Python 加载陈旧字节码
|
||||||
|
(旧逻辑上线,曾导致 1062 假象)。部署后必须 `find src -name __pycache__ -exec rm -rf {} +` 再重启;
|
||||||
|
`pkill -f` 会匹配 SSH 自身命令导致断连(用 PID 文件或精确 pattern)。
|
||||||
|
- **镜像表键设计(2026-08-29 定稿)**:`sync_identity_map` 不再以 Oracle `person_identity_map.id` 作主键
|
||||||
|
(Oracle 库重建会复用 id),改 NAS 本地自增 `id` + 唯一键 `(video_id, raw_uid)`,Oracle id 落 `oracle_id` 列溯源;
|
||||||
|
同批 upsert 的 UPDATE 子句不改写键列,避免 uq 二次冲突 1062。sync_videos/events/model_calls 同理(风险低未改)。
|
||||||
|
|
||||||
## 提交规范(强制)
|
## 提交规范(强制)
|
||||||
- 格式 `[阶段X.Y子任务号] 子任务名称 - 完成内容简述`;Bug 修复 `fix(模块): 问题简述`
|
- 格式 `[阶段X.Y子任务号] 子任务名称 - 完成内容简述`;Bug 修复 `fix(模块): 问题简述`
|
||||||
|
|||||||
@@ -390,8 +390,12 @@ def upsert_sync_model_calls(rows: List[Dict]) -> int:
|
|||||||
def upsert_sync_identity_map(rows: List[Dict]) -> int:
|
def upsert_sync_identity_map(rows: List[Dict]) -> int:
|
||||||
"""批量 upsert Oracle 传来的 person_identity_map 增量(幂等,重复覆盖)。
|
"""批量 upsert Oracle 传来的 person_identity_map 增量(幂等,重复覆盖)。
|
||||||
|
|
||||||
甲骨文不稳定,提取出的有效数据(含闭集人物识别结果/纠错记录)都要同步到
|
v2 修复 (2026-08-29):不再以 Oracle 的 person_identity_map.id 作为 NAS 主键。
|
||||||
NAS 防丢失——这张表跟 sync_people/sync_model_calls 走一样的镜像模式。
|
该 id 只是 Oracle 端代理主键,一旦 Oracle 库重建/恢复,id 会被复用(实测出现
|
||||||
|
id=744 同时对应 NAS 上的 (1302,'人物E') 与 Oracle 现在的 (1073,'汤圆')),
|
||||||
|
旧写法会先按 id 撞主键、再 UPDATE 成新组合,触发 uq_video_raw_uid 二次冲突
|
||||||
|
报 1062。现改为:NAS 本地自增 id 做主键,业务键 (video_id, raw_uid) 唯一,
|
||||||
|
Oracle 的 id 只落 oracle_id 列作溯源参考。
|
||||||
"""
|
"""
|
||||||
if not rows:
|
if not rows:
|
||||||
return 0
|
return 0
|
||||||
@@ -402,11 +406,10 @@ def upsert_sync_identity_map(rows: List[Dict]) -> int:
|
|||||||
for r in rows:
|
for r in rows:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""INSERT INTO sync_identity_map
|
"""INSERT INTO sync_identity_map
|
||||||
(id, video_id, raw_uid, canonical_name, source, updated_at, synced_at)
|
(oracle_id, video_id, raw_uid, canonical_name, source, updated_at, synced_at)
|
||||||
VALUES (%s,%s,%s,%s,%s,%s, NOW())
|
VALUES (%s,%s,%s,%s,%s,%s, NOW())
|
||||||
ON DUPLICATE KEY UPDATE
|
ON DUPLICATE KEY UPDATE
|
||||||
video_id=VALUES(video_id),
|
oracle_id=VALUES(oracle_id),
|
||||||
raw_uid=VALUES(raw_uid),
|
|
||||||
canonical_name=VALUES(canonical_name),
|
canonical_name=VALUES(canonical_name),
|
||||||
source=VALUES(source),
|
source=VALUES(source),
|
||||||
updated_at=VALUES(updated_at),
|
updated_at=VALUES(updated_at),
|
||||||
|
|||||||
@@ -188,8 +188,12 @@ CREATE TABLE IF NOT EXISTS sync_cursor (
|
|||||||
-- 7.5 人物对应关系表(Oracle person_identity_map 镜像,2026-08-22 新增)
|
-- 7.5 人物对应关系表(Oracle person_identity_map 镜像,2026-08-22 新增)
|
||||||
-- 闭集人物识别:某视频里 Gemini 给的原始 uid 与解析出的规范名(爷爷/爸爸/媳妇/
|
-- 闭集人物识别:某视频里 Gemini 给的原始 uid 与解析出的规范名(爷爷/爸爸/媳妇/
|
||||||
-- 汤圆)之间的映射,供事件时间轴"纠错"按钮定位、追溯识别来源
|
-- 汤圆)之间的映射,供事件时间轴"纠错"按钮定位、追溯识别来源
|
||||||
|
-- v2 (2026-08-29):id 改为 NAS 本地自增主键,业务键 (video_id, raw_uid) 唯一;
|
||||||
|
-- Oracle 的 person_identity_map.id 只落 oracle_id 列溯源。原以 Oracle id 为主键,
|
||||||
|
-- 一旦 Oracle 库重建/复用 id 会引发 uq_video_raw_uid 二次冲突 (1062)。
|
||||||
CREATE TABLE IF NOT EXISTS sync_identity_map (
|
CREATE TABLE IF NOT EXISTS sync_identity_map (
|
||||||
id INT PRIMARY KEY COMMENT 'Oracle person_identity_map.id',
|
id INT AUTO_INCREMENT PRIMARY KEY COMMENT 'NAS 本地自增主键',
|
||||||
|
oracle_id INT COMMENT 'Oracle person_identity_map.id(仅溯源参考)',
|
||||||
video_id INT NOT NULL COMMENT '关联 sync_videos.id',
|
video_id INT NOT NULL COMMENT '关联 sync_videos.id',
|
||||||
raw_uid VARCHAR(100) COMMENT '该视频里 Gemini 给的原始 uid',
|
raw_uid VARCHAR(100) COMMENT '该视频里 Gemini 给的原始 uid',
|
||||||
canonical_name VARCHAR(100) COMMENT '当前生效的规范名',
|
canonical_name VARCHAR(100) COMMENT '当前生效的规范名',
|
||||||
|
|||||||
Reference in New Issue
Block a user