Skip to content

Commit c240997

Browse files
LiGaCuJiatong Liwweitao
authored
feat: launch one remote workspace for all workspace folders (#1348)
* feat: initial modifications for launching one container per workspace * feat: use real workspaceIdentifier passed from extensions * chore: add singleton consumer of message queue * fix: prevent hang from remoteWorkspaceIdPromise under race condition * fix: eliminate dependency upload issues * feat: make artifacts upload obey global size limit * fix: prevent existing .classpath being overridden * chore: add back client-side a/b testing check * chore: fix TypeScript ESLint rule violations * chore: refactor WebSocketClient send method --------- Co-authored-by: Jiatong Li <[email protected]> Co-authored-by: Weitao Wang <[email protected]>
1 parent 80fdbdf commit c240997

File tree

8 files changed

+456
-687
lines changed

8 files changed

+456
-687
lines changed

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

Lines changed: 73 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,13 @@ const IGNORE_PATTERNS = [
4747
'**/target/**', // Maven/Gradle builds
4848
]
4949

50-
const MAX_UNCOMPRESSED_SRC_SIZE_MB = 250 // 250 MB limit per language per workspace folder
51-
const MAX_UNCOMPRESSED_SRC_SIZE_BYTES = MAX_UNCOMPRESSED_SRC_SIZE_MB * 1024 * 1024 // Convert to bytes
50+
interface FileSizeDetails {
51+
includedFileCount: number
52+
includedSize: number
53+
skippedFileCount: number
54+
skippedSize: number
55+
}
56+
const MAX_UNCOMPRESSED_SRC_SIZE_BYTES = 2 * 1024 * 1024 * 1024 // 2 GB
5257

5358
export class ArtifactManager {
5459
private workspace: Workspace
@@ -81,6 +86,12 @@ export class ArtifactManager {
8186

8287
async addNewDirectories(newDirectories: URI[]): Promise<FileMetadata[]> {
8388
let zipFileMetadata: FileMetadata[] = []
89+
const fileSizeDetails: FileSizeDetails = {
90+
includedFileCount: 0,
91+
includedSize: 0,
92+
skippedFileCount: 0,
93+
skippedSize: 0,
94+
}
8495

8596
for (const directory of newDirectories) {
8697
const workspaceFolder = this.workspaceFolders.find(ws => directory.path.startsWith(URI.parse(ws.uri).path))
@@ -95,7 +106,12 @@ export class ArtifactManager {
95106
const relativePath = path.relative(workspacePath, directory.path)
96107

97108
const filesByLanguage = await this.processDirectory(workspaceFolder, directory.path, relativePath)
98-
zipFileMetadata = await this.processFilesByLanguage(workspaceFolder, filesByLanguage, relativePath)
109+
zipFileMetadata = await this.processFilesByLanguage(
110+
workspaceFolder,
111+
fileSizeDetails,
112+
filesByLanguage,
113+
relativePath
114+
)
99115
} catch (error) {
100116
this.logging.warn(`Error processing new directory ${directory.path}: ${error}`)
101117
}
@@ -286,9 +302,12 @@ export class ArtifactManager {
286302
return filesMetadata
287303
}
288304

289-
cleanup(preserveDependencies: boolean = false) {
305+
cleanup(preserveDependencies: boolean = false, workspaceFolders?: WorkspaceFolder[]) {
290306
try {
291-
this.workspaceFolders.forEach(workspaceToRemove => {
307+
if (workspaceFolders === undefined) {
308+
workspaceFolders = this.workspaceFolders
309+
}
310+
workspaceFolders.forEach(workspaceToRemove => {
292311
const workspaceDirPath = path.join(this.tempDirPath, workspaceToRemove.name)
293312

294313
if (preserveDependencies) {
@@ -430,43 +449,50 @@ export class ArtifactManager {
430449

431450
private async createZipForLanguage(
432451
workspaceFolder: WorkspaceFolder,
452+
fileSizeDetails: FileSizeDetails,
433453
language: CodewhispererLanguage,
434454
files: FileMetadata[],
435455
subDirectory: string = ''
436-
): Promise<FileMetadata> {
456+
): Promise<FileMetadata | undefined> {
437457
const zipDirectoryPath = path.join(this.tempDirPath, workspaceFolder.name, subDirectory)
438458
this.createFolderIfNotExist(zipDirectoryPath)
439459

440460
const zipPath = path.join(zipDirectoryPath, `${language}.zip`)
441461

442-
let currentSize = 0
443462
let skippedSize = 0
444463
let skippedFiles = 0
445464
const filesToInclude: FileMetadata[] = []
446465

447466
// Don't add files to the zip if the total size of uncompressed source code would go over the limit
448467
// Currently there is no ordering on the files. If the first file added to the zip is equal to the limit, only it will be added and no other files will be added
449468
for (const file of files) {
450-
if (currentSize + file.contentLength <= MAX_UNCOMPRESSED_SRC_SIZE_BYTES) {
469+
if (fileSizeDetails.includedSize + file.contentLength <= MAX_UNCOMPRESSED_SRC_SIZE_BYTES) {
451470
filesToInclude.push(file)
452-
currentSize += file.contentLength
471+
fileSizeDetails.includedSize += file.contentLength
472+
fileSizeDetails.includedFileCount += 1
453473
} else {
454474
skippedSize += file.contentLength
455475
skippedFiles += 1
476+
fileSizeDetails.skippedSize += file.contentLength
477+
fileSizeDetails.skippedFileCount += 1
456478
}
457479
}
458480

459-
const zipBuffer = await this.createZipBuffer(filesToInclude)
460-
await fs.promises.writeFile(zipPath, zipBuffer)
461-
462-
const stats = fs.statSync(zipPath)
463-
464481
if (skippedFiles > 0) {
465482
this.log(
466483
`Skipped ${skippedFiles} ${language} files of total size ${skippedSize} bytes due to exceeding the maximum zip size`
467484
)
468485
}
469486

487+
if (filesToInclude.length === 0) {
488+
return undefined
489+
}
490+
491+
const zipBuffer = await this.createZipBuffer(filesToInclude)
492+
await fs.promises.writeFile(zipPath, zipBuffer)
493+
494+
const stats = fs.statSync(zipPath)
495+
470496
return {
471497
filePath: zipPath,
472498
relativePath: path.join(workspaceFolder.name, subDirectory, `files.zip`),
@@ -569,18 +595,35 @@ export class ArtifactManager {
569595
private async processWorkspaceFolders(workspaceFolders: WorkspaceFolder[]): Promise<FileMetadata[]> {
570596
const startTime = performance.now()
571597
let zipFileMetadata: FileMetadata[] = []
598+
const fileSizeDetails: FileSizeDetails = {
599+
includedFileCount: 0,
600+
includedSize: 0,
601+
skippedFileCount: 0,
602+
skippedSize: 0,
603+
}
572604

573605
for (const workspaceFolder of workspaceFolders) {
574606
const workspacePath = URI.parse(workspaceFolder.uri).path
575607

576608
try {
577609
const filesByLanguage = await this.processDirectory(workspaceFolder, workspacePath)
578-
const fileMetadata = await this.processFilesByLanguage(workspaceFolder, filesByLanguage)
610+
const fileMetadata = await this.processFilesByLanguage(
611+
workspaceFolder,
612+
fileSizeDetails,
613+
filesByLanguage
614+
)
579615
zipFileMetadata.push(...fileMetadata)
580616
} catch (error) {
581617
this.logging.warn(`Error processing workspace folder ${workspacePath}: ${error}`)
582618
}
583619
}
620+
if (fileSizeDetails.skippedFileCount > 0) {
621+
this.logging.warn(
622+
`Skipped ${fileSizeDetails.skippedFileCount} files (total size: ` +
623+
`${fileSizeDetails.skippedSize} bytes) due to exceeding the maximum artifact size`
624+
)
625+
}
626+
584627
const totalTime = performance.now() - startTime
585628
this.log(`Creating workspace source code artifacts took: ${totalTime.toFixed(2)}ms`)
586629

@@ -589,22 +632,31 @@ export class ArtifactManager {
589632

590633
private async processFilesByLanguage(
591634
workspaceFolder: WorkspaceFolder,
635+
fileSizeDetails: FileSizeDetails,
592636
filesByLanguage: Map<CodewhispererLanguage, FileMetadata[]>,
593637
relativePath?: string
594638
): Promise<FileMetadata[]> {
595639
const zipFileMetadata: FileMetadata[] = []
596640
await this.updateWorkspaceFiles(workspaceFolder, filesByLanguage)
597-
598641
for (const [language, files] of filesByLanguage.entries()) {
599-
// Genrate java .classpath and .project files
642+
// Generate java .classpath and .project files
600643
const processedFiles =
601644
language === 'java' ? await this.processJavaProjectConfig(workspaceFolder, files) : files
602645

603-
const zipMetadata = await this.createZipForLanguage(workspaceFolder, language, processedFiles, relativePath)
604-
this.log(
605-
`Created zip for language ${language} out of ${processedFiles.length} files in ${workspaceFolder.name}`
646+
const zipMetadata = await this.createZipForLanguage(
647+
workspaceFolder,
648+
fileSizeDetails,
649+
language,
650+
processedFiles,
651+
relativePath
606652
)
607-
zipFileMetadata.push(zipMetadata)
653+
654+
if (zipMetadata) {
655+
this.log(
656+
`Created zip for language ${language} out of ${processedFiles.length} files in ${workspaceFolder.name}`
657+
)
658+
zipFileMetadata.push(zipMetadata)
659+
}
608660
}
609661
return zipFileMetadata
610662
}

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

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ export class WebSocketClient {
1111
private reconnectAttempts: number = 0
1212
private readonly maxReconnectAttempts: number = 5
1313
private messageQueue: string[] = []
14-
private readonly messageThrottleDelay: number = 100
1514

1615
constructor(url: string, logging: Logging, credentialsProvider: CredentialsProvider) {
1716
this.url = url
@@ -104,7 +103,11 @@ export class WebSocketClient {
104103
while (this.messageQueue.length > 0) {
105104
const message = this.messageQueue.shift()
106105
if (message) {
107-
this.send(message).catch(error => this.logging.error(`Error sending message: ${error}`))
106+
try {
107+
this.send(message)
108+
} catch (error) {
109+
this.logging.error(`Error sending message: ${error}`)
110+
}
108111
}
109112
}
110113
}
@@ -141,23 +144,10 @@ export class WebSocketClient {
141144
}
142145
}
143146

144-
// https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_client_applications#sending_data_to_the_server
145-
// TODO, the approach of delaying websocket messages should be investigated and validated
146-
// The current approach could be susceptible to race conditions that might result in out of order events
147-
// Consider this scenario
148-
// wsClient.send("message1"); // needs throttling, will wait 100ms
149-
// wsClient.send("message2"); // runs immediately without waiting
150-
151-
// What actually happens:
152-
// - Both calls start executing simultaneously
153-
// - Both check timeSinceLastMessage at nearly the same time
154-
// - Both might determine they need to wait
155-
// - They could end up sending in unpredictable order
156-
// It might be better to keep an active queue in the client and expose enqueueMessage instead of send
157-
public async send(message: string): Promise<void> {
147+
public send(message: string): void {
158148
if (this.ws?.readyState === WebSocket.OPEN) {
159-
await new Promise(resolve => setTimeout(resolve, this.messageThrottleDelay))
160149
this.ws.send(message)
150+
this.logging.debug('Message sent successfully to the remote workspace')
161151
} else {
162152
this.queueMessage(message)
163153
}

server/aws-lsp-codewhisperer/src/language-server/workspaceContext/dependency/dependencyDiscoverer.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ export class DependencyDiscoverer {
1212
private workspaceFolders: WorkspaceFolder[]
1313
public dependencyHandlerRegistry: LanguageDependencyHandler<BaseDependencyInfo>[] = []
1414
private initializedWorkspaceFolder = new Map<WorkspaceFolder, boolean>()
15+
// Create a SharedArrayBuffer with 4 bytes (for a 32-bit unsigned integer) for thread-safe counter
16+
protected dependencyUploadedSizeSum = new Uint32Array(new SharedArrayBuffer(4))
1517

1618
constructor(
1719
workspace: Workspace,
@@ -21,6 +23,7 @@ export class DependencyDiscoverer {
2123
) {
2224
this.workspaceFolders = workspaceFolders
2325
this.logging = logging
26+
this.dependencyUploadedSizeSum[0] = 0
2427

2528
let jstsHandlerCreated = false
2629
supportedWorkspaceContextLanguages.forEach(language => {
@@ -29,7 +32,8 @@ export class DependencyDiscoverer {
2932
workspace,
3033
logging,
3134
workspaceFolders,
32-
artifactManager
35+
artifactManager,
36+
this.dependencyUploadedSizeSum
3337
)
3438
if (handler) {
3539
// Share handler for javascript and typescript
@@ -130,6 +134,13 @@ export class DependencyDiscoverer {
130134
this.logging.log(`Dependency search completed successfully`)
131135
}
132136

137+
async reSyncDependenciesToS3(folders: WorkspaceFolder[]) {
138+
Atomics.store(this.dependencyUploadedSizeSum, 0, 0)
139+
for (const dependencyHandler of this.dependencyHandlerRegistry) {
140+
await dependencyHandler.zipDependencyMap(folders)
141+
}
142+
}
143+
133144
async handleDependencyUpdateFromLSP(language: string, paths: string[], workspaceRoot?: WorkspaceFolder) {
134145
for (const dependencyHandler of this.dependencyHandlerRegistry) {
135146
if (dependencyHandler.language != language) {
@@ -144,6 +155,7 @@ export class DependencyDiscoverer {
144155
this.dependencyHandlerRegistry.forEach(dependencyHandler => {
145156
dependencyHandler.dispose()
146157
})
158+
Atomics.store(this.dependencyUploadedSizeSum, 0, 0)
147159
}
148160

149161
public disposeWorkspaceFolder(workspaceFolder: WorkspaceFolder) {

server/aws-lsp-codewhisperer/src/language-server/workspaceContext/dependency/dependencyHandler/LanguageDependencyHandler.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,21 +26,23 @@ export abstract class LanguageDependencyHandler<T extends BaseDependencyInfo> {
2626
protected workspaceFolders: WorkspaceFolder[]
2727
// key: workspaceFolder, value: {key: dependency name, value: Dependency}
2828
protected dependencyMap = new Map<WorkspaceFolder, Map<string, Dependency>>()
29-
protected dependencyUploadedSize = new Map<WorkspaceFolder, number>()
29+
protected dependencyUploadedSizeMap = new Map<WorkspaceFolder, number>()
30+
protected dependencyUploadedSizeSum: Uint32Array<SharedArrayBuffer>
3031
protected dependencyWatchers: Map<string, fs.FSWatcher> = new Map<string, fs.FSWatcher>()
3132
protected artifactManager: ArtifactManager
3233
protected dependenciesFolderName: string
3334
protected eventEmitter: EventEmitter
3435
protected readonly MAX_SINGLE_DEPENDENCY_SIZE: number = 500 * 1024 * 1024 // 500 MB
35-
protected readonly MAX_WORKSPACE_DEPENDENCY_SIZE: number = 5 * 1024 * 1024 * 1024 //5 GB
36+
protected readonly MAX_WORKSPACE_DEPENDENCY_SIZE: number = 8 * 1024 * 1024 * 1024 // 8 GB
3637

3738
constructor(
3839
language: CodewhispererLanguage,
3940
workspace: Workspace,
4041
logging: Logging,
4142
workspaceFolders: WorkspaceFolder[],
4243
artifactManager: ArtifactManager,
43-
dependenciesFolderName: string
44+
dependenciesFolderName: string,
45+
dependencyUploadedSizeSum: Uint32Array<SharedArrayBuffer>
4446
) {
4547
this.language = language
4648
this.workspace = workspace
@@ -54,7 +56,7 @@ export abstract class LanguageDependencyHandler<T extends BaseDependencyInfo> {
5456
this.workspaceFolders.forEach(workSpaceFolder =>
5557
this.dependencyMap.set(workSpaceFolder, new Map<string, Dependency>())
5658
)
57-
59+
this.dependencyUploadedSizeSum = dependencyUploadedSizeSum
5860
this.eventEmitter = new EventEmitter()
5961
}
6062

@@ -85,14 +87,18 @@ export abstract class LanguageDependencyHandler<T extends BaseDependencyInfo> {
8587
dependencyMap: Map<string, Dependency>
8688
): void
8789

88-
public onDependencyChange(callback: (workspaceFolder: WorkspaceFolder, zips: FileMetadata[]) => void): void {
90+
public onDependencyChange(
91+
callback: (workspaceFolder: WorkspaceFolder, zips: FileMetadata[], addWSFolderPathInS3: boolean) => void
92+
): void {
8993
this.eventEmitter.on('dependencyChange', callback)
9094
}
9195

9296
protected emitDependencyChange(workspaceFolder: WorkspaceFolder, zips: FileMetadata[]): void {
9397
if (zips.length > 0) {
9498
this.logging.log(`Emitting ${this.language} dependency change event for ${workspaceFolder.name}`)
95-
this.eventEmitter.emit('dependencyChange', workspaceFolder, zips)
99+
// If language is JavaScript or TypeScript, we want to preserve the workspaceFolder path in S3 path
100+
const addWSFolderPathInS3 = this.language === 'javascript' || this.language === 'typescript'
101+
this.eventEmitter.emit('dependencyChange', workspaceFolder, zips, addWSFolderPathInS3)
96102
return
97103
}
98104
}
@@ -170,10 +176,11 @@ export abstract class LanguageDependencyHandler<T extends BaseDependencyInfo> {
170176
}
171177
currentChunk.push(dependency)
172178
currentChunkSize += dependency.size
173-
this.dependencyUploadedSize.set(
179+
this.dependencyUploadedSizeMap.set(
174180
workspaceFolder,
175-
(this.dependencyUploadedSize.get(workspaceFolder) || 0) + dependency.size
181+
(this.dependencyUploadedSizeMap.get(workspaceFolder) || 0) + dependency.size
176182
)
183+
Atomics.add(this.dependencyUploadedSizeSum, 0, dependency.size)
177184
// Mark this dependency that has been zipped
178185
dependency.zipped = true
179186
this.dependencyMap.get(workspaceFolder)?.set(dependency.name, dependency)
@@ -299,7 +306,7 @@ export abstract class LanguageDependencyHandler<T extends BaseDependencyInfo> {
299306
* However, everytime flare server restarts, this dependency map will be initialized.
300307
*/
301308
private validateWorkspaceDependencySize(workspaceFolder: WorkspaceFolder): boolean {
302-
let uploadedSize = this.dependencyUploadedSize.get(workspaceFolder)
309+
let uploadedSize = Atomics.load(this.dependencyUploadedSizeSum, 0)
303310
if (uploadedSize && this.MAX_WORKSPACE_DEPENDENCY_SIZE < uploadedSize) {
304311
return false
305312
}
@@ -308,14 +315,15 @@ export abstract class LanguageDependencyHandler<T extends BaseDependencyInfo> {
308315

309316
dispose(): void {
310317
this.dependencyMap.clear()
311-
this.dependencyUploadedSize.clear()
318+
this.dependencyUploadedSizeMap.clear()
312319
this.dependencyWatchers.forEach(watcher => watcher.close())
313320
this.dependencyWatchers.clear()
314321
}
315322

316323
disposeWorkspaceFolder(workspaceFolder: WorkspaceFolder): void {
317324
this.dependencyMap.delete(workspaceFolder)
318-
this.dependencyUploadedSize.delete(workspaceFolder)
325+
Atomics.sub(this.dependencyUploadedSizeSum, 0, this.dependencyUploadedSizeMap.get(workspaceFolder) || 0)
326+
this.dependencyUploadedSizeMap.delete(workspaceFolder)
319327
this.disposeWatchers(workspaceFolder)
320328
this.disposeDependencyInfo(workspaceFolder)
321329
}

0 commit comments

Comments
 (0)