Skip to content

fix(updater): ignore stale check results#330

Open
ThunderTr77 wants to merge 5 commits into
TouchAI-org:mainfrom
ThunderTr77:fix/updater-stale-check-race
Open

fix(updater): ignore stale check results#330
ThunderTr77 wants to merge 5 commits into
TouchAI-org:mainfrom
ThunderTr77:fix/updater-stale-check-race

Conversation

@ThunderTr77

@ThunderTr77 ThunderTr77 commented May 31, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes a stale updater race where in-flight update checks could finish after the user changed channels or after a newer check completed.

  • Adds a request version guard to AppUpdateController checks.
  • Invalidates pending check results when the update channel changes.
  • Prevents stale successes from persisting lastCheckedAt and stale failures from restoring old state.
  • Covers out-of-order same-channel checks so older results cannot overwrite newer results.

Related issue or RFC

AI assistance disclosure

  • Tool(s) used: local development assistant
  • Scope of assistance: bug discovery, regression tests, local patch, validation, and PR preparation
  • Human review or rewrite performed: reviewed duplicate search results, diff, and command outputs before submission
  • Architecture or boundary impact: no boundary changes; controller-level stale request guard only

Testing evidence

  • corepack pnpm --dir apps/desktop exec vitest run --configLoader runner tests/services/AppUpdateService/service.test.ts tests/services/AppUpdateService/state.test.ts passed: 2 files, 26 tests
  • corepack pnpm --dir apps/desktop exec eslint src/services/AppUpdateService/index.ts tests/services/AppUpdateService/service.test.ts passed
  • corepack pnpm --dir apps/desktop exec prettier --check src/services/AppUpdateService/index.ts tests/services/AppUpdateService/service.test.ts passed
  • corepack pnpm --dir apps/desktop run test:typecheck passed
  • git diff --check passed
  • Full pnpm test:pr was not run locally; CI is covering the broader matrix.

Risk notes

  • AgentService, runtime, MCP, or schema impact: none.
  • database baseline or migration impact: none.
  • release or packaging impact: low; updater state handling only, no packaging scripts changed.

Screenshots or recordings

No UI change; not applicable.

Checklist

  • The PR title follows Conventional Commits and is valid for squash merge.
  • This PR is either ready for review or explicitly marked as a Draft PR.
  • I did not use [WIP] or similar title prefixes.
  • If AI materially assisted this PR, I disclosed the tools and scope and I personally reviewed every affected change.
  • I can explain the why, what, and how of this change without relying on an AI tool.
  • If this touches AgentService, runtime, MCP, or schema boundaries, there is an accepted RFC.
  • If this changes architecture or adds a new cross-boundary abstraction, there is an accepted RFC.
  • I ran pnpm test:pr for this code PR, or this is a docs-only change.
  • If I changed Rust behavior or tests, I reviewed pnpm test:coverage:rust or relied on CI coverage evidence.
  • If I changed desktop startup/window/search/popup/settings/E2E paths, I ran pnpm test:e2e locally or documented why CI is the first valid proof.
  • I added tests or explained why tests are not appropriate.
  • I updated docs when behavior changed.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @ThunderTr77, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai

coderabbitai Bot commented May 31, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes

    • Fixed update checks so channel changes during in-progress checks no longer allow stale results to overwrite state.
    • Ensured overlapping manual checks only apply the most recent result.
  • Tests

    • Added tests covering race conditions and failure scenarios around channel changes, concurrent checks, and persistence errors.

Walkthrough

AppUpdateController adds per-request versioning so only the latest check for the active channel can commit completion/failure or persist lastCheckedAt. Tests and test helpers are extended to cover channel-switch races, persistence failures, and overlapping checks.

Changes

Stale Update Check Prevention

Layer / File(s) Summary
Request versioning implementation
apps/desktop/src/services/AppUpdateService/index.ts
Adds checkRequestVersion, increments on setChannel and per checkNow call, captures a local requestVersion, adds isCurrentCheckRequest predicate, and short-circuits stale success/error paths to prevent stale state/persistence.
Race and persistence test coverage
apps/desktop/tests/services/AppUpdateService/service.test.ts
Extends createController with updateChannelError/updateLastCheckedAtError injection and adds tests: stale automatic/manual checks after channel switch, setChannel persistence failure during in-flight check, failed clearing of channel timestamp, and overlapping manual checks where newer result wins.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • TouchAI-org/TouchAI#315: Related updater work preventing stale download-completed/failed actions from overwriting state after a channel switch.

Poem

🐰 A numbered hop, each check wears a crest,
Old results get gated, the newest is blessed.
Channels leap sideways, races conclude—
Only the freshest timestamp gets renewed.
Tiny paws persist the tidy truth.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'fix(updater): ignore stale check results' follows Conventional Commits format and accurately summarizes the main change.
Description check ✅ Passed The PR description is comprehensive with all required sections: summary, linked issue, AI disclosure, testing evidence, risk notes, and completed checklist.
Linked Issues check ✅ Passed The changes fully address all objectives from issue #329: request versioning prevents stale checks, channel changes invalidate pending results, and out-of-order checks are handled correctly.
Out of Scope Changes check ✅ Passed All code changes are directly within scope, modifying only AppUpdateController logic and tests to add request versioning and stale result guards.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install timed out. The project may have too many dependencies for the sandbox.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/desktop/src/services/AppUpdateService/index.ts`:
- Around line 113-117: In setChannel (AppUpdateService), don't
increment/invalidate checkRequestVersion until after the channel write succeeds:
call await this.settings.updateAppUpdateChannel(channel) and await
this.settings.updateAppUpdateLastCheckedAt(null) first, and only then increment
this.checkRequestVersion so in-flight checks aren't incorrectly marked stale if
the persistence fails; adjust the order in the setChannel method accordingly
(references: setChannel, this.settings.updateAppUpdateChannel,
this.settings.updateAppUpdateLastCheckedAt, this.checkRequestVersion).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 3d72d99e-8170-4fea-b4a4-31163ff6b6fe

📥 Commits

Reviewing files that changed from the base of the PR and between f0bd14d and 54f738e.

📒 Files selected for processing (2)
  • apps/desktop/src/services/AppUpdateService/index.ts
  • apps/desktop/tests/services/AppUpdateService/service.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Rust Checks
  • GitHub Check: Frontend Quality
  • GitHub Check: Frontend Tests
  • GitHub Check: Desktop E2E Smoke (Windows)
  • GitHub Check: CodeQL (javascript-typescript)
  • GitHub Check: CodeQL (rust)
🔇 Additional comments (2)
apps/desktop/src/services/AppUpdateService/index.ts (1)

59-60: LGTM!

Also applies to: 130-156, 185-187

apps/desktop/tests/services/AppUpdateService/service.test.ts (1)

173-199: LGTM!

Also applies to: 278-306, 308-349

Comment thread apps/desktop/src/services/AppUpdateService/index.ts
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jun 7, 2026
async setChannel(channel: AppUpdateChannel): Promise<void> {
await this.initialize();
await this.settings.updateAppUpdateChannel(channel);
this.checkRequestVersion += 1;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential issue: checkRequestVersion is advanced before updateAppUpdateLastCheckedAt(null) succeeds. If the channel write succeeds but clearing lastCheckedAt fails while a check is in flight, the old check is now treated as stale, while channel-updated is never committed. That can leave the controller stuck in the previous channel's checking state even though the persisted channel has already changed.

Could we cover the updateAppUpdateLastCheckedAt(null) rejection case and either move invalidation until the channel switch is fully committed, or explicitly handle that partial-failure path by committing/rolling back state so the in-flight check does not get stranded?

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
apps/desktop/src/services/AppUpdateService/index.ts (1)

115-119: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Handle setChannel partial persistence failure as an atomic state transition.

If updateAppUpdateChannel(channel) succeeds but updateAppUpdateLastCheckedAt(null) fails, setChannel rejects before channel-updated commit and before version invalidation. That can leave persisted channel/state out of sync and allow an in-flight pre-switch check to still persist a timestamp.

Suggested direction
 async setChannel(channel: AppUpdateChannel): Promise<void> {
     await this.initialize();
     await this.settings.updateAppUpdateChannel(channel);
-    await this.settings.updateAppUpdateLastCheckedAt(null);
-    this.checkRequestVersion += 1;
-    this.commit({ type: 'channel-updated', channel });
+    try {
+        await this.settings.updateAppUpdateLastCheckedAt(null);
+    } catch (error) {
+        // Channel persistence already succeeded; keep controller state aligned.
+        this.checkRequestVersion += 1;
+        this.commit({ type: 'channel-updated', channel });
+        throw error;
+    }
+    this.checkRequestVersion += 1;
+    this.commit({ type: 'channel-updated', channel });
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/desktop/src/services/AppUpdateService/index.ts` around lines 115 - 119,
The setChannel flow can leave persisted state inconsistent if
updateAppUpdateChannel(channel) succeeds but updateAppUpdateLastCheckedAt(null)
fails; make the transition atomic by ensuring both settings updates succeed
before mutating in-memory state or committing. In setChannel, read and store the
previous channel, perform both settings.updateAppUpdateChannel(channel) and
settings.updateAppUpdateLastCheckedAt(null) (or perform last-checked update
first), and only after both succeed increment this.checkRequestVersion and call
this.commit({ type: 'channel-updated', channel }); if the second update fails,
revert by calling settings.updateAppUpdateChannel(previousChannel)
(catch/rethrow errors appropriately) so the persisted channel remains
consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@apps/desktop/src/services/AppUpdateService/index.ts`:
- Around line 115-119: The setChannel flow can leave persisted state
inconsistent if updateAppUpdateChannel(channel) succeeds but
updateAppUpdateLastCheckedAt(null) fails; make the transition atomic by ensuring
both settings updates succeed before mutating in-memory state or committing. In
setChannel, read and store the previous channel, perform both
settings.updateAppUpdateChannel(channel) and
settings.updateAppUpdateLastCheckedAt(null) (or perform last-checked update
first), and only after both succeed increment this.checkRequestVersion and call
this.commit({ type: 'channel-updated', channel }); if the second update fails,
revert by calling settings.updateAppUpdateChannel(previousChannel)
(catch/rethrow errors appropriately) so the persisted channel remains
consistent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: feeca101-181d-499a-9241-f5d7d04593b0

📥 Commits

Reviewing files that changed from the base of the PR and between 9ba57af and 8cb8b3e.

📒 Files selected for processing (2)
  • apps/desktop/src/services/AppUpdateService/index.ts
  • apps/desktop/tests/services/AppUpdateService/service.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Desktop E2E Smoke (Windows)
  • GitHub Check: Frontend Tests
  • GitHub Check: Rust Checks
  • GitHub Check: CodeQL (rust)
🔇 Additional comments (1)
apps/desktop/tests/services/AppUpdateService/service.test.ts (1)

43-53: LGTM!

Also applies to: 75-83

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

This pull request has been inactive for 21 days. Please update it or leave a note if it is still in progress.

@github-actions github-actions Bot added the Stale label Jul 3, 2026
@hiqiancheng

Copy link
Copy Markdown
Collaborator

setChannel() 持久化期间旧 check 仍可能写入结果,需要先失效旧请求。

Old checks can still commit while setChannel() is persisting; invalidate them before persistence.

@github-actions github-actions Bot removed the Stale label Jul 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Updater check results can overwrite newer channel state

2 participants