Merge pull request #34308 from backstage/freben/cached-service-cleanup

Fix cache eviction race in CachedUserInfoService
This commit is contained in:
Ben Lambert
2026-05-19 18:37:27 +02:00
committed by GitHub
3 changed files with 12 additions and 6 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-defaults': patch
---
Fixed a race condition in `CachedUserInfoService` where a failed request could incorrectly evict a newer cache entry for the same token. The error handler now verifies the map entry is still the same promise before deleting it.
@@ -19,6 +19,7 @@ import {
UserInfoService,
} from '@backstage/backend-plugin-api';
import { mockCredentials } from '@backstage/backend-test-utils';
import { createDeferred } from '@backstage/types';
import {
CachedUserInfoService,
UserInfoCacheEntry,
@@ -129,10 +130,8 @@ describe('CachedUserInfoService', () => {
});
it('evicts eagerly so concurrent waiters see the rejection and the next call retries', async () => {
let rejectFirst: (error: Error) => void;
const firstCall = new Promise<BackstageUserInfo>((_resolve, reject) => {
rejectFirst = reject;
});
const firstCall = createDeferred<BackstageUserInfo>();
firstCall.catch(() => {});
const delegate: UserInfoService = {
getUserInfo: jest
@@ -146,7 +145,7 @@ describe('CachedUserInfoService', () => {
const p1 = service.getUserInfo(creds);
const p2 = service.getUserInfo(creds);
rejectFirst!(new Error('boom'));
firstCall.reject(new Error('boom'));
await expect(p1).rejects.toThrow('boom');
await expect(p2).rejects.toThrow('boom');
@@ -73,7 +73,9 @@ export class CachedUserInfoService implements UserInfoService {
}
const promise = this.#delegate.getUserInfo(credentials).catch(error => {
this.#entries.delete(token);
if (this.#entries.get(token)?.promise === promise) {
this.#entries.delete(token);
}
throw error;
});