Skip to content

Commit 47a6692

Browse files
joshspicerCopilot
andauthored
fix: reduce /enabled endpoint traffic and surface unexpected status codes (#308879)
* fix: cache CCA disabled results with 5-min TTL to reduce /enabled traffic The checkCCAEnabled() method previously only cached enabled=true results (introduced in 19541d7). For the majority of users whose repos have CCA disabled, every provideChatSessionProviderOptions() call bypassed the cache and hit the jobs/:owner/:repo/enabled CAPI endpoint unconditionally. With growing adoption, this became significant upstream traffic. Fix: cache all /enabled results. enabled=true keeps the 30-min TTL. enabled=false/undefined uses a new 5-min TTL (CCA_DISABLED_CACHE_TTL_MS), short enough that users who just enabled CCA won't wait long, but long enough to dramatically reduce repeated calls. To support the shorter TTL for disabled entries without changing the enabled TTL, TtlCache.set() now accepts an optional per-entry ttlMs override that takes precedence over the cache-wide TTL. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: surface unexpected /enabled status codes (e.g. 429) in telemetry Previously, isCCAEnabled's default case returned { enabled: undefined } with no statusCode, swallowing 429 rate-limit and 5xx responses. Changes: - Widen CCAEnabledResult.statusCode from 401|403|422 to number so unexpected codes can be propagated - Return statusCode: response.status in isCCAEnabled's default case - Add sendTelemetryErrorEvent('copilot.codingAgent.CCAIsEnabledUnexpectedStatus') in checkCCAEnabled for any status code outside {401, 403, 422}, with isRateLimited flag for quick 429 filtering in dashboards Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor: hoist knownStatusCodes to constant and add GDPR annotation - Extract CCA_KNOWN_STATUS_CODES to file-level Set to avoid re-creating it on every call and centralize the list of handled status codes - Add __GDPR__ comment block for the new copilot.codingAgent.CCAIsEnabledUnexpectedStatus telemetry error event Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 1804dfd commit 47a6692

5 files changed

Lines changed: 57 additions & 15 deletions

File tree

extensions/copilot/src/extension/chatSessions/common/test/ttlCache.spec.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,23 @@ describe('TtlCache', () => {
8989
vi.advanceTimersByTime(1000);
9090
expect(cache.has('key')).toBe(false);
9191
});
92+
93+
it('supports per-entry TTL override', () => {
94+
const cache = new TtlCache<string>(5000);
95+
cache.set('default', 'value1');
96+
cache.set('short', 'value2', 1000);
97+
98+
vi.advanceTimersByTime(999);
99+
expect(cache.get('default')).toBe('value1');
100+
expect(cache.get('short')).toBe('value2');
101+
102+
vi.advanceTimersByTime(1);
103+
expect(cache.get('default')).toBe('value1');
104+
expect(cache.get('short')).toBeUndefined(); // expired at 1000ms
105+
106+
vi.advanceTimersByTime(4000);
107+
expect(cache.get('default')).toBeUndefined(); // expired at 5000ms
108+
});
92109
});
93110

94111
describe('SingleSlotTtlCache', () => {

extensions/copilot/src/extension/chatSessions/common/ttlCache.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
* Entries are evicted lazily on access when their TTL has elapsed.
99
*/
1010
export class TtlCache<V> {
11-
private readonly _entries = new Map<string, { value: V; timestamp: number }>();
11+
private readonly _entries = new Map<string, { value: V; timestamp: number; ttlMs?: number }>();
1212

1313
/**
1414
* @param _ttlMs The time-to-live in milliseconds for cache entries.
@@ -23,7 +23,8 @@ export class TtlCache<V> {
2323
if (!entry) {
2424
return undefined;
2525
}
26-
if (Date.now() - entry.timestamp >= this._ttlMs) {
26+
const ttl = entry.ttlMs ?? this._ttlMs;
27+
if (Date.now() - entry.timestamp >= ttl) {
2728
this._entries.delete(key);
2829
return undefined;
2930
}
@@ -32,9 +33,10 @@ export class TtlCache<V> {
3233

3334
/**
3435
* Stores a value in the cache with the current timestamp.
36+
* @param ttlMs Optional per-entry TTL override in milliseconds. If not provided, the cache-wide TTL is used.
3537
*/
36-
set(key: string, value: V): void {
37-
this._entries.set(key, { value, timestamp: Date.now() });
38+
set(key: string, value: V, ttlMs?: number): void {
39+
this._entries.set(key, { value, timestamp: Date.now(), ttlMs });
3840
}
3941

4042
/**

extensions/copilot/src/extension/chatSessions/vscode-node/copilotCloudSessionsProvider.ts

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -158,8 +158,13 @@ const CLEAR_CACHES_COMMAND_ID = 'github.copilot.chat.cloudSessions.clearCaches';
158158
const USER_SELECTED_REPOS_KEY = 'userSelectedRepositories';
159159
const USER_SELECTED_REPOS_EXPIRY_MS = 7 * 24 * 60 * 60 * 1000; // 1 week
160160

161-
// TTL for caching /enabled responses (only caches enabled=true; disabled results always re-fetch)
161+
// TTL for caching /enabled responses when CCA is enabled
162162
const CCA_ENABLED_CACHE_TTL_MS = 30 * 60 * 1_000; // 30 minutes
163+
// Shorter TTL for caching /enabled responses when CCA is disabled or undetermined,
164+
// so users aren't stuck but we don't hammer the endpoint on every options query
165+
const CCA_DISABLED_CACHE_TTL_MS = 5 * 60 * 1_000; // 5 minutes
166+
// Status codes that are expected/handled by isCCAEnabled; anything else is unexpected
167+
const CCA_KNOWN_STATUS_CODES = new Set([401, 403, 422]);
163168
// TTL for caching session provider options (custom agents, models, partner agents, etc.)
164169
const OPTIONS_CACHE_TTL_MS = 15 * 60 * 1_000; // 15 minutes
165170

@@ -273,7 +278,7 @@ export class CopilotCloudSessionsProvider extends Disposable implements vscode.C
273278
private readonly gitOperationsManager = new CopilotCloudGitOperationsManager(this.logService, this._gitService, this._gitExtensionService);
274279

275280
// TTL cache for CCA enabled status per repository (key: "owner/repo")
276-
// Only caches enabled=true results; disabled results always re-fetch to avoid stuck states
281+
// enabled=true cached for 30 min; disabled/undetermined cached for 5 min to reduce traffic
277282
private _ccaEnabledCache = new TtlCache<CCAEnabledResult>(CCA_ENABLED_CACHE_TTL_MS);
278283

279284
// Single-slot TTL cache for the full session provider options result (custom agents, models, partner agents, etc.)
@@ -600,8 +605,8 @@ export class CopilotCloudSessionsProvider extends Disposable implements vscode.C
600605
/**
601606
* Checks if the Copilot cloud agent is enabled for a repository.
602607
* Results are cached with a TTL: enabled=true results are cached for {@link CCA_ENABLED_CACHE_TTL_MS},
603-
* while enabled=false results are never cached (always re-fetched) so users who just
604-
* enabled CCA are not stuck in a disabled state.
608+
* while disabled/undetermined results are cached for a shorter {@link CCA_DISABLED_CACHE_TTL_MS}
609+
* to balance responsiveness with reducing endpoint traffic.
605610
* @param owner Repository owner
606611
* @param repo Repository name
607612
* @returns CCAEnabledResult with enabled status and optional status code
@@ -610,19 +615,20 @@ export class CopilotCloudSessionsProvider extends Disposable implements vscode.C
610615
const cacheKey = `${owner}/${repo}`;
611616

612617
const cached = this._ccaEnabledCache.get(cacheKey);
613-
if (cached !== undefined && cached.enabled === true) {
618+
if (cached !== undefined) {
614619
this.logService.trace(`copilotCloudSessionsProvider#checkCCAEnabled: using cached CCA enabled status for ${owner}/${repo}: ${cached.enabled}`);
615620
return cached;
616621
}
617622

618623
const result = await this._octoKitService.isCCAEnabled(owner, repo, {});
619624

620-
// Only cache enabled=true results with a TTL; disabled results should always re-fetch
625+
// Cache all results: enabled=true uses the default 30 min TTL,
626+
// disabled/undetermined uses a shorter 5 min TTL so users who just
627+
// enabled CCA aren't stuck for too long
621628
if (result.enabled === true) {
622629
this._ccaEnabledCache.set(cacheKey, result);
623630
} else {
624-
// Remove any stale positive cache entry
625-
this._ccaEnabledCache.delete(cacheKey);
631+
this._ccaEnabledCache.set(cacheKey, result, CCA_DISABLED_CACHE_TTL_MS);
626632
}
627633

628634
this.telemetry.sendTelemetryEvent('copilot.codingAgent.CCAIsEnabledCheck', { microsoft: true, github: false }, {
@@ -631,6 +637,22 @@ export class CopilotCloudSessionsProvider extends Disposable implements vscode.C
631637
cacheHit: 'false',
632638
});
633639

640+
// Track unexpected status codes (429 rate-limit, 5xx, etc.) as errors so they surface in dashboards
641+
if (result.statusCode !== undefined && !CCA_KNOWN_STATUS_CODES.has(result.statusCode)) {
642+
/* __GDPR__
643+
"copilot.codingAgent.CCAIsEnabledUnexpectedStatus" : {
644+
"owner": "joshspicer",
645+
"comment": "Fired when the /enabled endpoint returns an unexpected HTTP status code (e.g. 429 rate-limit or 5xx).",
646+
"statusCode": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "The unexpected HTTP status code returned by the /enabled endpoint." },
647+
"isRateLimited": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "comment": "True if the status code is 429 (rate limited)." }
648+
}
649+
*/
650+
this.telemetry.sendTelemetryErrorEvent('copilot.codingAgent.CCAIsEnabledUnexpectedStatus', { microsoft: true, github: false }, {
651+
statusCode: String(result.statusCode),
652+
isRateLimited: String(result.statusCode === 429),
653+
});
654+
}
655+
634656
this.logService.trace(`copilotCloudSessionsProvider#checkCCAEnabled: fetched CCA enabled status for ${owner}/${repo}: ${result.enabled}`);
635657
return result;
636658
}

extensions/copilot/src/platform/github/common/githubService.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,10 @@ export interface CCAEnabledResult {
9797
*/
9898
enabled: boolean | undefined;
9999
/**
100-
* The HTTP status code when the cloud agent is disabled (401, 403, or 422).
100+
* The HTTP status code from the /enabled response. Known values: 401, 403, 422.
101+
* Unexpected values (e.g. 429 rate-limit, 5xx) are also propagated for telemetry.
101102
*/
102-
statusCode?: 401 | 403 | 422;
103+
statusCode?: number;
103104
}
104105

105106
export interface IOctoKitSessionInfo {

extensions/copilot/src/platform/github/common/octoKitServiceImpl.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -530,7 +530,7 @@ export class OctoKitService extends BaseOctoKitService implements IOctoKitServic
530530
return { enabled: false, statusCode: 422 };
531531
default:
532532
this._logService.trace(`Unexpected status code for isCCAEnabled: ${response.status}`);
533-
return { enabled: undefined };
533+
return { enabled: undefined, statusCode: response.status };
534534
}
535535
} catch (e) {
536536
this._logService.error(`Error checking if CCA is enabled: ${e}`);

0 commit comments

Comments
 (0)