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:
parent
f9085bf799
commit
d4aa5fe20d
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Reference in a new issue