收敛平台会话 epoch 重装语义,避免原生计数与渲染层漂移

- GUI owner 重装与清除改为按 authority 快照重定基准,保持原生计数与渲染层认知一致
- 注释写明 epoch 交接的授权前提,以及写入单调性由渲染层 reserve 与 durable session revision 保证
- 同主体换凭据的重装保持身份代次,在途 operation 的冻结会话继续有效
- 补充 epoch 重装与换主体失效的回归用例,替换原先错误假设原生计数器单调的用例
This commit is contained in:
kdletters
2026-09-16 17:08:26 +08:00
parent e08171fd2e
commit b48293fb1f
@@ -445,33 +445,14 @@ pub(crate) fn replace_platform_session_for_gui_owner(
let mut current = platform_session()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
// 新的 GUI authority epoch 可以替换任意旧会话,但两个计数器只能前进:把写入下限
// 重置到请求值会让旧 epoch 的迟到写入重新变成“更新”。同一主体的重装(续期后的
// Runner 重挂、同一 epoch 的重新对账)保持身份代次,只更新凭据。
let same_identity = current.snapshot.as_ref().is_some_and(|current_snapshot| {
current_snapshot.user_id == snapshot.user_id
&& current_snapshot.api_base_url == snapshot.api_base_url
});
let next_identity_generation = if same_identity {
current
.identity_generation
.max(snapshot.identity_generation)
} else {
current
.identity_generation
.saturating_add(1)
.max(snapshot.identity_generation)
};
let next_revision = current.revision.saturating_add(1).max(snapshot.revision);
current.revision = next_revision;
current.identity_generation = next_identity_generation;
current.snapshot = Some(PlatformSessionSnapshot {
user_id: snapshot.user_id,
access_token: snapshot.access_token,
api_base_url: snapshot.api_base_url,
identity_generation: next_identity_generation,
revision: next_revision,
});
// 这条路径是 GUI authority epoch 的重定性入口:只有 durable claim 的 epoch + session
// revision 与登记完全一致时才会走到这里,新 epoch 可以替换旧进程留下的任意计数器。
// 因此按调用方快照重定基准,让原生计数与渲染层认知严格一致;Runner epoch 的幂等
// 重挂仍走 install_platform_session_checked 的精确相等校验。写入的持续单调性由渲染层
// reservemax(本地 + 1, 原生下限 + 1))和 durable session revision 保证。
current.revision = snapshot.revision;
current.identity_generation = snapshot.identity_generation;
current.snapshot = Some(snapshot);
Ok(())
}
@@ -544,12 +525,9 @@ pub(crate) fn clear_platform_session_for_gui_owner(identity_generation: u64, rev
let mut current = platform_session()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
// 与 replace 同一口径:清除也只能前进,不能让新 epoch 把下限归零
current.revision = current.revision.saturating_add(1).max(revision);
current.identity_generation = current
.identity_generation
.saturating_add(1)
.max(identity_generation);
// 与 replace 同一口径:epoch 交接按调用方快照重定基准,避免原生计数与渲染层认知漂移
current.revision = revision;
current.identity_generation = identity_generation;
current.snapshot = None;
}
@@ -849,7 +827,7 @@ mod tests {
}
#[test]
fn gui_owner_replacement_keeps_counters_monotonic_and_same_subject_identity() {
fn gui_owner_replacement_rebases_to_the_authority_and_only_subject_change_fences() {
let _session = clear_test_platform_session();
replace_platform_session_for_gui_owner("gui-owner-a", "token-a", TEST_ORIGIN, 5, 5)
.expect("install gui owner A");
@@ -857,37 +835,30 @@ mod tests {
assert_eq!(installed.identity_generation, 5);
assert_eq!(installed.revision, 5);
// 同一主体只换凭据:身份代次保持,写入 revision 前进。
// 同一主体只换凭据(续期后 Runner 重挂走的就是这条 replace 路径):身份代次保持、
// 写入 revision 前进,在途 operation 的冻结会话仍然有效。
replace_platform_session_for_gui_owner("gui-owner-a", "token-a2", TEST_ORIGIN, 5, 6)
.expect("refresh gui owner A credential");
let refreshed = current_platform_session().expect("gui owner A refreshed session");
assert_eq!(refreshed.identity_generation, 5);
assert_eq!(refreshed.revision, 6);
validate_frozen_platform_session(&installed)
.expect("same-subject credential replacement keeps the frozen session valid");
// 迟到的旧 epoch 写入不能把写入下限拉回去
replace_platform_session_for_gui_owner("gui-owner-a", "token-a", TEST_ORIGIN, 5, 4)
.expect("stale gui owner write");
let after_stale =
current_platform_session().expect("gui owner A session after stale write");
assert_eq!(
after_stale.identity_generation,
refreshed.identity_generation
);
assert!(after_stale.revision > refreshed.revision);
// 换主体必须推进身份代次,使旧身份的在途 operation 失效。
replace_platform_session_for_gui_owner("gui-owner-b", "token-b", TEST_ORIGIN, 5, 5)
// 换主体必须推进身份代次,旧身份的在途 operation 失败关闭
replace_platform_session_for_gui_owner("gui-owner-b", "token-b", TEST_ORIGIN, 6, 7)
.expect("switch gui owner");
let switched = current_platform_session().expect("gui owner B session");
assert_eq!(switched.user_id, "gui-owner-b");
assert!(switched.identity_generation > after_stale.identity_generation);
assert!(validate_frozen_platform_session(&after_stale).is_err());
assert_eq!(switched.identity_generation, 6);
assert!(validate_frozen_platform_session(&installed).is_err());
assert!(validate_frozen_platform_session(&refreshed).is_err());
// 清除同样只能前进,不能把下限归零
clear_platform_session_for_gui_owner(0, 0);
// epoch 交接后的清除同样按调用方快照重定基准,让原生计数与渲染层认知一致
clear_platform_session_for_gui_owner(7, 8);
let cleared = current_platform_session_write_state();
assert!(cleared.revision > switched.revision);
assert!(cleared.identity_generation > switched.identity_generation);
assert_eq!(cleared.revision, 8);
assert_eq!(cleared.identity_generation, 7);
assert!(current_platform_session().is_none());
}