Skip to content

Commit 51f4297

Browse files
Gkrumbach07Ambient Codeclaude
authored
Bug RHOAIENG-39096: Fix race condition when deleting projects quickly (#546)
## Summary Fixes a race condition that occurred when deleting multiple projects in quick succession. ## Problem When deleting two projects rapidly: 1. First deletion invalidates `projectKeys.lists()` 2. This triggers a refetch of the project list from backend 3. Second deletion happens before the refetch completes 4. State becomes inconsistent, causing "project not found" errors ## Solution Implemented optimistic updates with proper error handling in the `useDeleteProject` hook: - **Optimistic updates**: Projects are immediately removed from the cache before the API call completes - **Cancel in-flight queries**: Prevents race conditions by canceling any ongoing refetches - **Proper rollback**: Restores previous state on errors (except 404s, which are fine) - **Handles both response types**: Works with paginated and legacy list query responses - **Updates totalCount**: Correctly decrements count in paginated responses - **Idempotent deletion**: Silently ignores "not found" errors (project already deleted) ## Testing - ✅ Frontend builds successfully with no TypeScript errors - ✅ Linting passes without issues - ✅ Projects are removed from UI immediately on deletion - ✅ Multiple rapid deletions no longer cause race conditions - ✅ Error handling properly restores state on non-404 failures ## Files Changed - `components/frontend/src/services/queries/use-projects.ts` ## Related Issue https://issues.redhat.com/browse/RHOAIENG-39096 Co-authored-by: Ambient Code <ambient-code@users.noreply.github.com> Co-authored-by: Claude (claude-sonnet-4-5) <noreply@anthropic.com>
1 parent fea1fee commit 51f4297

1 file changed

Lines changed: 57 additions & 3 deletions

File tree

components/frontend/src/services/queries/use-projects.ts

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,17 +98,71 @@ export function useUpdateProject() {
9898

9999
/**
100100
* Hook to delete a project
101+
*
102+
* Implements optimistic updates to prevent race conditions when deleting
103+
* multiple projects in quick succession.
101104
*/
102105
export function useDeleteProject() {
103106
const queryClient = useQueryClient();
104107

105108
return useMutation({
106109
mutationFn: (name: string) => projectsApi.deleteProject(name),
110+
onMutate: async (name) => {
111+
// Cancel any outgoing refetches (so they don't overwrite our optimistic update)
112+
await queryClient.cancelQueries({ queryKey: projectKeys.lists() });
113+
114+
// Snapshot all list queries for rollback on error
115+
const previousQueries = new Map<string, unknown>();
116+
const queries = queryClient.getQueriesData({ queryKey: projectKeys.lists() });
117+
queries.forEach(([queryKey, data]) => {
118+
previousQueries.set(JSON.stringify(queryKey), data);
119+
});
120+
121+
// Optimistically remove the project from all list queries
122+
queryClient.setQueriesData(
123+
{ queryKey: projectKeys.lists() },
124+
(old: unknown) => {
125+
// Handle paginated response
126+
if (old && typeof old === 'object' && 'items' in old) {
127+
const paginatedData = old as { items: Project[]; totalCount?: number };
128+
return {
129+
...paginatedData,
130+
items: paginatedData.items.filter((p) => p.name !== name),
131+
totalCount: paginatedData.totalCount ? paginatedData.totalCount - 1 : undefined,
132+
};
133+
}
134+
// Handle legacy array response
135+
if (Array.isArray(old)) {
136+
return old.filter((p: Project) => p.name !== name);
137+
}
138+
return old;
139+
}
140+
);
141+
142+
// Return context with the snapshots
143+
return { previousQueries };
144+
},
145+
onError: (err, name, context) => {
146+
// Check if this is a "not found" error (which is fine during deletion)
147+
const errorMessage = err instanceof Error ? err.message : String(err);
148+
const isNotFoundError =
149+
errorMessage.toLowerCase().includes('not found') ||
150+
errorMessage.includes('404');
151+
152+
// Only rollback if it's NOT a "not found" error
153+
if (!isNotFoundError && context?.previousQueries) {
154+
// Restore all previous query states
155+
context.previousQueries.forEach((data, keyString) => {
156+
const queryKey = JSON.parse(keyString);
157+
queryClient.setQueryData(queryKey, data);
158+
});
159+
}
160+
// Silently ignore "not found" errors during deletion - the project is already gone
161+
},
107162
onSuccess: (_data, name) => {
108-
// Remove from cache
163+
// Remove the detailed project query from cache
109164
queryClient.removeQueries({ queryKey: projectKeys.detail(name) });
110-
// Invalidate lists
111-
queryClient.invalidateQueries({ queryKey: projectKeys.lists() });
165+
// No need to invalidate lists - already optimistically updated
112166
},
113167
});
114168
}

0 commit comments

Comments
 (0)