refactor(runtime): capture convergence tickets instead of hand-checked generations

execute 现在返回 RuntimeExecutionOutcome(成功与否 + 本次占用的并发流
代际),mode 与 rules 路径的 24 处手写代际 guard 收拢为 ConvergenceTicket
的 convergenceIsCurrent 检查;两处 flowGeneration + 1 手算随之消失。
行为逐字节保持:mode 路径的 mode 代际仍在派生前捕获、preparation 分量
在派生后推进;rules 路径四个代际事实全查不变。
This commit is contained in:
Qiu Yuzhou
2026-10-03 10:30:47 +08:00
parent 3aae39799a
commit dc35e70798
5 changed files with 133 additions and 74 deletions
@@ -228,9 +228,8 @@ extension ProxyRuntimeController {
guard preparation == runtimePreparationGeneration else { return false }
let document = contract.document
lastDocument = document
guard await execute(.run(document), document: document, preparedContract: contract) else {
return false
}
let execution = await execute(.run(document), document: document, preparedContract: contract)
guard execution.succeeded else { return false }
guard preparation == runtimePreparationGeneration else { return false }
state = .starting
return await presentLaunchHealth(
@@ -276,15 +275,26 @@ extension ProxyRuntimeController {
// MARK: - 计划动作执行
extension ProxyRuntimeController {
/// 按计划顺序执行动作;返回 false 表示中途失败、状态已呈现(后续动作与
/// 健康探测都不应继续)。
/// 把「运行定义档」意图推到运行时:计划动作 → 顺序执行。返回全部动作
/// 是否成功与本次占用的并发流代际。
func execute(
_ intent: RuntimeIntent, document: SslocalRuntimeDocument?,
preparedContract: PreparedRuntimeContract? = nil
) async -> Bool {
) async -> RuntimeExecutionOutcome {
cancelFirewallObservation()
flowGeneration += 1
let generation = flowGeneration
let flow = flowGeneration
let succeeded = await performPlannedActions(
intent, document: document, preparedContract: preparedContract, flow: flow)
return RuntimeExecutionOutcome(flow: flow, succeeded: succeeded)
}
/// 按计划顺序执行动作;返回 false 表示中途失败、状态已呈现(后续动作与
/// 健康探测都不应继续)。
private func performPlannedActions(
_ intent: RuntimeIntent, document: SslocalRuntimeDocument?,
preparedContract: PreparedRuntimeContract?, flow: Int
) async -> Bool {
let preparation = runtimePreparationGeneration
let contract: PreparedRuntimeContract?
if case .run(let desired) = intent {
@@ -307,11 +317,11 @@ extension ProxyRuntimeController {
RuntimeLog.emit(.contractUnchanged)
}
for action in actions {
guard generation == flowGeneration, preparation == runtimePreparationGeneration,
guard flow == flowGeneration, preparation == runtimePreparationGeneration,
await perform(action, preparedContract: contract)
else { return false }
}
return generation == flowGeneration && preparation == runtimePreparationGeneration
return flow == flowGeneration && preparation == runtimePreparationGeneration
}
/// 执行单个动作;返回 false 表示应终止后续动作(状态已呈现)。
@@ -0,0 +1,63 @@
import Foundation
/// 一次计划执行的结果:动作是否全部按序成功,以及本次执行占用的并发流
/// 代际。占用代际供收敛票据推进 flow 分量,取代调用方对 execute 内部
/// 「恰好自增一次」时序的依赖(原 `flowGeneration + 1` 手算)。
struct RuntimeExecutionOutcome {
/// 本次执行占用的 `flowGeneration`。
let flow: Int
/// 全部计划动作是否按序成功。
let succeeded: Bool
}
/// 收敛票据:运行时收敛操作的代际快照。入口捕获三个并发代际,之后每步
/// 推进用它判定「从入口到现在,并发流 / 模式切换 / 运行时派生是否前进了」;
/// 被更新提交或新意图取代的旧收敛不得再变更运行时状态或写契约。
///
/// 各路径校验的代际子集不同:mode 切换的 mode 代际在派生前捕获、派生
/// 代际在派生后推进(派生自身会推进它);rules 部署四个事实全查(含
/// 会话内 agent 意图);目录 / 启动路径只查派生代际。
struct ConvergenceTicket: Equatable {
/// 票据时效校验覆盖的代际事实子集。
struct Checks: OptionSet {
let rawValue: Int
/// 并发流代际:任何后续 execute 都使票据失效。
static let flow = Checks(rawValue: 1 << 0)
/// 模式切换代际。
static let mode = Checks(rawValue: 1 << 1)
/// 运行时文档派生代际。
static let preparation = Checks(rawValue: 1 << 2)
/// 会话内 agent 意图仍开启。
static let agentEnabled = Checks(rawValue: 1 << 3)
}
/// 并发流代际;每次 execute 之后由调用方以实际占用值推进。
var flow: Int
/// 模式切换代际;捕获后不再变化。
let mode: Int
/// 运行时文档派生代际;每次派生之后由调用方以当前值推进。
var preparation: Int
}
extension ProxyRuntimeController {
/// 以当前代际捕获收敛票据。
func convergenceTicket() -> ConvergenceTicket {
ConvergenceTicket(
flow: flowGeneration, mode: modeChangeGeneration,
preparation: runtimePreparationGeneration)
}
/// 票据是否仍代表当前收敛:`checking` 列出的代际事实全部未前进。
func convergenceIsCurrent(
_ ticket: ConvergenceTicket, checking: ConvergenceTicket.Checks
) -> Bool {
var current = true
if checking.contains(.flow) { current = current && ticket.flow == flowGeneration }
if checking.contains(.mode) { current = current && ticket.mode == modeChangeGeneration }
if checking.contains(.preparation) {
current = current && ticket.preparation == runtimePreparationGeneration
}
if checking.contains(.agentEnabled) { current = current && settings.agentEnabled }
return current
}
}
@@ -100,43 +100,37 @@ extension ProxyRuntimeController {
return .runtimeUnchanged
}
let preparation = runtimePreparationGeneration
modeChangeGeneration += 1
let generation = modeChangeGeneration
let runtimeGeneration = flowGeneration + 1
var ticket = convergenceTicket()
let checks: ConvergenceTicket.Checks = [.flow, .mode, .preparation, .agentEnabled]
let contract = try? PreparedRuntimeContract(nextDocument)
lastDocument = nextDocument
state = .starting
guard await execute(.run(nextDocument), document: nextDocument, preparedContract: contract)
else {
guard ruleDeploymentIsCurrent(preparation, generation, runtimeGeneration)
else {
return .runtimeChanged(rulesRestored: false)
}
return await restoreCustomRules(previousRules, snapshot: snapshot)
}
guard ruleDeploymentIsCurrent(preparation, generation, runtimeGeneration)
else {
let execution = await execute(
.run(nextDocument), document: nextDocument, preparedContract: contract)
ticket.flow = execution.flow
guard convergenceIsCurrent(ticket, checking: checks) else {
return .runtimeChanged(rulesRestored: false)
}
guard execution.succeeded else {
return await restoreCustomRules(previousRules, snapshot: snapshot, ticket: ticket)
}
let healthy = await presentLaunchHealth(
nextDocument,
requiresReceipt: true,
convergeProxyOnSuccess: false,
preserveProxyOnFailure: true, preparedContract: contract)
guard ruleDeploymentIsCurrent(preparation, generation, runtimeGeneration)
else {
guard convergenceIsCurrent(ticket, checking: checks) else {
return .runtimeChanged(rulesRestored: false)
}
guard healthy else {
return await restoreCustomRules(previousRules, snapshot: snapshot)
return await restoreCustomRules(previousRules, snapshot: snapshot, ticket: ticket)
}
lastDocument = nextDocument
await convergeSystemProxy()
guard ruleDeploymentIsCurrent(preparation, generation, runtimeGeneration)
else {
guard convergenceIsCurrent(ticket, checking: checks) else {
return .runtimeChanged(rulesRestored: false)
}
return .applied
@@ -154,13 +148,6 @@ extension ProxyRuntimeController {
}
}
private func ruleDeploymentIsCurrent(
_ preparation: Int, _ mode: Int, _ runtime: Int
) -> Bool {
preparation == runtimePreparationGeneration && mode == modeChangeGeneration
&& runtime == flowGeneration && settings.agentEnabled
}
/// 保存前校验(issue #66 AC2):固定本地冲突与重复整批拒绝并返回可解释原因。
/// 同行动覆盖和相反行动遮蔽不阻止保存;编译时按当前骨架解释完整集合。
func validateCustomRulesForPersistence(_ rules: [CustomRule]) -> (
@@ -177,8 +164,10 @@ extension ProxyRuntimeController {
private func restoreCustomRules(
_ previousRules: CustomRuleDocument,
snapshot: ModeTransitionSnapshotForRules
snapshot: ModeTransitionSnapshotForRules,
ticket: ConvergenceTicket
) async -> CustomRuleUpdateOutcome {
var ticket = ticket
var failures: [String] = []
do {
try ruleDocuments.save(previousRules)
@@ -195,19 +184,17 @@ extension ProxyRuntimeController {
guard settings.agentEnabled else {
return .runtimeChanged(rulesRestored: rulesRestored)
}
let modeGeneration = modeChangeGeneration
let preparation = runtimePreparationGeneration
let runtimeGeneration = flowGeneration + 1
let checks: ConvergenceTicket.Checks = [.flow, .mode, .preparation, .agentEnabled]
let contract = try? PreparedRuntimeContract(previousDocument)
lastDocument = previousDocument
state = .starting
let executed = await execute(
let execution = await execute(
.run(previousDocument), document: previousDocument, preparedContract: contract)
guard ruleDeploymentIsCurrent(preparation, modeGeneration, runtimeGeneration)
else {
ticket.flow = execution.flow
guard convergenceIsCurrent(ticket, checking: checks) else {
return .runtimeChanged(rulesRestored: rulesRestored)
}
guard executed else {
guard execution.succeeded else {
failures.append(String(describing: state))
await holdSystemProxyIntent()
return .recoveryFailed(detail: failures.joined(separator: "; "), rulesRestored: rulesRestored)
@@ -215,8 +202,7 @@ extension ProxyRuntimeController {
let healthy = await presentLaunchHealth(
previousDocument, requiresReceipt: true, convergeProxyOnSuccess: false,
preparedContract: contract)
guard ruleDeploymentIsCurrent(preparation, modeGeneration, runtimeGeneration)
else {
guard convergenceIsCurrent(ticket, checking: checks) else {
return .runtimeChanged(rulesRestored: rulesRestored)
}
if !healthy {
@@ -51,7 +51,7 @@ extension ProxyRuntimeController {
settings = next
proxyMode = mode
modeChangeGeneration += 1
let generation = modeChangeGeneration
var ticket = convergenceTicket()
guard settings.agentEnabled, state != .off,
let currentDocument = lastDocument ?? runtimeFileStore.loadDocument()
@@ -68,10 +68,13 @@ extension ProxyRuntimeController {
} catch {
// 快照缺失/损坏:不静默退化,恢复旧模式与旧子选项。
RuntimeLog.emit(.runtimePersistFailed(detail: String(describing: error)))
ticket.preparation = runtimePreparationGeneration
await restoreModeTransition(
snapshot: snapshot.resolvingDocument(currentDocument), generation: generation)
snapshot: snapshot.resolvingDocument(currentDocument), ticket: ticket)
return
}
// 派生自身推进了派生代际:票据以派生后的当前值推进 preparation 分量。
ticket.preparation = runtimePreparationGeneration
guard nextDocument.aclRuntime != currentDocument.aclRuntime else {
guard settings.systemProxyEnabled else { return }
state = .starting
@@ -80,8 +83,7 @@ extension ProxyRuntimeController {
}
await deployModeTransition(
nextDocument, snapshot: snapshot.resolvingDocument(currentDocument),
generation: generation, preparation: runtimePreparationGeneration)
nextDocument, snapshot: snapshot.resolvingDocument(currentDocument), ticket: ticket)
}
enum RulePreparationError: Error { case superseded }
@@ -167,23 +169,24 @@ extension ProxyRuntimeController {
private func deployModeTransition(
_ document: SslocalRuntimeDocument,
snapshot: ModeTransitionSnapshot,
generation: Int, preparation: Int
ticket: ConvergenceTicket
) async {
guard generation == modeChangeGeneration, preparation == runtimePreparationGeneration else {
guard convergenceIsCurrent(ticket, checking: [.mode, .preparation]) else {
return
}
let contract = try? PreparedRuntimeContract(document)
lastDocument = document
state = .starting
guard await execute(.run(document), document: document, preparedContract: contract) else {
guard generation == modeChangeGeneration, preparation == runtimePreparationGeneration else {
let execution = await execute(.run(document), document: document, preparedContract: contract)
guard execution.succeeded else {
guard convergenceIsCurrent(ticket, checking: [.mode, .preparation]) else {
return
}
await restoreModeTransition(snapshot: snapshot, generation: generation)
await restoreModeTransition(snapshot: snapshot, ticket: ticket)
return
}
guard generation == modeChangeGeneration, preparation == runtimePreparationGeneration else {
guard convergenceIsCurrent(ticket, checking: [.mode, .preparation]) else {
return
}
let healthy = await presentLaunchHealth(
@@ -191,11 +194,11 @@ extension ProxyRuntimeController {
requiresReceipt: true,
convergeProxyOnSuccess: false,
preserveProxyOnFailure: true, preparedContract: contract)
guard generation == modeChangeGeneration, preparation == runtimePreparationGeneration else {
guard convergenceIsCurrent(ticket, checking: [.mode, .preparation]) else {
return
}
guard healthy else {
await restoreModeTransition(snapshot: snapshot, generation: generation)
await restoreModeTransition(snapshot: snapshot, ticket: ticket)
return
}
@@ -205,9 +208,8 @@ extension ProxyRuntimeController {
private func restoreModeTransition(
snapshot: ModeTransitionSnapshot,
generation: Int
ticket: ConvergenceTicket
) async {
let preparation = runtimePreparationGeneration
// 两个调用点都经 resolvingDocument 补齐文档;防御性解包失败即无事可做。
guard let previousDocument = snapshot.document else { return }
let restoredSettings = restoredSettings(for: snapshot)
@@ -218,7 +220,7 @@ extension ProxyRuntimeController {
persistenceFailed = true
RuntimeLog.emit(.runtimePersistFailed(detail: String(describing: error)))
}
guard generation == modeChangeGeneration, preparation == runtimePreparationGeneration else {
guard convergenceIsCurrent(ticket, checking: [.mode, .preparation]) else {
return
}
settings = restoredSettings
@@ -228,10 +230,10 @@ extension ProxyRuntimeController {
let contract = try? PreparedRuntimeContract(previousDocument)
guard
await restoreRuntimeDocument(
previousDocument, generation: generation, preparedContract: contract)
previousDocument, ticket: ticket, preparedContract: contract)
else { return }
guard generation == modeChangeGeneration, preparation == runtimePreparationGeneration else {
guard convergenceIsCurrent(ticket, checking: [.mode, .preparation]) else {
return
}
state = .starting
@@ -239,7 +241,7 @@ extension ProxyRuntimeController {
previousDocument,
requiresReceipt: true,
convergeProxyOnSuccess: false, preparedContract: contract)
guard generation == modeChangeGeneration, preparation == runtimePreparationGeneration else {
guard convergenceIsCurrent(ticket, checking: [.mode, .preparation]) else {
return
}
guard restored else {
@@ -267,7 +269,7 @@ extension ProxyRuntimeController {
/// 代理并返回 false(健康检查由调用方继续)。
private func restoreRuntimeDocument(
_ previousDocument: SslocalRuntimeDocument,
generation: Int, preparedContract: PreparedRuntimeContract?
ticket: ConvergenceTicket, preparedContract: PreparedRuntimeContract?
) async -> Bool {
let expectedDigest = preparedContract?.sha256 ?? previousDocument.deploymentSHA256
let currentDocument = runtimeFileStore.loadDocument()
@@ -298,7 +300,7 @@ extension ProxyRuntimeController {
if !previousInstanceIsRunning {
guard
await relaunchPreviousInstance(
wrapper: wrapper, previousDocument: previousDocument, generation: generation,
wrapper: wrapper, previousDocument: previousDocument, ticket: ticket,
preparedContract: preparedContract)
else { return false }
}
@@ -310,9 +312,8 @@ extension ProxyRuntimeController {
private func relaunchPreviousInstance(
wrapper: WrapperProcessState,
previousDocument: SslocalRuntimeDocument,
generation: Int, preparedContract: PreparedRuntimeContract?
ticket: ConvergenceTicket, preparedContract: PreparedRuntimeContract?
) async -> Bool {
let preparation = runtimePreparationGeneration
switch wrapper {
case .running(let pid):
guard sendSignal(pid, SIGUSR1) == 0 else {
@@ -321,12 +322,11 @@ extension ProxyRuntimeController {
return false
}
case .notRunning:
guard
await execute(
.run(previousDocument), document: previousDocument,
preparedContract: preparedContract)
else {
guard generation == modeChangeGeneration, preparation == runtimePreparationGeneration else {
let execution = await execute(
.run(previousDocument), document: previousDocument,
preparedContract: preparedContract)
guard execution.succeeded else {
guard convergenceIsCurrent(ticket, checking: [.mode, .preparation]) else {
return false
}
await holdSystemProxyIntent()
@@ -26,7 +26,7 @@ extension ProxyRuntimeControllerTests {
await fulfillment(of: [started], timeout: 3)
try FileManager.default.removeItem(at: runtimeStore.pidFileURL)
let completed = await oldOperation.value
XCTAssertFalse(completed)
XCTAssertFalse(completed.succeeded)
XCTAssertEqual(runtimeStore.loadDocument(), previous, "Stale stop must not delete the contract")
loader.release.signal()
await newMode.value