Skip to content

Commit d07f79a

Browse files
authored
fix(amazonq): add cancel support to loading developer profiles (#940)
Problem There can be some latency while loading developer profiles, caused by either a poor internet connection, a sluggish backend, or both. Clients need a way to cancel the request for loading profiles. Solution Wire in support for the Cancellation Token. This change is being done in tandem with VS Toolkit change aws/aws-toolkit-visual-studio-staging#2443
1 parent 461957d commit d07f79a

File tree

5 files changed

+49
-10
lines changed

5 files changed

+49
-10
lines changed

server/aws-lsp-codewhisperer/src/language-server/configuration/qConfigurationServer.test.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
} from './qConfigurationServer'
99
import { TestFeatures } from '@aws/language-server-runtimes/testing'
1010
import { CodeWhispererServiceToken } from '../../shared/codeWhispererService'
11-
import { InitializeParams, Server } from '@aws/language-server-runtimes/server-interface'
11+
import { CancellationTokenSource, InitializeParams, Server } from '@aws/language-server-runtimes/server-interface'
1212
import { AmazonQTokenServiceManager } from '../../shared/amazonQServiceManager/AmazonQTokenServiceManager'
1313
import { setCredentialsForAmazonQTokenServiceManagerFactory } from '../../shared/testUtils'
1414
import { Q_CONFIGURATION_SECTION } from '../../shared/constants'
@@ -96,6 +96,7 @@ describe('ServerConfigurationProvider', () => {
9696
let codeWhispererService: StubbedInstance<CodeWhispererServiceToken>
9797
let testFeatures: TestFeatures
9898
let listAvailableProfilesHandlerSpy: sinon.SinonSpy
99+
let tokenSource: CancellationTokenSource
99100

100101
const setCredentials = setCredentialsForAmazonQTokenServiceManagerFactory(() => testFeatures)
101102

@@ -119,6 +120,7 @@ describe('ServerConfigurationProvider', () => {
119120
}
120121

121122
beforeEach(() => {
123+
tokenSource = new CancellationTokenSource()
122124
codeWhispererService = stubInterface<CodeWhispererServiceToken>()
123125
codeWhispererService.listAvailableCustomizations.resolves({
124126
customizations: [],
@@ -152,14 +154,14 @@ describe('ServerConfigurationProvider', () => {
152154
it(`does not use listAvailableProfiles handler when developer profiles is disabled`, async () => {
153155
setupServerConfigurationProvider(false)
154156

155-
const result = await serverConfigurationProvider.listAvailableProfiles()
157+
const result = await serverConfigurationProvider.listAvailableProfiles(tokenSource.token)
156158

157159
sinon.assert.notCalled(listAvailableProfilesHandlerSpy)
158160
assert.deepStrictEqual(result, [])
159161
})
160162

161163
it(`uses listAvailableProfiles handler when developer profiles is enabled`, async () => {
162-
await serverConfigurationProvider.listAvailableProfiles()
164+
await serverConfigurationProvider.listAvailableProfiles(tokenSource.token)
163165

164166
sinon.assert.calledOnce(listAvailableProfilesHandlerSpy)
165167
})

server/aws-lsp-codewhisperer/src/language-server/configuration/qConfigurationServer.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,11 @@ export const QConfigurationServerToken =
8080
case Q_CONFIGURATION_SECTION:
8181
;[customizations, developerProfiles] = await Promise.all([
8282
serverConfigurationProvider.listAvailableCustomizations(),
83-
serverConfigurationProvider.listAvailableProfiles(),
83+
serverConfigurationProvider.listAvailableProfiles(token),
8484
])
8585

86+
throwIfCancelled(token)
87+
8688
return amazonQServiceManager.getEnableDeveloperProfileSupport()
8789
? { customizations, developerProfiles }
8890
: { customizations }
@@ -91,7 +93,9 @@ export const QConfigurationServerToken =
9193

9294
return customizations
9395
case Q_DEVELOPER_PROFILES_CONFIGURATION_SECTION:
94-
developerProfiles = await serverConfigurationProvider.listAvailableProfiles()
96+
developerProfiles = await serverConfigurationProvider.listAvailableProfiles(token)
97+
98+
throwIfCancelled(token)
9599

96100
return developerProfiles
97101
default:
@@ -115,6 +119,12 @@ export const QConfigurationServerToken =
115119
return () => {}
116120
}
117121

122+
function throwIfCancelled(token: CancellationToken) {
123+
if (token.isCancellationRequested) {
124+
throw new ResponseError(LSPErrorCodes.RequestCancelled, 'Request cancelled')
125+
}
126+
}
127+
118128
const ON_GET_CONFIGURATION_FROM_SERVER_ERROR_PREFIX = 'Failed to fetch: '
119129

120130
export class ServerConfigurationProvider {
@@ -130,7 +140,7 @@ export class ServerConfigurationProvider {
130140
)
131141
}
132142

133-
async listAvailableProfiles(): Promise<AmazonQDeveloperProfile[]> {
143+
async listAvailableProfiles(token: CancellationToken): Promise<AmazonQDeveloperProfile[]> {
134144
if (!this.serviceManager.getEnableDeveloperProfileSupport()) {
135145
this.logging.debug('Q developer profiles disabled - returning empty list')
136146
return []
@@ -140,6 +150,7 @@ export class ServerConfigurationProvider {
140150
const profiles = await this.listAllAvailableProfilesHandler({
141151
connectionType: this.credentialsProvider.getConnectionType(),
142152
logging: this.logging,
153+
token: token,
143154
})
144155

145156
return profiles

server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/AmazonQTokenServiceManager.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,7 @@ export class AmazonQTokenServiceManager extends BaseAmazonQServiceManager<CodeWh
305305
const profiles = await getListAllAvailableProfilesHandler(this.serviceFactory)({
306306
connectionType: 'identityCenter',
307307
logging: this.logging,
308+
token: token,
308309
})
309310

310311
this.handleTokenCancellationRequest(token)

server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/qDeveloperProfiles.test.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ import * as assert from 'assert'
22
import sinon, { StubbedInstance, stubInterface } from 'ts-sinon'
33
import { CodeWhispererServiceToken } from '../codeWhispererService'
44
import { SsoConnectionType } from '../utils'
5-
import { AWSInitializationOptions, Logging } from '@aws/language-server-runtimes/server-interface'
5+
import {
6+
AWSInitializationOptions,
7+
CancellationTokenSource,
8+
Logging,
9+
} from '@aws/language-server-runtimes/server-interface'
610
import {
711
AmazonQDeveloperProfile,
812
getListAllAvailableProfilesHandler,
@@ -35,6 +39,7 @@ describe('ListAllAvailableProfiles Handler', () => {
3539

3640
let codeWhispererService: StubbedInstance<CodeWhispererServiceToken>
3741
let handler: ListAllAvailableProfilesHandler
42+
let tokenSource: CancellationTokenSource
3843

3944
const listAvailableProfilesResponse = {
4045
profiles: [
@@ -52,13 +57,15 @@ describe('ListAllAvailableProfiles Handler', () => {
5257
codeWhispererService.listAvailableProfiles.resolves(listAvailableProfilesResponse)
5358

5459
handler = getListAllAvailableProfilesHandler(() => codeWhispererService)
60+
tokenSource = new CancellationTokenSource()
5561
})
5662

5763
it('should aggregrate profiles retrieved from different regions', async () => {
5864
const profiles = await handler({
5965
connectionType: 'identityCenter',
6066
logging,
6167
endpoints: SOME_AWS_Q_ENDPOINTS,
68+
token: tokenSource.token,
6269
})
6370

6471
assert.strictEqual(
@@ -73,6 +80,7 @@ describe('ListAllAvailableProfiles Handler', () => {
7380
const profiles = await handler({
7481
connectionType,
7582
logging,
83+
token: tokenSource.token,
7684
})
7785

7886
assert.deepStrictEqual(profiles, [])
@@ -103,6 +111,7 @@ describe('ListAllAvailableProfiles Handler', () => {
103111
connectionType: 'identityCenter',
104112
logging,
105113
endpoints: SOME_AWS_Q_ENDPOINT,
114+
token: tokenSource.token,
106115
})
107116

108117
sinon.assert.calledThrice(codeWhispererService.listAvailableProfiles)
@@ -120,6 +129,7 @@ describe('ListAllAvailableProfiles Handler', () => {
120129
connectionType: 'identityCenter',
121130
logging,
122131
endpoints: SOME_AWS_Q_ENDPOINT,
132+
token: tokenSource.token,
123133
})
124134

125135
assert.strictEqual(codeWhispererService.listAvailableProfiles.callCount, MAX_EXPECTED_PAGES)

server/aws-lsp-codewhisperer/src/shared/amazonQServiceManager/qDeveloperProfiles.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
AWSInitializationOptions,
3+
CancellationToken,
34
Logging,
45
LSPErrorCodes,
56
ResponseError,
@@ -22,6 +23,7 @@ export interface ListAllAvailableProfilesHandlerParams {
2223
connectionType: SsoConnectionType
2324
logging: Logging
2425
endpoints?: Map<string, string> // override option for flexibility, we default to all (AWS_Q_ENDPOINTS)
26+
token: CancellationToken
2527
}
2628

2729
export type ListAllAvailableProfilesHandler = (
@@ -33,7 +35,7 @@ const MAX_Q_DEVELOPER_PROFILES_PER_PAGE = 10
3335

3436
export const getListAllAvailableProfilesHandler =
3537
(service: (region: string, endpoint: string) => CodeWhispererServiceToken): ListAllAvailableProfilesHandler =>
36-
async ({ connectionType, logging, endpoints }) => {
38+
async ({ connectionType, logging, endpoints, token }) => {
3739
if (!connectionType || connectionType !== 'identityCenter') {
3840
logging.debug('Connection type is not set or not identityCenter - returning empty response.')
3941
return []
@@ -42,13 +44,21 @@ export const getListAllAvailableProfilesHandler =
4244
let allProfiles: AmazonQDeveloperProfile[] = []
4345
const qEndpoints = endpoints ?? AWS_Q_ENDPOINTS
4446

47+
if (token.isCancellationRequested) {
48+
return []
49+
}
50+
4551
const result = await Promise.allSettled(
4652
Array.from(qEndpoints.entries(), ([region, endpoint]) => {
4753
const codeWhispererService = service(region, endpoint)
48-
return fetchProfilesFromRegion(codeWhispererService, region, logging)
54+
return fetchProfilesFromRegion(codeWhispererService, region, logging, token)
4955
})
5056
)
5157

58+
if (token.isCancellationRequested) {
59+
return []
60+
}
61+
5262
const fulfilledResults = result.filter(settledResult => settledResult.status === 'fulfilled')
5363

5464
if (fulfilledResults.length === 0) {
@@ -63,7 +73,8 @@ export const getListAllAvailableProfilesHandler =
6373
async function fetchProfilesFromRegion(
6474
service: CodeWhispererServiceToken,
6575
region: string,
66-
logging: Logging
76+
logging: Logging,
77+
token: CancellationToken
6778
): Promise<AmazonQDeveloperProfile[]> {
6879
let allRegionalProfiles: AmazonQDeveloperProfile[] = []
6980
let nextToken: string | undefined = undefined
@@ -73,6 +84,10 @@ async function fetchProfilesFromRegion(
7384
do {
7485
logging.debug(`Fetching profiles from region: ${region} (iteration: ${numberOfPages})`)
7586

87+
if (token.isCancellationRequested) {
88+
return allRegionalProfiles
89+
}
90+
7691
const response = await service.listAvailableProfiles({
7792
maxResults: MAX_Q_DEVELOPER_PROFILES_PER_PAGE,
7893
nextToken: nextToken,

0 commit comments

Comments
 (0)