Skip to content

Commit 81e19b9

Browse files
LiGaCuJiatong Li
andauthored
fix(amazonq): stop continuous monitor when WCS sees ServiceQuotaExceeded (#1957)
Co-authored-by: Jiatong Li <[email protected]>
1 parent 528f820 commit 81e19b9

File tree

3 files changed

+166
-6
lines changed

3 files changed

+166
-6
lines changed

server/aws-lsp-codewhisperer/src/language-server/workspaceContext/workspaceContextServer.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ export const WorkspaceContextServer = (): Server => features => {
221221
isLoggedInUsingBearerToken(credentialsProvider) &&
222222
abTestingEnabled &&
223223
!workspaceFolderManager.getOptOutStatus() &&
224+
!workspaceFolderManager.getServiceQuotaExceededStatus() &&
224225
workspaceIdentifier
225226
)
226227
}
@@ -302,6 +303,7 @@ export const WorkspaceContextServer = (): Server => features => {
302303
await evaluateABTesting()
303304
isWorkflowInitialized = true
304305

306+
workspaceFolderManager.resetAdminOptOutAndServiceQuotaStatus()
305307
if (!isUserEligibleForWorkspaceContext()) {
306308
return
307309
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { WorkspaceFolderManager } from './workspaceFolderManager'
2+
import sinon, { stubInterface, StubbedInstance } from 'ts-sinon'
3+
import { AmazonQTokenServiceManager } from '../../shared/amazonQServiceManager/AmazonQTokenServiceManager'
4+
import { CredentialsProvider, Logging } from '@aws/language-server-runtimes/server-interface'
5+
import { DependencyDiscoverer } from './dependency/dependencyDiscoverer'
6+
import { WorkspaceFolder } from 'vscode-languageserver-protocol'
7+
import { ArtifactManager } from './artifactManager'
8+
import { CodeWhispererServiceToken } from '../../shared/codeWhispererService'
9+
import { CreateWorkspaceResponse } from '../../client/token/codewhispererbearertokenclient'
10+
import { AWSError } from 'aws-sdk'
11+
12+
describe('WorkspaceFolderManager', () => {
13+
let mockServiceManager: StubbedInstance<AmazonQTokenServiceManager>
14+
let mockLogging: StubbedInstance<Logging>
15+
let mockCredentialsProvider: StubbedInstance<CredentialsProvider>
16+
let mockDependencyDiscoverer: StubbedInstance<DependencyDiscoverer>
17+
let mockArtifactManager: StubbedInstance<ArtifactManager>
18+
let mockCodeWhispererService: StubbedInstance<CodeWhispererServiceToken>
19+
let workspaceFolderManager: WorkspaceFolderManager
20+
21+
beforeEach(() => {
22+
mockServiceManager = stubInterface<AmazonQTokenServiceManager>()
23+
mockLogging = stubInterface<Logging>()
24+
mockCredentialsProvider = stubInterface<CredentialsProvider>()
25+
mockDependencyDiscoverer = stubInterface<DependencyDiscoverer>()
26+
mockArtifactManager = stubInterface<ArtifactManager>()
27+
mockCodeWhispererService = stubInterface<CodeWhispererServiceToken>()
28+
29+
mockServiceManager.getCodewhispererService.returns(mockCodeWhispererService)
30+
})
31+
32+
afterEach(() => {
33+
sinon.restore()
34+
})
35+
36+
describe('getServiceQuotaExceededStatus', () => {
37+
it('should return true when service quota is exceeded', async () => {
38+
// Setup
39+
const workspaceFolders: WorkspaceFolder[] = [
40+
{
41+
uri: 'file:///test/workspace',
42+
name: 'test-workspace',
43+
},
44+
]
45+
46+
// Mock the createWorkspace method to throw a ServiceQuotaExceededException
47+
const mockError: AWSError = {
48+
name: 'ServiceQuotaExceededException',
49+
message: 'You have too many active running workspaces.',
50+
code: 'ServiceQuotaExceededException',
51+
time: new Date(),
52+
retryable: false,
53+
statusCode: 400,
54+
}
55+
56+
mockCodeWhispererService.createWorkspace.rejects(mockError)
57+
58+
// Create the WorkspaceFolderManager instance using the static createInstance method
59+
workspaceFolderManager = WorkspaceFolderManager.createInstance(
60+
mockServiceManager,
61+
mockLogging,
62+
mockArtifactManager,
63+
mockDependencyDiscoverer,
64+
workspaceFolders,
65+
mockCredentialsProvider,
66+
'test-workspace-identifier'
67+
)
68+
69+
// Spy on clearAllWorkspaceResources and related methods
70+
const clearAllWorkspaceResourcesSpy = sinon.stub(
71+
workspaceFolderManager as any,
72+
'clearAllWorkspaceResources'
73+
)
74+
75+
// Act - trigger the createNewWorkspace method which sets isServiceQuotaExceeded
76+
await (workspaceFolderManager as any).createNewWorkspace()
77+
78+
// Assert
79+
expect(workspaceFolderManager.getServiceQuotaExceededStatus()).toBe(true)
80+
81+
// Verify that clearAllWorkspaceResources was called
82+
sinon.assert.calledOnce(clearAllWorkspaceResourcesSpy)
83+
})
84+
85+
it('should return false when service quota is not exceeded', async () => {
86+
// Setup
87+
const workspaceFolders: WorkspaceFolder[] = [
88+
{
89+
uri: 'file:///test/workspace',
90+
name: 'test-workspace',
91+
},
92+
]
93+
94+
// Mock successful response
95+
const mockResponse: CreateWorkspaceResponse = {
96+
workspace: {
97+
workspaceId: 'test-workspace-id',
98+
workspaceStatus: 'RUNNING',
99+
},
100+
}
101+
102+
mockCodeWhispererService.createWorkspace.resolves(mockResponse as any)
103+
104+
// Create the WorkspaceFolderManager instance using the static createInstance method
105+
workspaceFolderManager = WorkspaceFolderManager.createInstance(
106+
mockServiceManager,
107+
mockLogging,
108+
mockArtifactManager,
109+
mockDependencyDiscoverer,
110+
workspaceFolders,
111+
mockCredentialsProvider,
112+
'test-workspace-identifier'
113+
)
114+
115+
// Spy on clearAllWorkspaceResources
116+
const clearAllWorkspaceResourcesSpy = sinon.stub(
117+
workspaceFolderManager as any,
118+
'clearAllWorkspaceResources'
119+
)
120+
121+
// Act - trigger the createNewWorkspace method
122+
await (workspaceFolderManager as any).createNewWorkspace()
123+
124+
// Assert
125+
expect(workspaceFolderManager.getServiceQuotaExceededStatus()).toBe(false)
126+
127+
// Verify that clearAllWorkspaceResources was not called
128+
sinon.assert.notCalled(clearAllWorkspaceResourcesSpy)
129+
})
130+
})
131+
})

server/aws-lsp-codewhisperer/src/language-server/workspaceContext/workspaceFolderManager.ts

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { DependencyDiscoverer } from './dependency/dependencyDiscoverer'
1919
import { AmazonQTokenServiceManager } from '../../shared/amazonQServiceManager/AmazonQTokenServiceManager'
2020
import { URI } from 'vscode-uri'
2121
import path = require('path')
22+
import { isAwsError } from '../../shared/utils'
2223

2324
interface WorkspaceState {
2425
remoteWorkspaceState: WorkspaceStatus
@@ -47,12 +48,14 @@ export class WorkspaceFolderManager {
4748
private credentialsProvider: CredentialsProvider
4849
private readonly INITIAL_CHECK_INTERVAL = 40 * 1000 // 40 seconds
4950
private readonly INITIAL_CONNECTION_TIMEOUT = 2 * 60 * 1000 // 2 minutes
50-
private readonly CONTINUOUS_MONITOR_INTERVAL = 5 * 60 * 1000 // 30 minutes
51+
private readonly CONTINUOUS_MONITOR_INTERVAL = 30 * 60 * 1000 // 30 minutes
5152
private readonly MESSAGE_PUBLISH_INTERVAL: number = 100 // 100 milliseconds
5253
private continuousMonitorInterval: NodeJS.Timeout | undefined
5354
private optOutMonitorInterval: NodeJS.Timeout | undefined
5455
private messageQueueConsumerInterval: NodeJS.Timeout | undefined
5556
private isOptedOut: boolean = false
57+
// Tracks if the user has reached their maximum allowed remote workspaces quota
58+
private isServiceQuotaExceeded: boolean = false
5659

5760
static createInstance(
5861
serviceManager: AmazonQTokenServiceManager,
@@ -135,6 +138,15 @@ export class WorkspaceFolderManager {
135138
return this.isOptedOut
136139
}
137140

141+
getServiceQuotaExceededStatus(): boolean {
142+
return this.isServiceQuotaExceeded
143+
}
144+
145+
resetAdminOptOutAndServiceQuotaStatus(): void {
146+
this.isOptedOut = false
147+
this.isServiceQuotaExceeded = false
148+
}
149+
138150
getWorkspaceState(): WorkspaceState {
139151
return this.workspaceState
140152
}
@@ -343,7 +355,7 @@ export class WorkspaceFolderManager {
343355
await this.checkRemoteWorkspaceStatusAndReact(true)
344356

345357
// Set up continuous monitoring which periodically invokes checkRemoteWorkspaceStatusAndReact
346-
if (!this.isOptedOut && this.continuousMonitorInterval === undefined) {
358+
if (!this.isOptedOut && !this.isServiceQuotaExceeded && this.continuousMonitorInterval === undefined) {
347359
this.logging.log(`Starting continuous monitor for workspace [${this.workspaceIdentifier}]`)
348360
this.continuousMonitorInterval = setInterval(async () => {
349361
try {
@@ -592,6 +604,13 @@ export class WorkspaceFolderManager {
592604

593605
private async createNewWorkspace() {
594606
const createWorkspaceResult = await this.createWorkspace(this.workspaceIdentifier)
607+
608+
this.isServiceQuotaExceeded = createWorkspaceResult.isServiceQuotaExceeded
609+
if (this.isServiceQuotaExceeded) {
610+
// Stop continuous monitor and all actions
611+
this.clearAllWorkspaceResources()
612+
}
613+
595614
const workspaceDetails = createWorkspaceResult.response
596615
if (!workspaceDetails) {
597616
this.logging.warn(`Failed to create remote workspace for [${this.workspaceIdentifier}]`)
@@ -712,23 +731,31 @@ export class WorkspaceFolderManager {
712731
return { metadata, optOut, error }
713732
}
714733

715-
private async createWorkspace(workspaceRoot: WorkspaceRoot) {
734+
private async createWorkspace(workspaceRoot: WorkspaceRoot): Promise<{
735+
response: CreateWorkspaceResponse | undefined | null
736+
isServiceQuotaExceeded: boolean
737+
error: any
738+
}> {
716739
let response: CreateWorkspaceResponse | undefined | null
740+
let isServiceQuotaExceeded = false
741+
let error: any
717742
try {
718743
response = await this.serviceManager.getCodewhispererService().createWorkspace({
719744
workspaceRoot: workspaceRoot,
720745
})
721-
return { response, error: null }
722746
} catch (e: any) {
723747
this.logging.warn(
724748
`Error while creating workspace (${workspaceRoot}): ${e.message}. Error is ${e.retryable ? '' : 'not'} retryable}`
725749
)
726-
const error = {
750+
if (isAwsError(e) && e.code === 'ServiceQuotaExceededException') {
751+
isServiceQuotaExceeded = true
752+
}
753+
error = {
727754
message: e.message,
728755
retryable: e.retryable ?? false,
729756
originalError: e,
730757
}
731-
return { response: null, error }
732758
}
759+
return { response, isServiceQuotaExceeded, error }
733760
}
734761
}

0 commit comments

Comments
 (0)