Friend 后端整账号批量迁移(Account Cutover)架构解析:状态机、代际栅栏与故障安全访问控制
Friend 后端整账号批量迁移Account Cutover架构解析状态机、代际栅栏与故障安全访问控制【免费下载链接】FriendAI that sees your screen, listens to your conversations and tells you what to do项目地址: https://gitcode.com/GitHub_Trending/fr/Friend导读本文围绕 backend/utils/account_cutover/ARCHITECTURE.md 展开系统讲解 Friend 后端为整账号批量迁移whole-account cohort cutover设计的六大基础模块合法状态机、控制投影、代际栅栏、HTTP/WS 故障安全fail-closed访问控制、可恢复幂等协调器与隐私安全遥测。读完本文你将掌握该迁移体系的完整状态转换规则、离线队列协议、代际generation递增与栅栏机制以及如何在真实请求路径上强制产品流量 fail-closed从而在自身项目中复现一套可审计、可回滚、默认不迁移任何用户的迁移基础设施。一、模块全景一份职责清晰的迁移基础ARCHITECTURE.md 将整套迁移能力拆成六个彼此独立的模块各司其职模块职责state.py合法状态转换legacy → migrating → {new \| rolled_back_stranded}、new → rolled_back_stranded、stranded → migrating。禁止静默的migrating → legacy进入migrating/new时代际必须递增栅栏fenced状态拒绝离线队列 drain 指令。control.py经过认证的引导投影bootstrap projection代际、各平台最低构建号、客户端动作、离线队列指令。显式的 build0会被保留new状态在destination_backend_bound之前始终保持阻塞。fence.py面向 legacy 产品写入与后台任务跳过决策的代际栅栏。access.pyHTTP/WS 的 fail-closed 强制执行auth / bootstrap / control 路径始终可达。coordinator.py可恢复、幂等的前向迁移清单manifest/检查点checkpoint接缝显式队列成员资格带逐次写令牌轮换的事务化 CAScompleted只能经由complete_to_new写入离线 drain 仅允许在栅栏拉起之前。telemetry.py闭枚举的 Prometheus 计数器与日志未知原因归并到other不记录任何原始用户内容。从源码结构看这套分层刻意把规则state、投影control、写栅栏fence、访问强制access、流程编排coordinator和观测telemetry解耦任何一层都可以独立测试与替换也方便未来接入真正的目标后端。二、合法状态机绝不允许静默回退2.1 状态集合在 models/account_cutover.py 中定义了四个合法状态legacy仍在旧数据平面legacy planemigrating迁移进行中旧平面写入被栅栏new迁移完成账号切换到新平面rolled_back_stranded从new有损回滚后回到旧数据平面但承认新后端写入可能残留stranded且不自动对账。2.2 转换表源码级证据state.py 中的_FORWARD表精确实现了 ARCHITECTURE.md 声明的规则_FORWARD: dict[AccountCutoverState, frozenset[AccountCutoverState]] { AccountCutoverState.legacy: frozenset({AccountCutoverState.migrating}), # Abort during migration goes through rolled_back_stranded (never silent legacy). AccountCutoverState.migrating: frozenset({AccountCutoverState.new, AccountCutoverState.rolled_back_stranded}), AccountCutoverState.new: frozenset({AccountCutoverState.rolled_back_stranded}), AccountCutoverState.rolled_back_stranded: frozenset({AccountCutoverState.migrating}), }要点解读legacy只能进入migrating不允许直接跳到new迁移中途中止只能走rolled_back_stranded有损回滚绝无migrating → legacy的静默回退路径——这是整个体系最核心的不变量new只能回滚到rolled_back_stranded同样回不到legacyrolled_back_stranded可以再次发起迁移begin但不会通过随意刷新offline_queue_instructionnone来假装上一轮栅栏不存在见_QUARANTINE_REQUIRED_STATES的设计意图。2.3 代际递增强制_GENERATION_BUMP_STATES {migrating, new}即进入这两个状态的转换必须让account_generation递增若请求显式携带next_account_generation则使用该值否则自动1若显式值不大于当前代际抛出account_generation_regression错误state.py。进入legacy/rolled_back_stranded时则只要求代际不回退 current。这样所有写方都能用我期望的代际做条件写入任何并发导致的代际漂移都会在状态层被拒绝。2.4 幂等的同态刷新当request.target_state record.state时apply_cutover_transition走_refresh_same_statestate.py不改变状态仅选择性更新offline_queue_instruction、checkpoint_phase、checkpoint_token、manifest_id、stranded_new_data——但更新前仍会通过_assert_offline_instruction_legal校验离线指令合法性保证幂等刷新不会绕过栅栏规则。三、离线队列协议可接受的有限损失ARCHITECTURE.md 定义了简洁的两步协议prepare_offline_drainlegacy/stranded 状态下→drainbegin→migratingquarantine接受未排空项的可接受有限损失。源码层面的语义coordinator.pyprepare_offline_drain只能在legacy或rolled_back_stranded调用一旦进入migrating后再请求 drain会抛出offline_drain_after_fence。原因是服务器侧已经封锁产品变更客户端无法诚实地完成 drain。begin会把offline_queue_instruction原子地置为quarantine——栅栏拉起即隔离未排空的离线项被认定为可接受的有损accepted bounded loss。状态层_assert_offline_instruction_legalstate.py进一步约束migrating/new只接受quarantinerolled_back_stranded上直接drain是非法的必须重新走prepare_offline_drain接缝。这条协议的价值在于它把尽力而为的排空与严格的迁移栅栏明确区分为两个阶段避免客户端在旧平面已不可写的情况下继续上报队列进度。四、控制投影客户端该看到什么4.1 引导端点的落地控制投影由经过认证的只读端点对外暴露GET /v1/account/cutover/controlrouters/account_cutover.py——返回AccountCutoverControl文档缺失时投影为legacy文档损坏时以 503 fail-closed该端点属于_ALWAYS_REACHABLE_PREFIXES白名单见下文访问控制因此在产品流量被栅栏期间依然可达。4.2 构建号解析的兼容细节control.py 的parse_client_build保留了一个关键兼容点移动端历史上把versionbuild拼在X-App-Version中因此解析时会取之后的部分显式的0会被保留而非当作缺失使得运营商可以把 0 作为合法的最低构建号语义。构建号来源优先级为X-App-Build→X-App-Version。4.3 客户端动作决策resolve_client_actioncontrol.py按以下顺序判定若平台在MINIMUM_SUPPORTED_BUILDS中配置了非零下限且客户端构建号缺失或低于下限 →force_upgrade状态为migrating→migration_maintenance状态为new→ 即使目标后端已绑定只要客户端路由尚未接入新平面仍返回migration_maintenance当前两个 bridge 客户端都不会依据 ui/api generation 选择后端因此new账号一律不许进入 legacy 产品壳其余 →none。同时product_traffic_allowedcontrol.py规定只要client_action ! none或状态为migrating/new产品流量即不允许。4.4 默认值代码所有非运营商必填config/account_cutover.py 定义了服务器侧默认值ACCOUNT_CUTOVER_COHORT frozenset()——代码所有、默认空的队列成员集合。begin会拒绝任何非成员 uidcutover_not_enrolled因此这套基础在默认配置下一个用户都不会被迁移MINIMUM_SUPPORTED_BUILDSios / android / macos / windows / linux / web全部为0即默认不触发强制升级运营商需在 bridge 版本发布后显式抬高DEFAULT_UI_GENERATION DEFAULT_API_GENERATION 0legacy 账号始终看到 generation 0ACCOUNT_CUTOVER_SCHEMA_VERSION 1持久化 schema 版本由 config 拥有模型层用assert保持字面量一致。五、代际栅栏写路径与后台任务的守门人fence.py 提供两类语义legacy_writes_allowed_for_statelegacy与rolled_back_stranded允许 legacy 产品写rolled_back_stranded会恢复旧数据平面的读写但仍对外公布新后端数据可能残留。evaluate_write_fence/assert_legacy_product_write_allowed当account_generation 0时调用方必须显式携带期望代际缺失即视为不匹配防止调用方静默继承当前值不匹配抛出account_generation_mismatch。background_job_should_skip_accountmigrating/new状态下排队/后台账号任务应跳过变更类工作。需要强调的是fence.py 的 docstring 明确承认其边界它不是每条 legacy 写路径上的 Firestore CAS 替代品——在途 handler 可能与begin竞态因此对每个数据库写都加 CAS属于无界重写超出本基础 PR 范围。可执行的接缝是worker 入场检查 显式栅栏调用方这正是访问层与协调器所依赖的。六、访问控制HTTP/WS 全面 fail-closed6.1 开关与白名单强制开关为环境变量ACCOUNT_CUTOVER_ENFORCEMENT默认off取值1/true/on/yes任一即开启默认关闭以保证 legacy 账号保持既有主链路延迟与行为access.py。白名单前缀_ALWAYS_REACHABLE_PREFIXESaccess.py/v1/auth、/v1/users/delete-account账号删除独立栅栏、/v1/account/cutover、/v2/desktop/update-policy、/v1/updates、/health、/v1/health。前缀匹配刻意收窄避免误放行。6.2 判定流程evaluate_account_cutover_access未开启强制或命中白名单 → 直接放行读取 Firestore 中的 cutover 文档文档损坏MalformedDocError→ 以account_cutover_state_unavailable503、可重试fail-closed解析X-Account-Generation请求头当账号代际 0 且为变更方法POST/PUT/PATCH/DELETE时必须携带一致的代际否则以account_generation_mismatch拒绝依据客户端头X-App-Platform/X-App-Build/X-App-Version构建控制投影并决策force_upgrade→ HTTP 426migration_maintenance且非rolled_back_stranded→ HTTP 403可重试new状态下的变更方法 →legacy_plane_closed403不可重试。6.3 WebSocket 的特殊处理产品 WebSocket 会话在准入之后会执行采集/变更因此在 enforce_account_cutover_ws_access 中一律按mutatingTrue评估——WS 是长寿命产品面不是安全读。拒绝时以自定义关闭码4006WS_AUTH_CODE_ACCOUNT_CUTOVER断开。6.4 后台任务的入场检查should_skip_background_account_mutationaccess.py在强制开启时为 worker 提供入场判定文档损坏同样 fail-closed跳过migrating/new则跳过变更。它被设计为入场检查而非写事务栅栏。七、协调器可恢复、幂等的前向迁移接缝7.1 检查点阶段机coordinator.py 定义了完整的检查点阶段转换not_started → inventory → offline_queue_fenced → exporting → importing → verifying → cutover_ready → completed任意非终态均可进入paused/failedfailed可回inventory重新开始而终端completed只能由complete_to_new写入直接调用checkpoint(completed)会被拒绝。7.2 三个关键设计点令牌轮换每次持久化写prepare_offline_drain、begin、checkpoint、bind_destination_product_generations、complete_to_new、rollback_lossy都会轮换checkpoint_tokensecrets.token_hex(8)1..128 字符校验配合 CAS 使并发写必有一方失败CAS 持久化database/account_cutover.py 把文档放在users/{uid}/account_cutover/state下cas_set_account_cutover_record同时校验expected_account_generation与expected_checkpoint_token并对require_existing语义区分首写与续写诚实接缝honest seamcheckpoint进入importing/verifying或bind_destination_product_generations推进ui_generation/api_generation或complete_to_new完成迁移之前都要求destination_backend_bound True否则抛出destination_backend_unbound。ARCHITECTURE.md 将其列为明确非目标——本基础不实现目标后端绑定留给后续 PR。7.3 有损回滚与重试安全rollback_lossy将账号置为rolled_back_stranded并默认标记stranded_new_dataTruecoordinator.py。complete_to_new则是重试安全的若已处于new completed且代际匹配直接返回当前记录——避免成功响应丢失后重试被误判为失败。八、遥测闭枚举 无用户内容telemetry.py 暴露两个 Prometheus 计数器omi_account_cutover_transitions_total{from_state, to_state, reason}状态转换事件omi_account_cutover_access_total{state, decision, client_action}产品流量访问决策。所有标签值都经过闭枚举归并telemetry.py未知状态、未知原因、未知决策一律落为other字符串经过清洗仅保留字母数字与._:-并截断到 64 字符任何未列出的调用方文案都不会进入指标。docstring 明确承诺绝不记录原始用户内容、提示词、转录或除 uid-free 枚举与代际之外的任何 PII。指标打点异常也被吞掉telemetry must never fail the path观测不成为迁移路径的故障点。九、复用的既有接缝ARCHITECTURE.md 明确列出了四组被复用的既有机制均可从仓库中直接定位客户端设备头backend/utils/client_device.py 与X-App-Platform/X-App-Version/X-App-Build——控制投影与访问判定都依赖这三类头来解析平台与构建号代际栅栏模式task intelligence 与 memory V3 使用的账号代际栅栏模式X-Account-Generation头 条件写本模块将其抽象为独立工具桌面端强制升级提示DesktopUpdatePolicyManager与GET /v2/desktop/update-policy——force_upgrade客户端动作与桌面端更新策略保持一致且该路径在白名单内始终可达控制投影模式GET /v1/candidates/control——/v1/account/cutover/control沿用了同一套认证只读控制投影的接口形态。此外仓库中还有配套的运维手册 backend/docs/runbooks/account-cohort-cutover.md 与测试证据脚本 backend/scripts/cutover_evidence_readiness.py可继续深入。十、明确非目标与适用前提ARCHITECTURE.md 用一节Non-goals圈定了本模块的边界这是理解整个设计的关键不实现目标后端destination_backend_bound在本基础中始终为false导入器由后续 PR 接入不做双写、反向对账、通用边缘网关默认不迁移任何用户ACCOUNT_CUTOVER_COHORT默认空集begin拒绝非成员不推进ui_generation/api_generation除非先完成bind_destination_product_generations。适用前提该套机制是legacy 侧的迁移基础foundation面向整账号批次cohort迁移场景它解决的是迁移过程中如何保证数据平面不被撕裂、流量如何 fail-closed、失败如何有损回滚且可审计而不是迁移完成后新后端的行为。若你的系统尚无账号代际概念、也没有可复用的客户端设备头与更新策略机制则需先补齐这些前置设施。通过本文可以看到Friend 这套 cutover 基础把迁移做成了一台可以随时暂停、检查点续跑、有损回滚、全程可观测的确定性状态机状态层拒绝一切非法跳转代际层拒绝一切并发漂移访问层拒绝一切栅栏内的流量而协调器则用令牌轮换 CAS 保证并发安全与重试安全。对于任何需要在线数据平台迁移的工程团队这份默认不迁移、显式准入、fail-closed、诚实接缝的设计都是值得直接借鉴的范本。【免费下载链接】FriendAI that sees your screen, listens to your conversations and tells you what to do项目地址: https://gitcode.com/GitHub_Trending/fr/Friend创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考