Skip to content
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

Open
wants to merge 8 commits into
base: master
Choose a base branch
from

Conversation

mbfreder
Copy link
Contributor

@mbfreder mbfreder commented Dec 6, 2024

Context

We currently allow multiple build processes to run at the same time for the same template.

Problem

When running a build, it’s possible to start a second build for the same project. This results in both builds failings

Solution

Prevent parallel builds on the same template


  • Treat all work as PUBLIC. Private feature/x branches will not be squash-merged at release time.
  • Your code changes must meet the guidelines in CONTRIBUTING.md.

License: I confirm that my contribution is made under the terms of the Apache 2.0 license.

@mbfreder mbfreder requested a review from a team as a code owner December 6, 2024 16:57
@mbfreder mbfreder changed the title fix(appBuilder): Prevent parallel build processes on the same template fix(appbuilder): Prevent parallel build processes on the same template Dec 6, 2024
*/
export function throwIfTemplateIsBeingBuilt(templatePath: string) {
if (isBuildInProgress(templatePath)) {
throw new ToolkitError('Template is already being built', { code: 'BuildInProgress' })
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's say "This template is already being built" to make sure users know they are trying to build the same template again, and that they can build different templates simultaneously.

@@ -108,6 +108,35 @@ export async function updateRecentResponse(
getLogger().warn(`sam: unable to save response at key "${key}": %s`, err)
}
}

const buildProcessMementoRootKey = 'samcli.build.processes'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's change this to something like

const BUILD_PROCESS_MEMENTO_ROOT_KEY = 'samcli.build.processes'; and maybe put it in the samutils if it makes sense to. We can also do the same with the 'global' as we should limit the amount of constant strings we have

Copy link
Contributor Author

@mbfreder mbfreder Dec 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The above format wouldn't work since eslint only allows camelCase or PascalCase formats. The constant is defined in the sam/utils file. Are you referring to a different utils file?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally, we'd put this in a constants.ts file, but if not we can disable eslint for this line. The convention is that constants should be in all caps.

}

export async function registerTemplateBuild(templatePath: string) {
await updateRecentResponse(buildProcessMementoRootKey, 'global', templatePath, 'true')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add error handling here to catch if something goes wrong in the process of registering a template build? I think currently, the error would just bubble up from the actual sam build command, but it could be the case that build command succeeds but the registering fails

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updateRecentResponse() already handles that by logging the error.

export async function updateRecentResponse(
    mementoRootKey: string,
    identifier: string,
    key: string,
    value: string | undefined
) {
    try {
        const root = globals.context.workspaceState.get(mementoRootKey, {} as Record<string, Record<string, string>>)
        await globals.context.workspaceState.update(mementoRootKey, {
            ...root,
            [identifier]: { ...root[identifier], [key]: value },
        })
    } catch (err) {
        getLogger().warn(`sam: unable to save response at key "${key}": %s`, err)
    }
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we capture the error so that we can see it in our Kibana dashboard? I don't think we see logger errors in our dashboard iirc

*/
export function throwIfTemplateIsBeingBuilt(templatePath: string) {
if (isBuildInProgress(templatePath)) {
throw new ToolkitError('Template is already being built', { code: 'BuildInProgress' })
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make the error code an enum? I think we had a class with different error codes already right, let's add this one in with them

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those are specific SAM CLI error patterns. I don't think it makes sense to add this one to them since this error doesn't come directly from SAM CLI.

@mbfreder mbfreder force-pushed the parallel-build branch 2 times, most recently from 654bf52 to 3471429 Compare December 10, 2024 04:52
* SPDX-License-Identifier: Apache-2.0
*/

export const BUILD_PROCESS_MEMENTO_ROOT_KEY = 'samcli.build.processes'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the standard we have in the toolkit is to use camal case, even for constants

@@ -0,0 +1,12 @@
/* eslint-disable header/header */
/* eslint-disable @typescript-eslint/naming-convention */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you shouldn't need these es-lint disables

@@ -1,3 +1,5 @@
/* eslint-disable header/header */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you shouldn't need these es-lint disables

* @Param templatePath The path to the template.yaml file
*/
function isBuildInProgress(templatePath: string): boolean {
return getRecentResponse(buildProcessMementoRootKey, globalIdentifier, templatePath) !== undefined
Copy link
Contributor

Choose a reason for hiding this comment

The 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 packages/core/src/shared/globalState.ts , except for "workspace" state. That makes it clear when callers are using this global state. And it collects the pattern in one place, instead of many spread-out functions.

Copy link
Member

@roger-zhangg roger-zhangg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left few comments, thanks for implementing these changes!

*/
export function throwIfTemplateIsBeingBuilt(templatePath: string) {
if (isBuildInProgress(templatePath)) {
throw new ToolkitError('This template is already being built', { code: 'BuildInProgress' })
Copy link
Member

@roger-zhangg roger-zhangg Dec 12, 2024

Choose a reason for hiding this comment

The 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 unregisterTemplateBuild or even toolkit maybe reload itself during the build process. In this case the user will never able to build the template again. One possible solution is to add a timeout to this cache. Like 2 minutes or 5 minutes. In this way, if the worst case happens ( build finished but we failed to update memeto) the user will still be able to build after time out is reached.

location: vscode.ProgressLocation.Notification,
},
async (progress) => {
progress.report({ message: `Building SAM template at ${params.template.uri.path}` })
Copy link
Member

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:

const progress = await showMessageWithCancel(
localize('AWS.cli.installProgress', 'Installing: {0} CLI', cliToInstall.name),
timeout
)

@@ -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))])
Copy link
Member

Choose a reason for hiding this comment

The 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 throwIfTemplateIsBeingBuilt both passes and then both run registerTemplateBuild and proceed to build. In this case the two build will be run together and cause test to fail. I would suggest we add a 0.05s wait time between them to avoid the unlikely but possible race condition.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants