fix: make update center state consistent
TallyNote release / linux-x64 (push) Successful in 6m53s

This commit is contained in:
Qiufeng
2026-09-05 16:26:34 +08:00
parent ed0b492461
commit efbd0e0d87
5 changed files with 195 additions and 29 deletions
+37 -2
View File
@@ -487,12 +487,47 @@ export function reconcileOrphanedUpdateJobs(database: Database.Database, config:
let reconciled = 0;
const reconciledIds = new Set<string>();
for (const row of rows) {
// A fresh request/state marker means the privileged runner still owns the
// hand-off. Do not expire a staged/apply row while the runner is finishing
// a successful switch and finalization after a service restart.
const matchingFreshRequest = requestMarkerJobId === row.id && requestFresh;
const matchingFreshState = stateMarkerJobId === row.id && stateFresh;
// A staged archive is actionable only while it is strictly newer than the
// release currently serving requests. This can become false when an
// administrator upgrades the host by another path (or another operator
// completes the same release) before returning to this page. Treat the
// archive as an expired terminal task so it cannot keep blocking the
// queue or appear as an "apply" action for the current version.
if (row.status === "staged" && !isNewerVersion(config.appVersion, row.version) && !matchingFreshRequest && !matchingFreshState) {
const changed = database.transaction(() => {
const result = database.prepare(`
UPDATE update_jobs
SET status='failed', error_message=?, completed_at=?, updated_at=?
WHERE id=? AND status='staged'
`).run("暂存更新已过期,当前版本无需再次升级", now, now, row.id);
if (result.changes !== 1) return false;
writeAudit(database, {
requestId: row.requestId || randomUUID(),
actorAdminId: row.adminId,
action: "update.reconciled",
targetType: "update",
targetId: row.id,
outcome: "failure",
before: { status: row.status, operation: row.operation, version: row.version },
after: { status: "failed", version: row.version, reason: "staged_version_not_newer" },
});
return true;
})();
if (changed) {
reconciled += 1;
reconciledIds.add(row.id);
}
continue;
}
// A request that never gets claimed by the root runner must not remain in
// the UI as an endless "queued" task. Once the short hand-off window has
// elapsed and no recovery marker exists, release the queue explicitly;
// a fresh state marker proves that the runner has already claimed it.
const matchingFreshRequest = requestMarkerJobId === row.id && requestFresh;
const matchingFreshState = stateMarkerJobId === row.id && stateFresh;
if (row.status === "queued" && typeof row.updatedAt === "number" && !matchingFreshState && now - row.updatedAt >= QUEUED_UPDATE_TIMEOUT_MS) {
if (matchingFreshRequest) continue;
const changed = database.transaction(() => {