mirror of https://github.com/pulumi/pulumi.git
140 lines
4.9 KiB
TypeScript
140 lines
4.9 KiB
TypeScript
// Copyright 2016-2022, Pulumi Corporation.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
import * as log from "../log";
|
|
import * as state from "./state";
|
|
|
|
/**
|
|
* debugPromiseLeaks can be set to enable promises leaks debugging.
|
|
*
|
|
* @internal
|
|
*/
|
|
export const debugPromiseLeaks: boolean = !!process.env.PULUMI_DEBUG_PROMISE_LEAKS;
|
|
|
|
/**
|
|
* leakDetectorScheduled is true when the promise leak detector is scheduled for
|
|
* process exit.
|
|
*/
|
|
let leakDetectorScheduled: boolean = false;
|
|
|
|
/**
|
|
* @internal
|
|
*/
|
|
export function leakedPromises(): [Set<Promise<any>>, string] {
|
|
const localStore = state.getStore();
|
|
const leaked = localStore.leakCandidates;
|
|
const promisePlural = leaked.size === 1 ? "promise was" : "promises were";
|
|
const message =
|
|
leaked.size === 0
|
|
? ""
|
|
: `The Pulumi runtime detected that ${leaked.size} ${promisePlural} still active\n` +
|
|
"at the time that the process exited. There are a few ways that this can occur:\n" +
|
|
" * Not using `await` or `.then` on a Promise returned from a Pulumi API\n" +
|
|
" * Introducing a cyclic dependency between two Pulumi Resources\n" +
|
|
" * A bug in the Pulumi Runtime\n" +
|
|
"\n" +
|
|
"Leaving promises active is probably not what you want. If you are unsure about\n" +
|
|
"why you are seeing this message, re-run your program " +
|
|
"with the `PULUMI_DEBUG_PROMISE_LEAKS`\n" +
|
|
"environment variable. The Pulumi runtime will then print out additional\n" +
|
|
"debug information about the leaked promises.";
|
|
|
|
if (debugPromiseLeaks) {
|
|
for (const leak of leaked) {
|
|
console.error("Promise leak detected:");
|
|
console.error(promiseDebugString(leak));
|
|
}
|
|
}
|
|
|
|
localStore.leakCandidates = new Set();
|
|
return [leaked, message];
|
|
}
|
|
|
|
/**
|
|
* @internal
|
|
*/
|
|
export function promiseDebugString(p: Promise<any>): string {
|
|
return `CONTEXT(${(<any>p)._debugId}): ${(<any>p)._debugCtx}\n` + `STACK_TRACE:\n` + `${(<any>p)._debugStackTrace}`;
|
|
}
|
|
|
|
let promiseId = 0;
|
|
|
|
/**
|
|
* Optionally wraps a promise with some goo to make it easier to debug common
|
|
* problems.
|
|
*
|
|
* @internal
|
|
*/
|
|
export function debuggablePromise<T>(p: Promise<T>, ctx: any): Promise<T> {
|
|
const localStore = state.getStore();
|
|
// Whack some stack onto the promise. Leave them non-enumerable to avoid awkward rendering.
|
|
Object.defineProperty(p, "_debugId", { writable: true, value: promiseId });
|
|
Object.defineProperty(p, "_debugCtx", { writable: true, value: ctx });
|
|
Object.defineProperty(p, "_debugStackTrace", { writable: true, value: new Error().stack });
|
|
|
|
promiseId++;
|
|
|
|
if (!leakDetectorScheduled) {
|
|
process.on("exit", (code: number) => {
|
|
// Only print leaks if we're exiting normally. Otherwise, it could be a crash, which of
|
|
// course yields things that look like "leaks".
|
|
//
|
|
// process.exitCode is undefined unless set, in which case it's the exit code that was
|
|
// passed to process.exit.
|
|
if ((process.exitCode === undefined || process.exitCode === 0) && !log.hasErrors()) {
|
|
const [leaks, message] = leakedPromises();
|
|
if (leaks.size === 0) {
|
|
// No leaks - proceed with the exit.
|
|
return;
|
|
}
|
|
|
|
// If we haven't opted-in to the debug error message, print a more user-friendly message.
|
|
if (!debugPromiseLeaks) {
|
|
console.error(message);
|
|
}
|
|
|
|
// Fail the deployment if we leaked any promises.
|
|
process.exitCode = 1;
|
|
}
|
|
});
|
|
leakDetectorScheduled = true;
|
|
}
|
|
|
|
// Add this promise to the leak candidates list, and schedule it for removal if it resolves.
|
|
localStore.leakCandidates.add(p);
|
|
return p
|
|
.then((val: any) => {
|
|
localStore.leakCandidates.delete(p);
|
|
return val;
|
|
})
|
|
.catch((err: any) => {
|
|
localStore.leakCandidates.delete(p);
|
|
err.promise = p;
|
|
throw err;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Produces a string from an error, conditionally including additional
|
|
* diagnostics.
|
|
*
|
|
* @internal
|
|
*/
|
|
export function errorString(err: Error): string {
|
|
if (err.stack) {
|
|
return err.stack;
|
|
}
|
|
return err.toString();
|
|
}
|