Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 100 additions & 7 deletions packages/plugins/scm-github/src/graphql-batch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,25 @@ const etagCache: ETagCache = {
reviewComments: new LRUCache(MAX_REVIEW_COMMENTS_ETAGS),
};

const TRANSIENT_NETWORK_PATTERNS = [
"error connecting to api.github.com",
"check your internet connection",
"could not resolve host",
"failed to connect to api.github.com",
"network is unreachable",
"connection timed out",
"connection reset",
"ssl",
"tls",
"certificate",
"dial tcp",
] as const;

const MAX_TRANSIENT_WARNING_KEYS =
(MAX_PR_LIST_ETAGS + MAX_COMMIT_STATUS_ETAGS + MAX_REVIEW_COMMENTS_ETAGS) * 2;

const transientNetworkWarningKeys = new Set<string>();

/**
* Result of checking if PR data has changed via ETag guards.
*/
Expand All @@ -114,6 +133,7 @@ export function clearETagCache(): void {
etagCache.prList.clear();
etagCache.commitStatus.clear();
etagCache.reviewComments.clear();
transientNetworkWarningKeys.clear();
}

/**
Expand Down Expand Up @@ -411,6 +431,58 @@ function extractErrorOutput(err: unknown): string | null {
return combined.length > 0 ? combined : null;
}

function isTransientNetworkError(message: string): boolean {
const normalized = message.toLowerCase();
return TRANSIENT_NETWORK_PATTERNS.some((pattern) => normalized.includes(pattern));
}

function pruneTransientNetworkWarningKeys(): void {
if (transientNetworkWarningKeys.size < MAX_TRANSIENT_WARNING_KEYS) {
return;
}

const activeWarningKeys = new Set<string>();
for (const repoKey of etagCache.prList.keys()) activeWarningKeys.add(`ETag Guard 1:${repoKey}`);
for (const commitKey of etagCache.commitStatus.keys())
activeWarningKeys.add(`ETag Guard 2:${commitKey}`);
for (const reviewKey of etagCache.reviewComments.keys())
activeWarningKeys.add(`ETag Guard 3:${reviewKey}`);

for (const key of transientNetworkWarningKeys) {
if (!activeWarningKeys.has(key)) {
transientNetworkWarningKeys.delete(key);
}
}

while (transientNetworkWarningKeys.size >= MAX_TRANSIENT_WARNING_KEYS) {
const oldestKey = transientNetworkWarningKeys.values().next().value;
if (!oldestKey) break;
transientNetworkWarningKeys.delete(oldestKey);
}
}

function warnETagGuardFailure(
observer: BatchObserver | undefined,
guard: string,
key: string,
message: string,
): void {
const warningKey = `${guard}:${key}`;
if (isTransientNetworkError(message)) {
pruneTransientNetworkWarningKeys();
if (transientNetworkWarningKeys.has(warningKey)) {
return;
}
transientNetworkWarningKeys.add(warningKey);
}

observer?.log("warn", `[${guard}] ${message}`);
}

function markETagGuardSuccess(guard: string, key: string): void {
transientNetworkWarningKeys.delete(`${guard}:${key}`);
}

/**
* Extract ETag from HTTP response output.
* Used on both 200 and 304 paths — RFC 7232 allows servers to rotate
Expand Down Expand Up @@ -457,6 +529,7 @@ async function checkPRListETag(
// Re-read ETag on 304 — RFC 7232 allows rotated validators
const rotatedETag = extractETag(output);
if (rotatedETag) setPRListETag(owner, repo, rotatedETag);
markETagGuardSuccess("ETag Guard 1", repoKey);
return false;
}

Expand All @@ -466,6 +539,7 @@ async function checkPRListETag(
setPRListETag(owner, repo, newETag);
}

markETagGuardSuccess("ETag Guard 1", repoKey);
// PR list changed - cost: 1 REST point
return true;
} catch (err) {
Expand All @@ -474,6 +548,7 @@ async function checkPRListETag(
if (output && is304(output)) {
const rotatedETag = extractETag(output);
if (rotatedETag) setPRListETag(owner, repo, rotatedETag);
markETagGuardSuccess("ETag Guard 1", repoKey);
return false;
}
Comment thread
ChiragArora31 marked this conversation as resolved.

Expand All @@ -482,9 +557,15 @@ async function checkPRListETag(
// that don't populate stdout on non-zero exit). Use is304() anchored to the HTTP status
// line to avoid false positives from URL paths like "pulls/304/comments".
if (is304(errorMsg)) {
markETagGuardSuccess("ETag Guard 1", repoKey);
return false;
}
observer?.log("warn", `[ETag Guard 1] PR list check failed for ${repoKey}: ${errorMsg}`);
warnETagGuardFailure(
observer,
"ETag Guard 1",
repoKey,
`PR list check failed for ${repoKey}: ${errorMsg}`,
);
return true; // Assume changed to be safe
}
}
Expand Down Expand Up @@ -528,6 +609,7 @@ async function checkCommitStatusETag(
if (is304(output)) {
const rotatedETag = extractETag(output);
if (rotatedETag) setCommitStatusETag(owner, repo, sha, rotatedETag);
markETagGuardSuccess("ETag Guard 2", commitKey);
return false;
}

Expand All @@ -537,6 +619,7 @@ async function checkCommitStatusETag(
setCommitStatusETag(owner, repo, sha, newETag);
}

markETagGuardSuccess("ETag Guard 2", commitKey);
// CI status changed - cost: 1 REST point
return true;
} catch (err) {
Expand All @@ -545,16 +628,20 @@ async function checkCommitStatusETag(
if (output && is304(output)) {
const rotatedETag = extractETag(output);
if (rotatedETag) setCommitStatusETag(owner, repo, sha, rotatedETag);
markETagGuardSuccess("ETag Guard 2", commitKey);
return false;
}

const errorMsg = err instanceof Error ? err.message : String(err);
if (is304(errorMsg)) {
markETagGuardSuccess("ETag Guard 2", commitKey);
return false;
}
observer?.log(
"warn",
`[ETag Guard 2] Commit status check failed for ${commitKey}: ${errorMsg}`,
warnETagGuardFailure(
observer,
"ETag Guard 2",
commitKey,
`Commit status check failed for ${commitKey}: ${errorMsg}`,
);
return true; // Assume changed to be safe
}
Expand Down Expand Up @@ -597,6 +684,7 @@ export async function checkReviewCommentsETag(
if (is304(output)) {
const rotatedETag = extractETag(output);
if (rotatedETag) etagCache.reviewComments.set(cacheKey, rotatedETag);
markETagGuardSuccess("ETag Guard 3", cacheKey);
return false;
}

Expand All @@ -605,22 +693,27 @@ export async function checkReviewCommentsETag(
etagCache.reviewComments.set(cacheKey, newETag);
}

markETagGuardSuccess("ETag Guard 3", cacheKey);
return true;
} catch (err) {
const output = extractErrorOutput(err);
if (output && is304(output)) {
const rotatedETag = extractETag(output);
if (rotatedETag) etagCache.reviewComments.set(cacheKey, rotatedETag);
markETagGuardSuccess("ETag Guard 3", cacheKey);
return false;
}

const errorMsg = err instanceof Error ? err.message : String(err);
if (is304(errorMsg)) {
markETagGuardSuccess("ETag Guard 3", cacheKey);
return false;
}
observer?.log(
"warn",
`[ETag Guard 3] Review comments check failed for ${cacheKey}: ${errorMsg}`,
warnETagGuardFailure(
observer,
"ETag Guard 3",
cacheKey,
`Review comments check failed for ${cacheKey}: ${errorMsg}`,
);
return true; // Assume changed to be safe
}
Expand Down
Loading
Loading