Skip to content

Commit 2d511bc

Browse files
author
andriypolandki
committed
fix add a regression test confirming different admission keys
1 parent 88a5c87 commit 2d511bc

2 files changed

Lines changed: 34 additions & 3 deletions

File tree

src/github/graphql-cache.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ async function graphqlCacheKey(query: string, token: string): Promise<string> {
6666
return `gql:v1:${authHash}:${queryHash}`;
6767
}
6868

69+
function graphqlSingleFlightKey(cacheKey: string, admissionKey?: GitHubRateLimitAdmissionKey): string {
70+
return `${cacheKey}:${admissionKey ?? ""}`;
71+
}
72+
6973
function recordGraphQlCacheMetric(result: "hit" | "miss" | "set" | "coalesced" | "bypassed" | "error", cls: string): void {
7074
incr(GITHUB_GRAPHQL_CACHE_METRIC, { result, class: cls });
7175
}
@@ -152,7 +156,8 @@ export async function fetchCachedGitHubGraphQl(
152156
}
153157
recordGraphQlCacheMetric("miss", cls);
154158

155-
const existing = inFlightGraphQlPosts.get(cacheKey);
159+
const singleFlightKey = graphqlSingleFlightKey(cacheKey, admissionKey);
160+
const existing = inFlightGraphQlPosts.get(singleFlightKey);
156161
if (existing) {
157162
recordGraphQlCacheMetric("coalesced", cls);
158163
const replay = await existing;
@@ -164,8 +169,8 @@ export async function fetchCachedGitHubGraphQl(
164169
(error: unknown) => ({ ok: false as const, error }),
165170
);
166171
const shared = request.then((settled) => (settled.ok ? settled.result.cached : null));
167-
const sharedWithCleanup = shared.finally(() => inFlightGraphQlPosts.delete(cacheKey));
168-
inFlightGraphQlPosts.set(cacheKey, sharedWithCleanup);
172+
const sharedWithCleanup = shared.finally(() => inFlightGraphQlPosts.delete(singleFlightKey));
173+
inFlightGraphQlPosts.set(singleFlightKey, sharedWithCleanup);
169174
const result = await request;
170175
if (!result.ok) throw result.error;
171176
return result.result.response;

test/unit/github-graphql-cache.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,32 @@ describe("fetchCachedGitHubGraphQl", () => {
194194
expect(b.headers.get(GITHUB_RESPONSE_CACHE_REPLAY_HEADER)).toBe("coalesced");
195195
});
196196

197+
it("does not coalesce concurrent cold misses when admission keys differ", async () => {
198+
installMemoryResponseCache();
199+
const keyA = githubRateLimitAdmissionKeyForInstallation(111);
200+
const keyB = githubRateLimitAdmissionKeyForInstallation(222);
201+
let fetches = 0;
202+
vi.stubGlobal("fetch", async () => {
203+
fetches += 1;
204+
await new Promise((resolve) => setTimeout(resolve, 20));
205+
return Response.json(
206+
{ data: { repository: { issues: { totalCount: 1 } } } },
207+
{ headers: { "x-ratelimit-remaining": String(5000 - fetches), "x-ratelimit-reset": "1782802800" } },
208+
);
209+
});
210+
211+
const [a, b] = await Promise.all([
212+
fetchCachedGitHubGraphQl(TOTALS_QUERY, "token-a", keyA),
213+
fetchCachedGitHubGraphQl(TOTALS_QUERY, "token-a", keyB),
214+
]);
215+
216+
expect(fetches).toBe(2);
217+
expect(a.headers.get(GITHUB_RESPONSE_CACHE_REPLAY_HEADER)).toBeNull();
218+
expect(b.headers.get(GITHUB_RESPONSE_CACHE_REPLAY_HEADER)).toBeNull();
219+
expect(latestGitHubRestRateLimitObservation(keyA)).not.toBeNull();
220+
expect(latestGitHubRestRateLimitObservation(keyB)).not.toBeNull();
221+
});
222+
197223
it("bypasses cache for mutable PR detail queries", async () => {
198224
installMemoryResponseCache();
199225
let fetches = 0;

0 commit comments

Comments
 (0)