Files
ft/CodexOutput/upserver/阶段3缓存同步方案与评审-2026-09-22.md

208 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 阶段3:缓存与可靠同步方案及实施裁决
2026-09-23状态更新:完整方案、自审及限定独立复审通过;两条收紧策略已于2026-09-22正式追认;3A、3B均已正式验收。3C详细方案、实施任务书和验收矩阵已落盘,等待其他AI独立方案审查,尚未编码或访问正式服。审查裁决后由主会话实施。正式 API 固定 **http://s101.local:18080/**。
## 1. 前置证据及目标
阶段2已通过42项契约、16项正式服专用key联调、双账号清理、Unity编译及独立终审。见《阶段2独立KV验收-2026-09-22.md》。不重复这些验证,不重新使用已清理的测试key。
沿用G1已选定的“云端基线+待写日志+三方比较”,先做独立模块和测试宿主。目标是本客户端顺序写入不丢待写数据、失败不伪装成功、环境/账号不串档。当前服务器无revision/CAS/幂等请求查询,不承诺跨设备强一致或资产事务恰好执行一次。
源码核对:
- `Core/PlayFabMgr.cs` 当前有五个十key桶、40秒周期,失败只日志,立即同步独立SDK请求;它们是阶段4要替换的现有入口,本批保留。
- `Services/UserService.cs` 当前拥有私有apiSession和登录代际;`CustomServerMgr`有SessionVersion,但transport可替换,不能直接承担KV固定会话传输。
- `Services/PlayerData/PlayerKVClient.cs` 已提供固定注入HttpClient、错误分类、取消和MayHaveCommitted。本批复用,不重新实现HTTP协议。
- 阶段1已盘点remoteStore=false和立即同步调用;阶段3不改FishingBehaviorB、TurntableRoll或业务扣减流程。
## 2. 方案比较与选定路线
| 路线 | 取舍 | 本次判断 |
|---|---|---|
| 独立状态/磁盘层,再串行同步,再接入游戏 | 故障可在纯测试中穷举,独立于Unity和PlayFab;需明确三个小验收门 | 推荐,延续G1及阶段2边界 |
| 直接替换PlayFabMgr内部并同时改登录 | 入口集中,但未验收缓存就改变真实存档,失败难定位 | 阶段4前不采用 |
| 先扩服务端版本条件写/幂等查询 | 可解决客户端无法独自解决的并发/未知结果问题,需服务端项目授权与接口变更 | 本批不改服务器;如要求自动且严格恢复,另立阶段 |
**对旧总体方案补充两条保守裁决,须明确确认:**
1. 写入结果未知后可只读核对,但不能仅凭一次Get成功自动恢复后续写。即使R等于B,也不能证明旧请求稍后不会提交。未知标记跨重启保留,本批无自动清除此标记的公开接口。
2. 缓存主文件损坏时,备份只作为恢复材料,不能自动当作最新存档继续同步。备份可能遗漏最后一次已接受的本地修改。没有有效主文件且有遗留/损坏材料时必须阻止可靠写入,禁止静默建空档;主文件有效时,备份较旧或损坏仅诊断,不因此冻结有效主档。
这会降低异常情况下的自动恢复能力,换取不继续自动覆盖数据。没有声称在无CAS条件下获得跨设备保证。后续若要自动解锁未知结果,需要服务端终态证据或另外评审有限风险策略;人工恢复工具不包含本批。
## 3. 分批实施与文件边界
| 小阶段 | 生产新增文件(均在Services/PlayerData,含meta) | 验收后才推进 |
|---|---|---|
| 3A 状态与持久化 | PlayerKVSyncState.cs、PlayerKVCacheStore.cs、PlayerKVSyncModels.cs | 状态转换、磁盘故障、隔离及重启恢复测试,Unity兼容编译 |
| 3B 固定会话及同步器 | PlayerKVSessionOwner.cs、PlayerKVSyncCoordinator.cs | 串行HTTP、迟到响应、取消/401、立即请求票据、未知结果冻结测试 |
| 3C 联合验收 | 默认不增加生产文件,仅修复评审确认问题 | 3A/3B组合测试、限定正式服fixture、Unity编译、独立review和G3结论 |
测试和证据:`CodexOutput/upserver/KVSyncTests/`、`KVSyncLiveTests/`、阶段3报告/日志。离线测试直接编译生产源码,引用现有Newtonsoft,不新增包或修改asmdef。
本批不改:PlayFabMgr、UserService、CustomServerMgr、RTService、游戏初始化/玩法、配置、SDK或服务器。阶段3的SessionOwner可用独立认证会话验证;接入UserService的具体生命周期合同见第8节,由阶段4另行评审实施。
范围扩展、新依赖或现有生产文件改动必须先补差异裁决;不批量回退其他人的外围改动,不自动提交/发布/打包。
## 4. 身份、快照与存储格式
### 4.1 固定归属
构造时固定规范化API基址(含路径,不仅host/port)、正整数十进制涂游ID和本次会话epoch。URL不含用户信息/query/fragment,末尾统一斜杠;host/scheme规范化,路径保留大小写。环境指纹为完整规范基址UTF-8的SHA256。不能把s101.local与某IP自动视为同服。
路径:调用方传入的专用根目录下 `player-kv/v1/<environmentHash>/<playerId>/state.json`。首次接入游戏时根目录取persistentDataPath,离线和联调宿主只用任务目录。路径各段由摘要和验证后的ID生成,拒绝任意路径输入。旧LocalData/LastUpdateTime PlayerPrefs完全不读取或迁移。
每个命名空间持有独占文件句柄锁,冲突返回Busy,不创建第二写入者;文件存在不等于锁活着,不删除仍被占用的锁来抢占。旧owner关闭并完成请求/磁盘操作后才能释放锁,新owner随后加载;同号重登也遵守。跨设备不受此锁保护。
### 4.2 单快照内容
- formatVersion=1、完整apiBaseUri、playerId、revision/nextSequence(本地单调序号,不冒充服务器版本)。
- baseline:最后确认远端的字典;字典缺键代表不存在,空字符串代表存在空值。
- pending:每key一个最新目标(Set字符串或Delete显式标记)、本地sequence、编辑基线存在标记/值。每项都必须有可靠基线,不能凭null猜测。
- localOnly:remoteStore=false的本地覆盖层;不进入待上传记录。
- activeBatch:在发网络前持久化的不可变批次ID、操作种类、key/sequence/base/目标快照;最多一批。
- unknownBatch:结果未知或重启遗留activeBatch的证据;保留所发版本及后来产生的新pending,不允许后者覆盖前者证据。
- conflictObservations:发生冲突时的远端观察值/存在标记,保留B/L/R;不输出日志。
快照只保存存档必需值,不保存Cookie、token或认证会话。运行日志只记录数量/序号/错误类别,不打印值或完整JSON。
### 4.3 持久化顺序与失败
同步状态变更先对副本计算,序列化完整快照,写同目录临时文件、flush、原子替换主文件,保留上一份完整备份,成功后才发布内存新状态。不存在主文件时使用同目录首次rename;不允许“先删除主文件再移动”。平台不支持原子替换时返回StorageError,不静默退化为不安全覆盖。
已有主文件校验格式、归属、非重复字段、类型、值/key限制、唯一sequence/批次关联和计数范围;不接受未来版本。sequence在pending内部唯一,active可引用同一key的同一sequence,也可保留该key已被新pending替换的旧sequence;这些关联不能误判为重复损坏。使用项目Json.NET,结构严格且沿用阶段2语法兼容边界。磁盘中的数据不可信,不能反序列化任意运行时类型。
主文件缺失且无bak/tmp等遗留证据,才视为无缓存;仍需远端Get成功后才能Ready。主文件损坏或缺失但有遗留材料,返回RecoveryRequired并保留文件;有效备份可供诊断但不自动Ready。主文件有效时,备份较旧或损坏仅诊断、不影响主文件加载;遗留tmp不替代主文件。第3C须验证崩溃发生在各个替换步骤的结果。
任何保存失败:不发布未持久化编辑,进入StorageError,禁止网络写入及新的可靠编辑。若HTTP200之后确认未能落盘,原磁盘activeBatch仍表示结果不确定,重启继续冻结;若替换已经完成后才报错,则磁盘也可能已有完整确认快照,需按下段核对,不断言active必然还在。不能因本次HTTP成功就虚报调用端可靠完成。无无限队列静默丢弃;序号溢出/容量限制均显式失败,保留已有状态。
**本地提交也可能不确定。** 已进入磁盘提交步骤之后报错,不能断言主文件未更新;例如替换成功后报告/后续flush失败,磁盘可能是新revision而内存尚未发布。编辑结果必须携带attemptSequence/目标revision及LocalCommitMayHaveSucceeded,进入StorageError,不清掉本次意图或自动补偿重做。只读重新打开并比较该版本可以核对磁盘结果,但不会自动恢复业务操作。后续调用方不可把StorageError解读成“这次修改一定没有保存”。磁盘确认成功是Accepted的必要条件,任何不确定都不能返回Accepted。
不承诺断电后物理介质绝对耐久性;文件替换/flush的运行平台行为需最终设备验收,桌面故障测试不替代该项。
## 5. 状态与编辑语义
状态:NotLoaded → Loading → Ready;异常为ReadFailed、Conflict、UnknownOutcome、Unauthorized、StorageError、RecoveryRequired、Closed。Busy是打开结果;不覆盖已有owner。主状态之外保留unknown证据与auth失效标志,二者可同时成立;StorageError/Closed等状态不允许抹掉unknown,重登也不能只改一个枚举就恢复写入。
- 加载和初始化期间拒绝编辑/Flush;首次全量Get失败返回ReadFailed,不返回空字典,不初始化默认业务档。
- Ready才开放可靠编辑。正常同步请求等待期间仍可编辑下一版本;锁保护内存与持久化状态,HTTP等待不得持锁。先完成更新落盘,再返回Accepted。
- Update(key,value,remoteStore=true):合法字符串值入pending;保留原编辑基线,替换该key最新目标并递增sequence,同时移除同key的localOnly覆盖。
- Update(...,remoteStore=false):只改localOnly。**不自动撤销此前同key已经产生的remote pending**,也不修改active快照。后续旧写确认不能覆盖本地专用值;下一次显式remote=true才清除此覆盖并创建新远端意图。
- Delete(key):明确删除意图,与空字符串不同,不新增游戏删除入口;与Set共享sequence和批次规则,在同一次持久化中清除同key的localOnly覆盖。这样删除确认后ReadLocal不会继续返回旧本地专用值。
- ReadLocal只返回只读副本或字符串;合成顺序baseline→pending→localOnly。Closed/未Ready/冲突状态只能使用显式诊断快照,不能向游戏伪装已就绪数据。
- 冲突、未知、Unauthorized等阻塞状态不接纳新的可靠玩法修改;重试Get不会丢弃日志或自动恢复Ready。允许可证明未发送的读失败重试和第6节普通冲突重读。
建议接口以结构化结果表达Accepted/Blocked/Validation/StorageError及本地sequence;不要复用阶段2PlayerKVError表示所有存储状态,也不以bool掩盖未知结果。
## 6. 初始合并与恢复
普通未发送pending(无active/unknown)适用B=编辑基线、L=最新目标、R=远端观察:
| 比较 | 决策 |
|---|---|
| R等于L(含删除的不存在) | 视为目标在本次观察时已满足,确认对应sequence,落盘后移除 |
| R等于B | 保留待写,允许正常串行发送 |
| 两者均不等 | Conflict,保留B/L/R,整实例暂停远端写 |
| 基线不可靠 | 除R等于L外均冲突,不猜时序 |
没有pending的key采用本次远端字典;保留localOnly。字符串精确比较,不解析合并业务JSON。整个合并结果一次落盘,不在发现后面的冲突前先发前面的key。重复Get若不再冲突可在完整合并/落盘后恢复Ready;否则保持暂停。
**UnknownOutcome优先于上表:** activeBatch遗留、写失败MayHaveCommitted=true、发出后取消或HTTP成功但确认快照未实际提交,均形成未知批次。允许读取观察并保存对比诊断,但不自动清未知标记、不根据R==B重发、不根据R==L放行后续新值;可能仍存在服务端迟到提交。若连观察都失败,仍保留UnknownOutcome和原始记录。未知结果的解除机制是后续服务端协议/单独恢复评审的前置,不在本批偷偷加“强制继续”按钮。HTTP200且完整确认快照已实际提交、仅提交返回时报错,是本地提交核对问题;重新加载有效确认快照不等于擅自清除持久化unknown,不能将两类不确定混为一谈。
这条保守暂停比旧总体方案更严格。阶段3的“恢复”明确包括未发送日志恢复、正常冲突的再观察以及未知状态的可靠保留,不等于所有网络故障均可自动继续游戏。
## 7. 串行发送及立即同步
### 7.1 批次
所有Get恢复、普通Flush、立即Flush和Delete由同一协调器串行调度,禁止两个变更HTTP同时在途。调用方显式触发FlushAsync;40秒定时触发和Unity暂停事件接线留阶段4,不在独立模块内创建无限后台定时器。
按sequence升序选取最多10个连续同类型待写key,遇到不同类型即截断,不能跳过中间Set而先发后面的Delete。先存activeBatch,再发对应Set或Delete。Set/Delete不同类型不能合成一个原子请求。单次Flush捕获调用时水位,处理其范围后返回;新编辑留后续,避免持续编辑导致Flush永不完成。水位内尚未发送的版本若被更高版本覆盖,原版本计为Superseded,不临时把新版本纳入本次Flush。Flush汇总明确Confirmed/Superseded/Blocked数量,不把队列暂时无水位内项理解为所有票据成功。
发送v1等待时编辑v2:v1成功并且会话仍有效,则先把已确认baseline设为v1;pending若仍是v1的sequence则移除;若已经是v2则保留v2并将其基线推进到v1。确认状态和activeBatch清除同一次持久化;失败保持pending及发出证据。
### 7.2 结果
- 200且持久化确认成功:本批确认成功;不修改更高sequence的目标。
- 未发送的取消/校验失败:无服务端未知写,可保留pending;参数失败不可不停自动重试。
- 已持久化active但尚未送入transport时取消:只有同一进程掌握确定未发送证据才能安全撤销active;崩溃恢复看不到该证据时保守未知。
- MayHaveCommitted=true:进入UnknownOutcome,无自动写重试;401/403同时记录认证失效,先禁止新请求,后续新认证不能清未知。
- Get失败不丢baseline/pending,也不变成空云端。
- 不修改已完成阶段2的保守HTTP错误分类;例如明确HTTP400目前仍可能标记MayHaveCommitted,此阶段不擅自推定服务器未写。
### 7.3 立即请求
独立模块提供带sequence的写入票据及等待结果Confirmed/Superseded/Blocked/Unknown/Cancelled。立即请求进入同一队列,不抢占在途写;队列中尚未发送v1已被同key v2替换时,v1票据返回Superseded,不因v2成功就称v1成功。
票据的等待取消只取消这个等待者,不撤销已持久化pending,不取消同批其他票据或已安排的Flush;等待者收到Cancelled不代表目标未发送/未保存。编辑准入前取消可以拒绝编辑,准入并提交后不能用取消回滚。协调器HTTP仅绑定owner生命周期token;已经接纳的Flush按其水位完成或进入阻塞,调用者取消等待不会改变共享任务。owner失效/Close才取消共享传输。此规则与阶段2单请求CancellationToken的含义不同,应通过显式WaitForTicketAsync/Flush等待包装表达,不把外部等待token直接传给共享HttpClient请求。
true兼容适配留阶段4:只有该票据确切目标获远端成功且确认落盘,或普通未发送pending在Get中确认等于目标,才返回true。未知/取消/被替换均不能true;失败不能触发玩法补回再扣减。阶段3不改现有SyncDataImmidiate签名或调用方。
## 8. 会话所有权与生产桥接合同
### 8.1 阶段3可执行独立Owner
由认证成功的固定S101AccountSession、调用方epoch/生命周期token、IsCurrent判断和缓存根目录构造owner。创建时必须在调用方会话切换的同一串行临界区捕获api/playerId/CookieContainer,验证已认证且归属一致;不开放运行期切换账号的setter。
owner新建自己的HttpClientHandler(该CookieContainer、UseCookies=true、AllowAutoRedirect=false)和HttpClient/PlayerKVClient,拥有它们及缓存锁/协调器;不Dispose调用方的认证session,不清共享Cookie,不使用可替换的CustomServerPost。CookieContainer本身可变,因此约定该捕获session不再Login重用;重新认证必须关闭旧owner并新建。
每个await前后核对epoch、IsCurrent和lifetime,旧响应不发布事件或影响新owner;已发送写晚到只在旧命名空间保留未知记录,不能转到新Cookie重发。Unauthorized只返回给捕获的owner,不能无归属发布全局重登事件。
CloseAsync幂等:先同步置Closed禁止新编辑/请求并取消,在途操作保留已持久化active;等待所有登记操作退出;释放transport和缓存锁,最后完成Close。退出不隐式Flush。取消token不是服务端回滚证明。若传输不响应取消,保持关闭等待/锁占用,禁止通过超时强行释放锁后启动同名新owner。
**原子准入与关闭次序:** Closed检查、操作登记、获取owner租约使用同一个短临界区;登记成功后才能访问状态/磁盘或进入调度,任何操作退出都在finally归还租约。Close也用这个门原子置Closed并禁止以后登记,因此不存在“检查通过→Close看到零操作并释放锁→旧操作才登记”的窗口。状态/磁盘锁和准入门内均不得await;Close退出锁后才Cancel(取消回调可能同步执行)、终结尚未确认的票据等待为Cancelled/Unknown、等待实际操作退出。被动票据等待者不计入Close必须等待的在途工作;已接纳的协调器worker/磁盘操作才计入。最后只释放一次transport/文件锁。Close不等待自身,不在持有state/dispatch锁时等待worker,避免相互等待。
### 8.2 阶段4接入点(本批只设计)
- UserService在API登录成功、代际仍有效时创建一次owner,固定认证session及账号/环境。开放的是窄的存档服务引用,不暴露全局任意可换Cookie传输。
- Logout/重新API认证的第一步使旧owner失效并发起CloseAsync,随后才清认证session。同步Logout不能阻塞Unity主线程等待任务;将关闭任务交给新登录流程await,在新owner拿同名缓存锁前必须完成。
- 当前阶段3不会在现有UserService加入这段代码;阶段4先完整审查登入→加载→初始化→Done、未授权、退出以及立即玩法调用,再开放具体文件。
- SessionOwner拥有独立传输,但共享认证Cookie的外部生命周期仍需宿主遵守;阶段3测试要故意失效session/换号验证不会借用新Cookie。SignalR连接继续归现有RTService,不迁到KV层。
## 9. 分阶段验收用例
### G3A:先状态和磁盘,完全离线
1. API含路径/账号隔离、非法ID/路径拒绝、同命名空间独占锁、同号关闭再打开。
2. 全量Get失败无Ready;合法空字典与缺数据、缺键、空字符串、显式Delete分别处理。
3. B/L/R四种规则;多key含冲突时没有部分自动发送;JSON字符串只精确比较。
4. 51/101个key无丢失;同key多版本、localOnly与既有pending/在途确认组合;序号溢出。
5. 暂存v1→编辑v2→确认v1,v2保留且基线正确;已被替换票据不误报成功。
6. 写前持久化active;在每个磁盘步骤注入失败;主文件/bak/tmp缺失与损坏、错误身份/版本/重复序号均有明确阻塞结果。
7. active/unknown跨重启保留、读回B/L/其他值均不能自动解除未知;已有记录不得被较旧备份静默覆盖。
8. 保存失败不发布编辑/确认成功,不发后续网络;证据不打印值。独立review与Unity兼容编译。
9. 原子替换成功后注入异常:返回LocalCommitMayHaveSucceeded及attemptSequence,重开看到新revision不会被当作未曾保存;禁止自动补偿/重复业务编辑。
### G3B:可控传输及会话,默认无正式服写
1. 使用实际PlayerKVClient配HttpMessageHandler替身;Set/Delete/立即票据串行,批10、51+key、发送中继续编辑、水位退出。
2. 写前取消、发出后取消、超时/401/403/400/500、成功后磁盘失败,逐项核对unknown/阻塞/日志保留。
3. 101旧成功/旧401→切102、新同号epoch;旧owner不影响新状态,绝不重新拿新Cookie发送旧值。
4. Close幂等、并发Close/Flush/Update、transport迟到成功和不响应取消、锁仅等待收尾后释放。
5. 未知结果观察无自动写;Get恢复失败不初始化默认档;票据只有精确确认且落盘才Confirmed。
6. 在检查Closed与准入登记之间设置可控调度点:Close不得提前释放;Cancel同步回调重入不能死锁;被动票据等待先终结,不阻塞Close。
7. 一张票据取消等待、同批另一票据继续确认;取消Flush等待仍不撤销已持久化目标;owner关闭才取消传输,未知证据保持。
### G3C:组合与限定联调
离线跨实例/进程重启测试完成后,才用101/102新唯一前缀key验证串行写、重新打开缓存/重登、隔离和清理;fixture适配限制所有请求只能覆盖预先登记测试key,禁止把全量真实业务存档放入测试缓存再执行写入。磁盘测试缓存只在CodexOutput任务目录,退出释放锁。
不得在正式服人为制造未知写或服务端延迟;不确定结果用可控替身复现,避免因“恢复测试”产生未终止的迟到写破坏清理。测试finally精确删除、双账号读回不存在,并记录失败清理项;不覆盖阶段2原证据。
最终Unity编译、独立review、源码hash及本批报告通过才能标G3。独立测试宿主不是Unity业务存档运行验收;平台原子替换/进程终止与真实SDK/设备仍按最终包单列。
## 10. 自审裁决及执行检查表
主会话自审:文件边界、HTTP复用、双层生命周期、磁盘先行、存在标记、同key多版本、localOnly、未知写和备份风险均已明确;无新增服务器契约/依赖,没有把“正常登录可用”当作“游戏KV可切换”。
- [x] 复用阶段2验收;读取既有设计、当前PlayFabMgr/账号/HTTP源代码。
- [x] 比较实施路线,明确3A/3B/3C和不改游戏入口边界。
- [x] 写出数据模型、状态、磁盘顺序、串行算法、会话桥接合同和验收矩阵。
- [x] 限定独立方案评审及R1–R4问题闭环;见《阶段3缓存同步独立方案审查-2026-09-22.md》。
- [x] 用户在方案评审后明确“继续”,两条收紧策略为用户后续确认;2026-09-22正式追认(不重新评审),3A已实施并验收。
- [x] 3A实现及独立验收通过,见《阶段3A缓存状态与磁盘验收-2026-09-22.md》。
- [x] 3B已实施并正式验收;见《阶段3B文档终审-2026-09-22.md》。
- [ ] 3C详细方案待独立审查和主会话裁决;通过后才实施。
当前阶段结论:阶段2、3A、3B完成;3C详细方案已形成,等待独立审查,所有3C矩阵项仍是计划而非测试结果。3C含测试宿主脱敏诊断,其后另立人工恢复专项;阶段4/5外围/6仍未放行。每个小步骤更新开发日志、TODO和交接入口。