Skip to content

perf(desktop): narrow project lock scope for concurrent worktree operations - #1240

Closed
AviPeltz wants to merge 2 commits into
mainfrom
multi-thread-delete
Closed

AviPeltz wants to merge 2 commits into
mainfrom
multi-thread-delete

Conversation

@AviPeltz

@AviPeltz AviPeltz commented Feb 5, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • Narrows the per-project mutex in workspace init and delete to only wrap git worktree add/remove (~5s), instead of holding it for the entire init flow (~2-3 min of network I/O)
  • Allows concurrent workspace creates/deletes for the same project to overlap their network fetches, teardown scripts, and config copies
  • Adds existsSync guards in post-lock phases to handle the case where a concurrent delete removes the worktree between lock release and config copy
  • Re-checks worktree existence inside the lock in deleteWorktree to eliminate TOCTOU window between pre-lock check and locked removal
  • Uses removeWorktreeFromDisk wrapper (from main) for consistent error handling

What changed

workspace-init.ts — The initializeWorkspaceWorktree function is restructured into three phases:

  1. No lock — network I/O (refreshDefaultBranch, branchExistsOnRemote, fetchDefaultBranch)
  2. Lock held — createWorktree / createWorktreeFromExistingBranch + cancellation cleanup
  3. No lock — config copy, DB update, analytics (with existsSync guard for concurrent delete safety)

delete.ts — The deleteWorktree mutation moves teardown before the lock, then re-checks existence inside the lock before calling removeWorktreeFromDisk.

Test plan

  • bun test — 684 pass, 0 fail
  • bun run lint:fix — no issues
  • bun run typecheck — passes (only pre-existing ai-chat missing-module errors)
  • Manual: create two workspaces for the same project simultaneously — second should not block on first's network fetch
  • Manual: delete a workspace while another is being created — delete should proceed without waiting for fetch

Summary by CodeRabbit

  • Bug Fixes
    • Improved reliability of workspace deletion and initialization operations
    • Enhanced handling of edge cases and concurrent operations during workspace setup
    • Better cleanup and cancellation handling to prevent partial or incomplete operations

Resolve conflict in delete.ts: keep narrowed lock scope while
adopting main's removeWorktreeFromDisk wrapper and hard-fail on
teardown errors.
…ations

Worktree creation held a per-project mutex for the entire init (~2-3 min
of network I/O), blocking concurrent deletes and other creates. Narrow
the lock to only wrap git worktree mutations (~5s), allowing network
fetches, teardown scripts, and config copies to run without holding
the lock.
@coderabbitai

coderabbitai Bot commented Feb 5, 2026 •

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

The PR restructures workspace deletion and initialization flows by reordering lock acquisition timing and introducing multi-phase existence checks. The delete flow now executes teardown before acquiring the project lock, then re-verifies and conditionally removes the worktree under lock. The init flow refactors locking to wrap git worktree mutations and adds defensive cancellation checks throughout critical sections.

Changes

Cohort / File(s) Summary
Workspace Deletion Flow
apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts
Reordered locking to run teardown (with branch argument) before acquiring project lock; introduced two-phase existence verification (pre-lock and post-lock) with conditional removal and warning logging for missing worktrees.
Workspace Initialization
apps/desktop/src/lib/trpc/routers/workspaces/utils/workspace-init.ts
Refactored lock acquisition to protect git worktree mutations; added defensive cancellation checks in pre- and post-lock regions; enhanced error handling with conditional cleanup re-acquiring locks when necessary; added post-creation existence checks to detect concurrent deletions.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 The locks are reshuffled with care,
Teardown runs first through the air,
Two checks for existence, so wise,
Cancellation prevented with tries,
Cleanup assured—no worktrees left where!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'perf(desktop): narrow project lock scope for concurrent worktree operations' clearly and specifically describes the main performance improvement—narrowing the project lock scope for concurrent worktree operations.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering the summary of changes, what changed in each file, and a test plan that addresses the key scenarios.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch multi-thread-delete

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
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts`:
- Around line 401-423: Before calling runTeardown, add a filesystem guard that
verifies the worktree.path actually exists on disk (e.g., fs.existsSync or
fs.stat) and skip teardown if the directory is missing so we don't fail deletes;
if the path exists, call runTeardown(project.mainRepoPath, worktree.path,
worktree.branch) as before but ensure any failure is logged with a contextual
prefix (e.g., "[workspaces/delete] Teardown failed: ...") before returning the
error response instead of silently swallowing it; update the block referencing
worktreeExists, runTeardown, project.mainRepoPath, worktree.path, and
worktree.branch accordingly.
🧹 Nitpick comments (2)
apps/desktop/src/lib/trpc/routers/workspaces/utils/workspace-init.ts (1)

419-432: Prefer object parameters for createWorktree to avoid ordering bugs.
Line 431 passes four positional args; consider switching createWorktree to accept an options object (or add a small wrapper) so call sites are self-documenting.
As per coding guidelines: Use object parameters for functions with 2 or more parameters instead of positional arguments.

apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts (1)

405-415: Consider object parameters for worktreeExists / runTeardown calls.
These calls pass multiple positional args (Line 405, Line 411). Switching these utilities to accept an options object would align with the codebase style and reduce ordering mistakes.
As per coding guidelines: Use object parameters for functions with 2 or more parameters instead of positional arguments.

Comment on lines +401 to 423
// Check worktree existence and run teardown before acquiring lock.
// Note: teardown runs user-defined shell commands (.superset/config.json)
// which could include git operations, but not git worktree mutations
// (which is what the project lock protects).
const exists = await worktreeExists(
project.mainRepoPath,
worktree.path,
);

try {
const exists = await worktreeExists(
if (exists) {
const teardownResult = await runTeardown(
project.mainRepoPath,
worktree.path,
worktree.branch,
);

if (exists) {
const teardownResult = await runTeardown(
project.mainRepoPath,
worktree.path,
worktree.branch,
);
if (!teardownResult.success) {
return {
success: false,
error: `Teardown failed: ${teardownResult.error}`,
};
}
if (!teardownResult.success) {
return {
success: false,
error: `Teardown failed: ${teardownResult.error}`,
};
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard teardown when the worktree path is missing to avoid failed deletes.
worktreeExists can return true even if the directory was manually removed. In that case runTeardown will fail (cwd missing) and the delete aborts. Add a filesystem guard and log teardown failures before returning.

🔧 Suggested fix
 const exists = await worktreeExists(
 	project.mainRepoPath,
 	worktree.path,
 );
+const pathExists = existsSync(worktree.path);

-if (exists) {
+if (exists && pathExists) {
 	const teardownResult = await runTeardown(
 		project.mainRepoPath,
 		worktree.path,
 		worktree.branch,
 	);
 	if (!teardownResult.success) {
+		console.error(
+			`[worktree/delete] Teardown failed for ${worktree.path}:`,
+			teardownResult.error,
+		);
 		return {
 			success: false,
 			error: `Teardown failed: ${teardownResult.error}`,
 		};
 	}
+} else if (exists && !pathExists) {
+	console.warn(
+		`[worktree/delete] Skipping teardown; worktree path missing: ${worktree.path}`,
+	);
 }

As per coding guidelines: Never swallow errors silently; at minimum log errors with context before rethrowing or handling them explicitly; Use prefixed console logging with consistent context pattern: [domain/operation] message for entry/exit of significant operations, external API calls, and error conditions.

🤖 Prompt for AI Agents
In `@apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts` around
lines 401 - 423, Before calling runTeardown, add a filesystem guard that
verifies the worktree.path actually exists on disk (e.g., fs.existsSync or
fs.stat) and skip teardown if the directory is missing so we don't fail deletes;
if the path exists, call runTeardown(project.mainRepoPath, worktree.path,
worktree.branch) as before but ensure any failure is logged with a contextual
prefix (e.g., "[workspaces/delete] Teardown failed: ...") before returning the
error response instead of silently swallowing it; update the block referencing
worktreeExists, runTeardown, project.mainRepoPath, worktree.path, and
worktree.branch accordingly.

@Kitenite

Copy link
Copy Markdown
Member

Closing: stale performance PR from Feb 5 with no recent activity.

@Kitenite Kitenite closed this Mar 13, 2026
@Kitenite

Copy link
Copy Markdown
Member

Hey — just a heads up, this was closed as part of an automated stale PR cleanup. If you think this was done in error, feel free to reopen it!

@Kitenite

Copy link
Copy Markdown
Member

Hey — this was closed by an automated cleanup of PRs with major merge conflicts that are 3+ weeks old. If you think this was done incorrectly, please feel free to reopen it. Sorry for any inconvenience!

@Kitenite
Kitenite deleted the multi-thread-delete branch March 15, 2026 16:09
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.

2 participants