-
Notifications
You must be signed in to change notification settings - Fork 471
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(appbuilder): Prevent parallel build processes on the same template #6172
base: master
Are you sure you want to change the base?
Changes from all commits
1568065
fe82be7
379ba6b
6c23ba3
00e99fe
86848f2
c261185
0922083
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
/*! | ||
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
export const buildProcessMementoRootKey = 'samcli.build.processes' | ||
export const globalIdentifier = 'global' | ||
export const buildMementoRootKey = 'samcli.build.params' | ||
export const deployMementoRootKey = 'samcli.deploy.params' | ||
export const syncMementoRootKey = 'samcli.sync.params' |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,6 +18,7 @@ import { telemetry } from '../telemetry/telemetry' | |
import globals from '../extensionGlobals' | ||
import { getLogger } from '../logger/logger' | ||
import { ChildProcessResult } from '../utilities/processUtils' | ||
import { buildProcessMementoRootKey, globalIdentifier } from './constants' | ||
|
||
/** | ||
* @description determines the root directory of the project given Template Item | ||
|
@@ -108,6 +109,33 @@ export async function updateRecentResponse( | |
getLogger().warn(`sam: unable to save response at key "${key}": %s`, err) | ||
} | ||
} | ||
|
||
/** | ||
* Returns true if there's an ongoing build process for the provided template, false otherwise | ||
* @Param templatePath The path to the template.yaml file | ||
*/ | ||
function isBuildInProgress(templatePath: string): boolean { | ||
return getRecentResponse(buildProcessMementoRootKey, globalIdentifier, templatePath) !== undefined | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Future note: these functions (and probably more, I guess) are using shared mutable state. You may want to consider wrapping this in a dedicated file similar to the existing |
||
} | ||
|
||
/** | ||
* Throws an error if there's a build in progress for the provided template | ||
* @Param templatePath The path to the template.yaml file | ||
*/ | ||
export function throwIfTemplateIsBeingBuilt(templatePath: string) { | ||
if (isBuildInProgress(templatePath)) { | ||
throw new ToolkitError('This template is already being built', { code: 'BuildInProgress' }) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would say this check is too strict. There are possibility that the build is finished or exited but we also failed to |
||
} | ||
} | ||
|
||
export async function registerTemplateBuild(templatePath: string) { | ||
await updateRecentResponse(buildProcessMementoRootKey, globalIdentifier, templatePath, 'true') | ||
} | ||
|
||
export async function unregisterTemplateBuild(templatePath: string) { | ||
await updateRecentResponse(buildProcessMementoRootKey, globalIdentifier, templatePath, undefined) | ||
} | ||
|
||
export function getSamCliErrorMessage(stderr: string): string { | ||
// Split the stderr string by newline, filter out empty lines, and get the last line | ||
const lines = stderr | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,6 +17,7 @@ import { | |
getBuildFlags, | ||
ParamsSource, | ||
runBuild, | ||
SamBuildResult, | ||
} from '../../../shared/sam/build' | ||
import { TreeNode } from '../../../shared/treeview/resourceTreeDataProvider' | ||
import { createWizardTester } from '../wizards/wizardTestUtils' | ||
|
@@ -32,6 +33,7 @@ import { samconfigCompleteData, validTemplateData } from './samTestUtils' | |
import { CloudFormationTemplateRegistry } from '../../../shared/fs/templateRegistry' | ||
import { getTestWindow } from '../vscode/window' | ||
import { CancellationError } from '../../../shared/utilities/timeoutUtils' | ||
import { SamAppLocation } from '../../../awsService/appBuilder/explorer/samProject' | ||
|
||
describe('SAM BuildWizard', async function () { | ||
const createTester = async (params?: Partial<BuildParams>, arg?: TreeNode | undefined) => | ||
|
@@ -388,22 +390,8 @@ describe('SAM runBuild', () => { | |
verifyCorrectDependencyCall() | ||
}) | ||
|
||
it('[entry: appbuilder node] with default flags should instantiate correct process in terminal', async () => { | ||
const prompterTester = PrompterTester.init() | ||
.handleQuickPick('Specify parameter source for build', async (quickPick) => { | ||
// Need sometime to wait for the template to search for template file | ||
await quickPick.untilReady() | ||
|
||
assert.strictEqual(quickPick.items.length, 2) | ||
const items = quickPick.items | ||
assert.strictEqual(quickPick.items.length, 2) | ||
assert.deepStrictEqual(items[0], { data: ParamsSource.Specify, label: 'Specify build flags' }) | ||
assert.deepStrictEqual(items[1].label, 'Use default values') | ||
quickPick.acceptItem(quickPick.items[1]) | ||
}) | ||
.build() | ||
|
||
// Invoke sync command from command palette | ||
it('[entry: appbuilder node] with default flags should instantiate correct process in terminal and show progress notification', async () => { | ||
const prompterTester = getPrompterTester() | ||
const expectedSamAppLocation = { | ||
workspaceFolder: workspaceFolder, | ||
samTemplateUri: templateFile, | ||
|
@@ -412,6 +400,10 @@ describe('SAM runBuild', () => { | |
|
||
await runBuild(new AppNode(expectedSamAppLocation)) | ||
|
||
getTestWindow() | ||
.getFirstMessage() | ||
.assertProgress(`Building SAM template at ${expectedSamAppLocation.samTemplateUri.path}`) | ||
|
||
assert.deepEqual(mockChildProcessClass.getCall(0).args, [ | ||
'sam-cli-path', | ||
[ | ||
|
@@ -437,6 +429,27 @@ describe('SAM runBuild', () => { | |
prompterTester.assertCallAll() | ||
}) | ||
|
||
it('[entry: appbuilder node] should throw an error when running two build processes in parallel for the same template', async () => { | ||
const prompterTester = getPrompterTester() | ||
const expectedSamAppLocation = { | ||
workspaceFolder: workspaceFolder, | ||
samTemplateUri: templateFile, | ||
projectRoot: projectRoot, | ||
} | ||
await assert.rejects( | ||
async () => { | ||
await runInParallel(expectedSamAppLocation) | ||
}, | ||
(e: any) => { | ||
assert.strictEqual(e instanceof ToolkitError, true) | ||
assert.strictEqual(e.message, 'This template is already being built') | ||
assert.strictEqual(e.code, 'BuildInProgress') | ||
return true | ||
} | ||
) | ||
prompterTester.assertCallAll(undefined, 2) | ||
}) | ||
|
||
it('[entry: command palette] use samconfig should instantiate correct process in terminal', async () => { | ||
const samconfigFile = vscode.Uri.file(await testFolder.write('samconfig.toml', samconfigCompleteData)) | ||
|
||
|
@@ -551,3 +564,23 @@ describe('SAM runBuild', () => { | |
}) | ||
}) | ||
}) | ||
|
||
async function runInParallel(samLocation: SamAppLocation): Promise<[SamBuildResult, SamBuildResult]> { | ||
return Promise.all([runBuild(new AppNode(samLocation)), runBuild(new AppNode(samLocation))]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I worry this would cause some flakiness as the two build is run at same time. There might be a race condition that both Build run |
||
} | ||
|
||
function getPrompterTester() { | ||
return PrompterTester.init() | ||
.handleQuickPick('Specify parameter source for build', async (quickPick) => { | ||
// Need sometime to wait for the template to search for template file | ||
await quickPick.untilReady() | ||
|
||
assert.strictEqual(quickPick.items.length, 2) | ||
const items = quickPick.items | ||
assert.strictEqual(quickPick.items.length, 2) | ||
assert.deepStrictEqual(items[0], { data: ParamsSource.Specify, label: 'Specify build flags' }) | ||
assert.deepStrictEqual(items[1].label, 'Use default values') | ||
quickPick.acceptItem(quickPick.items[1]) | ||
}) | ||
.build() | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
{ | ||
"type": "Bug Fix", | ||
"description": "SAM build: prevent running multiple build processes for the same template" | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we also add a cancel button to it? you can refer the implementation here:
aws-toolkit-vscode/packages/core/src/shared/utilities/cliUtils.ts
Lines 235 to 238 in 5bfb867