Fix a hang in nodejs remote components when an error is thrown within an apply (#7365)
This commit is contained in:
parent
3cdfbf2a71
commit
c37cbc998b
|
@ -22,3 +22,6 @@
|
|||
|
||||
- [multilang/python] - Fix nested module generation.
|
||||
[#7353](https://github.com/pulumi/pulumi/pull/7353)
|
||||
|
||||
- [multilang/nodejs] - Fix a hang when an error is thrown within an apply in a remote component.
|
||||
[#7365](https://github.com/pulumi/pulumi/pull/7365)
|
||||
|
|
1
Makefile
1
Makefile
|
@ -63,6 +63,7 @@ test_build:: $(SUB_PROJECTS:%=%_install)
|
|||
cd tests/integration/construct_component_unknown/testcomponent-go && go build -o pulumi-resource-testcomponent
|
||||
cd tests/integration/component_provider_schema/testcomponent && yarn install && yarn link @pulumi/pulumi && yarn run tsc
|
||||
cd tests/integration/component_provider_schema/testcomponent-go && go build -o pulumi-resource-testcomponent
|
||||
cd tests/integration/construct_component_error_apply/testcomponent && yarn install && yarn link @pulumi/pulumi && yarn run tsc
|
||||
|
||||
test_all:: build test_build $(SUB_PROJECTS:%=%_install)
|
||||
cd pkg && $(GO_TEST) ${PROJECT_PKGS}
|
||||
|
|
|
@ -284,6 +284,10 @@
|
|||
WorkingDirectory="$(TestsDirectory)\integration\construct_component_unknown\testcomponent" />
|
||||
<Exec Command="yarn link @pulumi/pulumi"
|
||||
WorkingDirectory="$(TestsDirectory)\integration\construct_component_unknown\testcomponent" />
|
||||
<Exec Command="yarn install"
|
||||
WorkingDirectory="$(TestsDirectory)\integration\construct_component_error_apply\testcomponent" />
|
||||
<Exec Command="yarn link @pulumi/pulumi"
|
||||
WorkingDirectory="$(TestsDirectory)\integration\construct_component_error_apply\testcomponent" />
|
||||
</Target>
|
||||
|
||||
<Target Name="TestBuild">
|
||||
|
@ -297,6 +301,7 @@
|
|||
<Exec Command="go build -o pulumi-resource-testcomponent.exe" WorkingDirectory="$(TestsDirectory)\integration\component_provider_schema\testcomponent-go" />
|
||||
<Exec Command="yarn run tsc" WorkingDirectory="$(TestsDirectory)\integration\construct_component_unknown\testcomponent" />
|
||||
<Exec Command="go build -o pulumi-resource-testcomponent.exe" WorkingDirectory="$(TestsDirectory)\integration\construct_component_unknown\testcomponent-go" />
|
||||
<Exec Command="yarn run tsc" WorkingDirectory="$(TestsDirectory)\integration\construct_component_error_apply\testcomponent" />
|
||||
|
||||
<!-- Install pulumi SDK into the venv managed by pipenv. -->
|
||||
<Exec Command="pipenv run pip install -e ."
|
||||
|
|
|
@ -37,13 +37,15 @@ const statusproto = require("../proto/status_pb.js");
|
|||
class Server implements grpc.UntypedServiceImplementation {
|
||||
readonly engineAddr: string;
|
||||
readonly provider: Provider;
|
||||
readonly uncaughtErrors: Set<Error>;
|
||||
|
||||
/** Queue of construct calls. */
|
||||
constructQueue = Promise.resolve();
|
||||
|
||||
constructor(engineAddr: string, provider: Provider) {
|
||||
constructor(engineAddr: string, provider: Provider, uncaughtErrors: Set<Error>) {
|
||||
this.engineAddr = engineAddr;
|
||||
this.provider = provider;
|
||||
this.uncaughtErrors = uncaughtErrors;
|
||||
}
|
||||
|
||||
// Satisfy the grpc.UntypedServiceImplementation interface.
|
||||
|
@ -267,6 +269,20 @@ class Server implements grpc.UntypedServiceImplementation {
|
|||
}
|
||||
|
||||
async constructImpl(call: any, callback: any): Promise<void> {
|
||||
// given that construct calls are serialized, we can attach an uncaught handler to pick up exceptions
|
||||
// in underlying user code. When we catch the error, we need to respond to the gRPC request with the error
|
||||
// to avoid a hang.
|
||||
const uncaughtHandler = (err: Error) => {
|
||||
if (!this.uncaughtErrors.has(err)) {
|
||||
this.uncaughtErrors.add(err);
|
||||
}
|
||||
// bubble the uncaught error in the user code back and terminate the outstanding gRPC request.
|
||||
callback(err, undefined);
|
||||
};
|
||||
process.on("uncaughtException", uncaughtHandler);
|
||||
// @ts-ignore 'unhandledRejection' will almost always invoke uncaughtHandler with an Error. so
|
||||
// just suppress the TS strictness here.
|
||||
process.on("unhandledRejection", uncaughtHandler);
|
||||
try {
|
||||
const req: any = call.request;
|
||||
const type = req.getType();
|
||||
|
@ -357,6 +373,10 @@ class Server implements grpc.UntypedServiceImplementation {
|
|||
} catch (e) {
|
||||
console.error(`${e}: ${e.stack}`);
|
||||
callback(e, undefined);
|
||||
} finally {
|
||||
// remove these uncaught handlers that are specific to this gRPC callback context
|
||||
process.off("uncaughtException", uncaughtHandler);
|
||||
process.off("unhandledRejection", uncaughtHandler);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -477,7 +497,7 @@ export async function main(provider: Provider, args: string[]) {
|
|||
const server = new grpc.Server({
|
||||
"grpc.max_receive_message_length": runtime.maxRPCMessageSize,
|
||||
});
|
||||
server.addService(provrpc.ResourceProviderService, new Server(engineAddr, provider));
|
||||
server.addService(provrpc.ResourceProviderService, new Server(engineAddr, provider, uncaughtErrors));
|
||||
const port: number = await new Promise<number>((resolve, reject) => {
|
||||
server.bindAsync(`0.0.0.0:0`, grpc.ServerCredentials.createInsecure(), (err, p) => {
|
||||
if (err) {
|
||||
|
|
3
tests/integration/construct_component_error_apply/nodejs/.gitignore
vendored
Normal file
3
tests/integration/construct_component_error_apply/nodejs/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
/.pulumi/
|
||||
/bin/
|
||||
/node_modules/
|
|
@ -0,0 +1,3 @@
|
|||
name: construct_component_nodejs_error_apply
|
||||
description: A program that constructs remote component resources.
|
||||
runtime: nodejs
|
|
@ -0,0 +1,19 @@
|
|||
// Copyright 2016-2021, Pulumi Corporation. All rights reserved.
|
||||
|
||||
import * as pulumi from "@pulumi/pulumi";
|
||||
|
||||
interface ComponentArgs {
|
||||
foo: pulumi.Input<string>;
|
||||
}
|
||||
|
||||
export class Component extends pulumi.ComponentResource {
|
||||
public readonly foo!: pulumi.Output<string>;
|
||||
|
||||
constructor(name: string, args: ComponentArgs, opts?: pulumi.ComponentResourceOptions) {
|
||||
const inputs: any = {};
|
||||
inputs["foo"] = args.foo;
|
||||
|
||||
super("testcomponent:index:Component", name, inputs, opts, true);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
// Copyright 2016-2021, Pulumi Corporation. All rights reserved.
|
||||
|
||||
import { Component } from "./component";
|
||||
|
||||
const componentA = new Component("a", {foo: "bar"});
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"name": "steps",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"typescript": "^3.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@pulumi/pulumi": "latest"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
// Copyright 2016-2021, Pulumi Corporation. All rights reserved.
|
||||
|
||||
import * as pulumi from "@pulumi/pulumi";
|
||||
import * as provider from "@pulumi/pulumi/provider";
|
||||
|
||||
class Component extends pulumi.ComponentResource {
|
||||
public readonly foo: pulumi.Output<string>;
|
||||
|
||||
constructor(name: string, foo: pulumi.Input<string>, opts?: pulumi.ComponentResourceOptions) {
|
||||
super("testcomponent:index:Component", name, {}, opts);
|
||||
|
||||
this.foo = pulumi.output(foo);
|
||||
|
||||
this.registerOutputs({
|
||||
foo: this.foo,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class Provider implements provider.Provider {
|
||||
public readonly version = "0.0.1";
|
||||
|
||||
construct(name: string, type: string, inputs: pulumi.Inputs,
|
||||
options: pulumi.ComponentResourceOptions): Promise<provider.ConstructResult> {
|
||||
if (type != "testcomponent:index:Component") {
|
||||
throw new Error(`unknown resource type ${type}`);
|
||||
}
|
||||
|
||||
const foo = pulumi.output("").apply(a => {
|
||||
throw new Error("intentional error from within an apply");
|
||||
return a;
|
||||
});
|
||||
|
||||
|
||||
const component = new Component(name, foo);
|
||||
return Promise.resolve({
|
||||
urn: component.urn,
|
||||
state: {
|
||||
foo: component.foo
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function main(args: string[]) {
|
||||
return provider.main(new Provider(), args);
|
||||
}
|
||||
|
||||
main(process.argv.slice(2));
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"name": "pulumi-resource-testcomponent",
|
||||
"main": "index.js",
|
||||
"devDependencies": {
|
||||
"typescript": "^3.0.0",
|
||||
"@types/node": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@pulumi/pulumi": "latest"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/bash
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
||||
node $SCRIPT_DIR/bin $@
|
|
@ -0,0 +1,4 @@
|
|||
@echo off
|
||||
setlocal
|
||||
set SCRIPT_DIR=%~dp0
|
||||
@node "%SCRIPT_DIR%/bin" %*
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"outDir": "bin",
|
||||
"target": "es2016",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"declaration": true,
|
||||
"sourceMap": false,
|
||||
"stripInternal": true,
|
||||
"experimentalDecorators": true,
|
||||
"pretty": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitReturns": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"files": [
|
||||
"index.ts",
|
||||
]
|
||||
}
|
|
@ -1017,3 +1017,31 @@ func TestComponentProviderSchemaNode(t *testing.T) {
|
|||
}
|
||||
testComponentProviderSchema(t, path)
|
||||
}
|
||||
|
||||
// Test throwing an error within an apply in a remote component written in nodejs.
|
||||
// The provider should return the error and shutdown gracefully rather than hanging.
|
||||
func TestConstructNodeErrorApply(t *testing.T) {
|
||||
dir := "construct_component_error_apply"
|
||||
componentDir := "testcomponent"
|
||||
|
||||
stderr := &bytes.Buffer{}
|
||||
expectedError := "intentional error from within an apply"
|
||||
|
||||
opts := &integration.ProgramTestOptions{
|
||||
Env: []string{pathEnv(t, filepath.Join(dir, componentDir))},
|
||||
Dir: filepath.Join(dir, "nodejs"),
|
||||
Dependencies: []string{"@pulumi/pulumi"},
|
||||
Quick: true,
|
||||
NoParallel: true,
|
||||
Stderr: stderr,
|
||||
ExpectFailure: true,
|
||||
ExtraRuntimeValidation: func(t *testing.T, stackInfo integration.RuntimeValidationStackInfo) {
|
||||
output := stderr.String()
|
||||
assert.Contains(t, output, expectedError)
|
||||
},
|
||||
}
|
||||
|
||||
t.Run(componentDir, func(t *testing.T) {
|
||||
integration.ProgramTest(t, opts)
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue