edit | blame | history | raw

B 站动态主动刷新采集器 V003 双阻断修订

创建人员:dev.developer.project.secondary / infodev-2

文件职责:仅关闭 V002 focused 复审剩余的 F1_R1_OBSERVATION_PARSED_COUNT_DOUBLE_COUNTS_UNPARSED_NODESF2_F4_R1_STALE_FORMAL_LOCK_RECOVERY_SELF_DEADLOCK。V001+V002 其余合同已通过并冻结;冲突处以 V003 为准。

基线:V001=24165/DA597448EFD47361779464B44BF92C8CDE9B4AC1769764EE0AF2301075C86045;V002=22460/EC7E1D887FF930D7DA75F208975E17812E9D299606CD2A79B3E9BF14DE723A54;HOLD/2 回执=msg_20260813130421656_1b883df3

1. F1-R1:互斥 observation node schema

V002 第 2.2 节的 parse_completeparsed_card_countparser_error_count 表述由本节完整替代。

每个 observation exact keys 修订为:

ordinal,observed_at,cursor_before,cursor_after,
visible_node_count,complete_card_count,unparsed_node_count,
cards,unparsed_nodes,limit_hit

1.1 完整卡与未解析节点互斥

  • cards 只允许完整卡,每项 exact keys=position,identifiers,stable_keys,published_at,content_type,source_url;不再存在 parse_complete 字段。
  • identifiers exact keys=dynamic_id,opus_id,bvid,缺项为 JSON null;CLI 从 identifiers+source_url 重算 stable_keys,至少一个。
  • unparsed_nodes 只保存无法形成完整卡的可见节点,每项 exact keys=position,node_fingerprint_sha256,reason_code
  • reason_code exact enum=IDENTITY_MISSING|IDENTITY_CONFLICT|PUBLISHED_AT_MISSING|PUBLISHED_AT_INVALID|CONTENT_TYPE_UNKNOWN|SOURCE_URL_INVALID|NODE_TRUNCATED|PARSER_REJECTED;不含 raw DOM/text/error。
  • node_fingerprint_sha256 是 extractor 对该节点的 bounded sanitized structure(标签名、固定 allowlist 属性名、可见文本长度,不含文本值、URL query、HTML、header或secret)按 contract canonical 算法所得 64 lowercase hex。CLI不能从hash还原内容,但可验证格式、唯一性和 observation canonical hash;extractor/parser身份由已审核hash约束。

互斥算术:

complete_card_count = len(cards)
unparsed_node_count = len(unparsed_nodes)
visible_node_count = complete_card_count + unparsed_node_count

cards.position 与 unparsed_nodes.position 各自唯一,二者不相交,并集必须精确等于整数区间 [0, visible_node_count)。因此一个 DOM node 恰好进入一个集合,不会双计,也不能从 proof 消失。visible_node_count<=50;达到50即 CARD_PER_OBSERVATION_LIMIT,禁止 no-new。

同 observation 的 stable-key component 与 unparsed fingerprint 均不得重复;跨 observation 重复允许用于检测 no-progress,但不增加 unique count。全局 unique_card_count 只数完整 card components;另记录 unique_unparsed_node_count。任意 unparsed_node_count>0unique_unparsed_node_count>0 均使 parse_complete=false、coverage incomplete。

1.2 proof 修订

coverage_proof counts exact keys:

observation_count,total_visible_node_count,total_complete_card_count,
total_unparsed_node_count,unique_card_count,unique_unparsed_node_count

还必须含 ordered observation hashes、ordered complete component hashes 和 ordered unparsed fingerprint hashes。CLI 从 observation 重算所有 counts/hash;输入 evidence 不携带 derived totals。任一 position 缺口/重叠、count drift、完整 card字段缺失、unparsed reason/hash异常均 E_EVIDENCE_SCHEMA,且 no-new token=0。

1.3 聚焦反例

测试至少覆盖:一个 incomplete node 只进入 unparsed;同node同时出现在 cards/unparsed;position重叠/缺口;count大/小漂移;未知reason;fingerprint重复;完整card缺字段;unparsed节点从两集合都消失;49 complete+1 unparsed命中50边界。全部从 public commit入口验证正式 manifests不变且不能 no-new。

2. F2/F4-R1:同 run stale formal-lock 安全接管

V002 “stale lock在存在 pending transaction 时不得接管”的绝对禁止被本节替代。只允许下面同 run、同 owner identity、owner 已证明死亡的恢复接管;其他情况仍 safety stop。

2.1 formal lock schema 与 pending 预绑定

formal lock canonical JSON schema 1 exact keys:

schema_version,task_id,run_id,owner_nonce,transaction_id,
recovery_generation,holder_pid,holder_process_created_at,
lock_created_at
  • holder_process_created_at 必须来自 OS 进程 creation time,不是当前wall clock猜测;PID和creation time组成 holder identity。
  • recovery_generation 初始0,只能每次成功接管加1。
  • pending schema2 的 transaction_identity 新增 formal_lock_claim={claim_state,lock_path,lock_bytes,lock_sha256,lock_record}takeover
  • claim_state=PLANNED|HELD。在创建 formal lock 之前,持 StateLock 先把待创建的 exact lock_record/bytes/hash原子写进 pending 为 PLANNED;随后 CreateNew exact lock并回读,再推进 HELD。这样崩溃后的锁永远可与 pending 预绑定。
  • PLANNED且主lock不存在:相同仍活 holder返回 E_BUSY;holder PID不存在时走同一 takeover状态机创建新generation,不能假装旧claim已持有。HELD但主lock不存在属于外部删除/歧义,除非 takeover phase 和 quarantine实物能按第2.3节唯一恢复,否则 safety stop。

2.2 owner-dead 判定

为使真实 kill 后恢复可达,V001/V002 所称 existing StateLock 必须从“仅靠 CreateNew 文件存在”升级为**稳定 lock file 的内核 advisory exclusive lock**:

  • state_dir/.collector.lock 是预创建/复用的普通非 reparse 稳定文件,内容固定为 1 byte 0x00;Windows 使用标准库 msvcrt.locking 锁定该 1-byte region,测试平台采用等价且进程死亡自动释放的内核锁;不得在持锁时 replace/unlink该文件;
  • 取得内核锁后,才原子写独立 state_dir/.collector.lock.owner.json(pid、process creation time、run_id、acquired_at);退出时删除 exact owner metadata并释放内核锁,稳定 lock file保留;
  • 进程崩溃时OS自动释放内核锁;下一个进程成功取得后可把已死亡且 exact schema 的旧 owner metadata原子隔离到 owned recovery path,再写自己的 metadata。活owner、PID reuse、metadata第三内容/reparse/查询失败均 safety stop;
  • check/move-completed/handoff 全部使用同一实现,公开 E_STATE_LOCKED 语义不变。不得通过删除 .collector.lock 或活 owner metadata“接管”。

测试必须包含活进程互斥、kill 后OS锁释放与旧metadata隔离/恢复,避免 formal lock修好却被原 StateLock 文件永久阻断。

恢复进程必须先独占上述 StateLock,再按顺序验证:

  1. pending为同 task/run/owner_nonce/transaction_id,phase为 TRANSACTION_INTENT|BUSINESS_COMMITTED 或正在进行的 takeover;
  2. formal lock path为配置机械派生路径,lock/pending均普通非reparse,父链无reparse;
  3. 主lock exact bytes/hash与 pending claim完全相等;
  4. lock holder PID 的 OS 状态:
  • PID存在且creation time与record相同:owner alive,E_FORMAL_LOCK_BUSY
  • PID存在但creation time不同:PID reuse,E_FORMAL_LOCK_PID_REUSE,不得接管;
  • PID不存在且OS查询成功:唯一 OWNER_DEAD_PROVEN,允许接管;
  • access denied、查询失败、creation time不可得:E_FORMAL_LOCK_OWNER_UNPROVEN,不得接管;
  1. pending claim的 lock bytes/hash、run/nonce/transaction/generation 任一不符,主lock第三内容、异run、未来time均 E_RECOVERY_AMBIGUOUS

不能只凭lock年龄、deadline或PID数字不存在缓存结果接管;owner-dead检查必须紧邻takeover,并在同一 StateLock临界区内完成。

2.3 crash-safe takeover 状态机

takeover exact phases:

NONE
TAKEOVER_PLANNED
OLD_LOCK_QUARANTINED
NEW_LOCK_HELD

owned quarantine path机械派生:

state_dir/refresh/lock-recovery/<run_id>-g<old_generation>-<old_lock_sha256>.json

接管顺序(全程持 StateLock):

  1. 证明 owner dead;构造 current recovery process identity 和 generation+1 new lock record;冻结 old claim、quarantine path、new claim;pending原子推进 TAKEOVER_PLANNED
  2. 再次复核主lock仍等于 old claim;用 no-overwrite atomic rename 把它移到 exact quarantine;fsync目录、回读 bytes/hash;pending推进 OLD_LOCK_QUARANTINED
  3. CreateNew 主lock为 new claim;回读 bytes/hash;pending 的 formal_lock_claim替换为 new HELD claim,takeover推进 NEW_LOCK_HELD
  4. 执行 V002 双manifest recovery/commit。terminal slot与latest成功后,只有 quarantine仍精确且无任何pending引用时才删除;cleanup失败记 W_STALE_LOCK_QUARANTINE,不得回滚业务终态。

StateLock保证步骤2主lock短暂缺失时其他本工具writer不能进入;所有writer固定先取StateLock,禁止只取formal lock。双recovery进程只有StateLock赢家能接管,输家 E_STATE_LOCKED;不得同时rename/create。

2.4 takeover reopen

  • TAKEOVER_PLANNED:主lock=old且quarantine不存在时继续步骤2;主lock缺失且quarantine=old时推进 OLD_LOCK_QUARANTINED;其他组合歧义停止。
  • OLD_LOCK_QUARANTINED:quarantine必须=old,主lock必须不存在;重新证明记录中的旧owner仍dead,再为当前恢复进程生成新的 generation+1 claim并CreateNew;主lock已存在只能在它精确等于 pending已冻结new claim且holder identity可核时推进,否则停止。
  • NEW_LOCK_HELD:主lock必须精确等于 new claim;若holder是当前进程继续恢复;若holder alive同identity返回busy;若该恢复进程又崩溃,则允许递归执行generation+1 takeover,旧quarantine链逐个保留至terminal cleanup。
  • pending不存在而留下formal lock或quarantine:本任务不自动接管/删除,E_RECOVERY_AMBIGUOUS

任何隔离/创建/回读/pending replace失败都保留当前phase与实物,下次只按矩阵继续;不以unlink主lock作为接管步骤,不覆盖quarantine或新lock。

2.5 测试

真实子进程在 PLANNED前后、lock CreateNew后、HELD后、每个transaction phase、TAKEOVER_PLANNED、rename后、OLD_LOCK_QUARANTINED、new CreateNew后、NEW_LOCK_HELD逐点kill。覆盖 alive owner、PID reuse模拟、OS query denied、异run/nonce/transaction、lock bytes/hash第三内容、reparse、双recovery竞争、quarantine已存在不同hash、generation递增和terminal cleanup失败。断言不会永久自闭锁、不会删除活owner锁、不会覆盖第三内容、不会重复formal append。

3. 冻结项与当前状态

V002 F3 mixed-history 22/16/5/1、F5 UTC-hour unique run/168 slots/durable terminal/latest recovery已关闭,不重开。source-controlled extractor、pending WAL、exact candidates、五终态和V001 preserved contracts继续有效。

状态=V003_TWO_BLOCKER_CLOSURE_PENDING_ORIGINAL_REVIEWER_IMPLEMENTATION_FROZEN

产品 code/test/fixture/formal modification=0;新增Chrome refresh/network retry/download/extension/policy/HKCU/session/formal write=0。V001+V002+V003组合方案只有原reviewer PASS/0后方可实施。