Switch to 'console.log' for our hang warning. Add warning to synchronous StackReference calls. (#3456)

Codepaths which could result in a hang will print a message to the console indicating the problem, along with a link to documentation on how to restructure code to best address it.

`StackReference.getOutputSync` and `requireOutputSync` have been deprecated as they may cause hangs on some combinations of Node and certain OS platforms. `StackReference.getOutput` and `requireOutput` should be used instead.
This commit is contained in:
CyrusNajmabadi 2019-11-19 15:51:14 -05:00 committed by Pat Gavlin
parent f9085bf799
commit d4aa5fe20d
4 changed files with 156 additions and 37 deletions

View file

@ -28,6 +28,15 @@ CHANGELOG
- Support for node 13.x, building with gcc 8 and newer. [#3512] (https://github.com/pulumi/pulumi/pull/3512)
- Codepaths which could result in a hang will print a message to the console indicating the problem, along with a link
to documentation on how to restructure code to best address it.
### Compatibility
- `StackReference.getOutputSync` and `requireOutputSync` are deprecated as they may cause hangs on
some combinations of Node and certain OS platforms. `StackReference.getOutput` and `requireOutput`
should be used instead.
## 1.5.2 (2019-11-13)
- `pulumi policy publish` now determines the Policy Pack name from the Policy Pack, and the

View file

@ -142,20 +142,28 @@ func (p *builtinProvider) Read(urn resource.URN, id resource.ID,
}, resource.StatusOK, nil
}
const readStackOutputs = "pulumi:pulumi:readStackOutputs"
const readStackResourceOutputs = "pulumi:pulumi:readStackResourceOutputs"
func (p *builtinProvider) Invoke(tok tokens.ModuleMember,
args resource.PropertyMap) (resource.PropertyMap, []plugin.CheckFailure, error) {
if tok != readStackResourceOutputs {
switch tok {
case readStackOutputs:
outs, err := p.readStackReference(args)
if err != nil {
return nil, nil, err
}
return outs, nil, nil
case readStackResourceOutputs:
outs, err := p.readStackResourceOutputs(args)
if err != nil {
return nil, nil, err
}
return outs, nil, nil
default:
return nil, nil, errors.Errorf("unrecognized function name: '%v'", tok)
}
outs, err := p.readStackResourceOutputs(args)
if err != nil {
return nil, nil, err
}
return outs, nil, nil
}
func (p *builtinProvider) StreamInvoke(

View file

@ -18,6 +18,7 @@ import * as grpc from "grpc";
import { AsyncIterable } from "@pulumi/query/interfaces";
import * as asset from "../asset";
import { Config } from "../config";
import { InvokeOptions } from "../invoke";
import * as log from "../log";
import { Inputs, Output } from "../output";
@ -68,18 +69,37 @@ const providerproto = require("../proto/provider_pb.js");
*/
export function invoke(tok: string, props: Inputs, opts: InvokeOptions = {}): Promise<any> {
if (opts.async) {
// Use specifically requested async invoking. Respect that.
// User specifically requested async invoking. Respect that.
return invokeAsync(tok, props, opts);
}
const config = new Config("pulumi");
const noSyncCalls = config.getBoolean("noSyncCalls");
if (noSyncCalls) {
// User globally disabled sync invokes.
return invokeAsync(tok, props, opts);
}
const syncResult = invokeSync(tok, props, opts);
// Wrap the synchronous value in a Promise view as well so that consumers can treat it
// either as the real value or something they can use as a Promise.
return createLiftedPromise(syncResult);
}
/**
* Invokes the provided token *synchronously* no matter what.
* @internal
*/
export function invokeSync<T>(tok: string, props: Inputs, opts: InvokeOptions = {}): T {
const syncInvokes = tryGetSyncInvokes();
if (!syncInvokes) {
// We weren't launched from a pulumi CLI that supports sync-invokes. Let the user know they
// should update and fall back to synchronously blocking on the async invoke.
return invokeFallbackToAsync(tok, props, opts);
return invokeFallbackToAsync<T>(tok, props, opts);
}
return invokeSync(tok, props, opts, syncInvokes);
return invokeSyncWorker<T>(tok, props, opts, syncInvokes);
}
export async function streamInvoke(
@ -133,10 +153,8 @@ export async function streamInvoke(
}
}
export function invokeFallbackToAsync(tok: string, props: Inputs, opts: InvokeOptions): Promise<any> {
const asyncResult = invokeAsync(tok, props, opts);
const syncResult = utils.promiseResult(asyncResult);
return createLiftedPromise(syncResult);
export function invokeFallbackToAsync<T>(tok: string, props: Inputs, opts: InvokeOptions): T {
return utils.promiseResult(invokeAsync(tok, props, opts));
}
async function invokeAsync(tok: string, props: Inputs, opts: InvokeOptions): Promise<any> {
@ -183,7 +201,7 @@ async function invokeAsync(tok: string, props: Inputs, opts: InvokeOptions): Pro
}
}
function invokeSync(tok: string, props: any, opts: InvokeOptions, syncInvokes: SyncInvokes): Promise<any> {
function invokeSyncWorker<T>(tok: string, props: any, opts: InvokeOptions, syncInvokes: SyncInvokes): T {
const label = `Invoking function: tok=${tok} synchronously`;
log.debug(label + (excessiveDebugOutput ? `, props=${JSON.stringify(props)}` : ``));
@ -213,7 +231,7 @@ function invokeSync(tok: string, props: any, opts: InvokeOptions, syncInvokes: S
const resp = providerproto.InvokeResponse.deserializeBinary(new Uint8Array(respBytes));
const resultValue = deserializeResponse(tok, resp);
return createLiftedPromise(resultValue);
return resultValue;
function getProviderRefSync() {
const provider = getProvider(tok, opts);
@ -223,8 +241,10 @@ function invokeSync(tok: string, props: any, opts: InvokeOptions, syncInvokes: S
}
if (provider.__registrationId === undefined) {
log.warn(
`Synchronous call made to "${tok}" with an unregistered provider.
// Have to do an explicit console.log here as the call to utils.promiseResult may hang
// node, and that may prevent our normal logging calls from making it back to the user.
console.log(
`Synchronous call made to "${tok}" with an unregistered provider. This is now deprecated and may cause the program to hang.
For more details see: https://www.pulumi.com/docs/troubleshooting/#synchronous-call`);
utils.promiseResult(ProviderResource.register(provider));
}

View file

@ -14,6 +14,7 @@
import { all, Input, Output, output } from "./output";
import { CustomResource, CustomResourceOptions } from "./resource";
import * as invoke from "./runtime/invoke";
import { promiseResult } from "./utils";
/**
@ -36,6 +37,15 @@ export class StackReference extends CustomResource {
*/
public readonly secretOutputNames!: Output<string[]>;
// Values we stash to support the getOutputSync and requireOutputSync calls without
// having to go through the async values above.
private readonly stackReferenceName: Input<string>;
private syncOutputsSupported: boolean | undefined;
private syncName: string | undefined;
private syncOutputs: Record<string, any> | undefined;
private syncSecretOutputNames: string[] | undefined;
/**
* Create a StackReference resource with the given unique name, arguments, and options.
*
@ -48,11 +58,15 @@ export class StackReference extends CustomResource {
constructor(name: string, args?: StackReferenceArgs, opts?: CustomResourceOptions) {
args = args || {};
const stackReferenceName = args.name || name;
super("pulumi:pulumi:StackReference", name, {
name: args.name || name,
name: stackReferenceName,
outputs: undefined,
secretOutputNames: undefined,
}, { ...opts, id: args.name || name });
}, { ...opts, id: stackReferenceName });
this.stackReferenceName = stackReferenceName;
}
/**
@ -61,7 +75,7 @@ export class StackReference extends CustomResource {
* @param name The name of the stack output to fetch.
*/
public getOutput(name: Input<string>): Output<any> {
// Note that this is subltly different from "apply" here. A default "apply" will set the secret bit if any
// Note that this is subtly different from "apply" here. A default "apply" will set the secret bit if any
// of the inputs are a secret, and this.outputs is always a secret if it contains any secrets. We do this dance
// so we can ensure that the Output we return is not needlessly tainted as a secret.
const value = all([output(name), this.outputs]).apply(([n, os]) => os[n]);
@ -84,42 +98,110 @@ export class StackReference extends CustomResource {
}
/**
* Fetches the value promptly of the named stack output. May return undefined if the value is
* Fetches the value promptly of the named stack output. May return undefined if the value is
* not known for some reason.
*
* This operation is not supported (and will throw) if any exported values of the StackReference
* are secrets.
* This operation is not supported (and will throw) if the named stack output is a secret.
*
* @param name The name of the stack output to fetch.
*/
public getOutputSync(name: string): any {
const out = this.getOutput(name);
const isSecret = promiseResult(out.isSecret);
const [out, isSecret] = this.readOutputSync("getOutputSync", name, false /*required*/);
if (isSecret) {
throw new Error("Cannot call 'getOutputSync' if the referenced stack has secret outputs. Use 'getOutput' instead.");
throw new Error("Cannot call 'getOutputSync' if the referenced stack output is a secret. Use 'getOutput' instead.");
}
return promiseResult(out.promise());
return out;
}
/**
* Fetches the value promptly of the named stack output. Throws an error if the stack output is
* Fetches the value promptly of the named stack output. Throws an error if the stack output is
* not found.
*
* This operation is not supported (and will throw) if any exported values of the StackReference
* are secrets.
* This operation is not supported (and will throw) if the named stack output is a secret.
*
* @param name The name of the stack output to fetch.
*/
public requireOutputSync(name: string): any {
const out = this.requireOutput(name);
const isSecret = promiseResult(out.isSecret);
const [out, isSecret] = this.readOutputSync("requireOutputSync", name, true /*required*/);
if (isSecret) {
throw new Error("Cannot call 'requireOutputSync' if the referenced stack has secret outputs. Use 'requireOutput' instead.");
throw new Error("Cannot call 'requireOutputSync' if the referenced stack output is a secret. Use 'requireOutput' instead.");
}
return out;
}
private readOutputSync(callerName: string, outputName: string, required: boolean): [any, boolean] {
const [stackName, outputs, secretNames, supported] = this.readOutputsSync("requireOutputSync");
// If the synchronous readStackOutputs call is supported by the engine, use its results.
if (supported) {
if (required && !outputs.hasOwnProperty(outputName)) {
throw new Error(`Required output '${outputName}' does not exist on stack '${stackName}'.`);
}
return [outputs[outputName], secretNames.includes(outputName)];
}
return promiseResult(out.promise());
// Otherwise, fall back to promiseResult.
console.log(`StackReference.${callerName} may cause your program to hang. Please update to the latest version of the Pulumi CLI.
For more details see: https://www.pulumi.com/docs/troubleshooting/#stackreference-sync`);
const out = required ? this.requireOutput(outputName) : this.getOutput(outputName);
return [promiseResult(out.promise()), promiseResult(out.isSecret)];
}
private readOutputsSync(callerName: string): [string, Record<string, any>, string[], boolean] {
// See if we already attempted to read in the outputs synchronously. If so, just use those values.
if (this.syncOutputs) {
return [this.syncName!, this.syncOutputs, this.syncSecretOutputNames!, this.syncOutputsSupported!];
}
// We need to pass along our StackReference name to the engine so it knows what results to
// return. However, because we're doing this synchronously, we can only do this safely if
// the stack-reference name is synchronously known (i.e. it's a string and not a
// Promise/Output). If it is only asynchronously known, then warn the user and make an unsafe
// call to the deasync lib to get the name.
let stackName: string;
if (this.stackReferenceName instanceof Promise) {
// Have to do an explicit console.log here as the call to utils.promiseResult may hang
// node, and that may prevent our normal logging calls from making it back to the user.
console.log(
`Call made to StackReference.${callerName} with a StackReference with a Promise name. This is now deprecated and may cause the program to hang.
For more details see: https://www.pulumi.com/docs/troubleshooting/#stackreference-sync`);
stackName = promiseResult(this.stackReferenceName);
}
else if (Output.isInstance(this.stackReferenceName)) {
console.log(
`Call made to StackReference.${callerName} with a StackReference with an Output name. This is now deprecated and may cause the program to hang.
For more details see: https://www.pulumi.com/docs/troubleshooting/#stackreference-sync`);
stackName = promiseResult(this.stackReferenceName.promise());
}
else {
stackName = this.stackReferenceName;
}
try {
const res = invoke.invokeSync<ReadStackOutputsResult>(
"pulumi:pulumi:readStackOutputs", { name: stackName });
this.syncName = stackName;
this.syncOutputs = res.outputs;
this.syncSecretOutputNames = res.secretOutputNames;
this.syncOutputsSupported = true;
} catch {
this.syncOutputs = {};
this.syncOutputsSupported = false;
}
return [this.syncName!, this.syncOutputs, this.syncSecretOutputNames!, this.syncOutputsSupported];
}
}
// Shape of the result that the engine returns to us when we invoke 'pulumi:pulumi:readStackOutputs'
interface ReadStackOutputsResult {
name: string;
outputs: Record<string, any>;
secretOutputNames: string[];
}
/**