[sdk/nodejs] Add multiple VM contexts support to closure serialization (#6648)

This commit is contained in:
Daniel Sokolowski 2021-04-12 18:13:47 +02:00 committed by GitHub
parent d82242882e
commit ee2f65510b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 66 additions and 40 deletions

View file

@ -17,6 +17,9 @@
### Improvements
- [sdk/nodejs] Add support for multiple V8 VM contexts in closure serialization.
[#6648](https://github.com/pulumi/pulumi/pull/6648)
- [cli] Add option to print absolute rather than relative dates in stack history
[#6742](https://github.com/pulumi/pulumi/pull/6742)

View file

@ -16,6 +16,7 @@
import * as inspector from "inspector";
import * as util from "util";
import * as vm from "vm";
import * as v8Hooks from "./v8Hooks";
/** @internal */
@ -105,45 +106,74 @@ type PostSession<TMethod, TParams, TReturn> = {
type EvaluationSession = PostSession<"Runtime.evaluate", inspector.Runtime.EvaluateParameterType, inspector.Runtime.EvaluateReturnType>;
type GetPropertiesSession = PostSession<"Runtime.getProperties", inspector.Runtime.GetPropertiesParameterType, inspector.Runtime.GetPropertiesReturnType>;
type CallFunctionSession = PostSession<"Runtime.callFunctionOn", inspector.Runtime.CallFunctionOnParameterType, inspector.Runtime.CallFunctionOnReturnType>;
type ContextSession = {
post(method: "Runtime.disable" | "Runtime.enable", callback?: (err: Error | null) => void): void;
once(event: "Runtime.executionContextCreated", listener: (message: inspector.InspectorNotification<inspector.Runtime.ExecutionContextCreatedEventDataType>) => void): void;
};
type InflightContext = {
contextId: number;
functions: Record<string, any>;
currentFunctionId: number;
calls: Record<string, any>;
currentCallId: number;
};
// Isolated singleton context accessible from the inspector.
// Used instead of `global` object to support executions with multiple V8 vm contexts as, e.g., done by Jest.
const inflightContext = createContext();
async function createContext(): Promise<InflightContext> {
const context: InflightContext = {
contextId: 0,
functions: {},
currentFunctionId: 0,
calls: {},
currentCallId: 0,
};
const session = <ContextSession>await v8Hooks.getSessionAsync();
const post = util.promisify(session.post);
// Create own context with known context id and functionsContext as `global`
await post.call(session, "Runtime.enable");
const contextIdAsync = new Promise<number>(resolve => {
session.once("Runtime.executionContextCreated", event => {
resolve(event.params.context.id);
});
});
vm.createContext(context);
context.contextId = await contextIdAsync;
await post.call(session, "Runtime.disable");
return context;
}
async function getRuntimeIdForFunctionAsync(func: Function): Promise<inspector.Runtime.RemoteObjectId> {
// In order to get information about an object, we need to put it in a well known location so
// that we can call Runtime.evaluate and find it. To do this, we just make a special map on the
// 'global' object, and map from a unique-id to that object. We then call Runtime.evaluate with
// an expression that then points to that unique-id in that global object. The runtime will
// then find the object and give us back an internal id for it. We can then query for
// information about the object through that internal id.
// that we can call Runtime.evaluate and find it. To do this, we use a special map on the
// 'global' object of a vm context only used for this purpose, and map from a unique-id to that
// object. We then call Runtime.evaluate with an expression that then points to that unique-id
// in that global object. The runtime will then find the object and give us back an internal id
// for it. We can then query for information about the object through that internal id.
//
// Note: the reason for the mapping object and the unique-id we create is so that we don't run
// into any issues when being called asynchronously. We don't want to place the object in a
// location that might be overwritten by another call while we're asynchronously waiting for our
// original call to complete.
//
// We also lazily initialize this in case pulumi has been loaded through another module and has
// already initialize this global state.
const globalAny = <any>global;
if (!globalAny.__inflightFunctions) {
globalAny.__inflightFunctions = {};
globalAny.__currentFunctionId = 0;
}
// Place the function in a unique location off of the global object.
const currentFunctionName = "id" + globalAny.__currentFunctionId++;
globalAny.__inflightFunctions[currentFunctionName] = func;
const session = <EvaluationSession>await v8Hooks.getSessionAsync();
const post = util.promisify(session.post);
// Place the function in a unique location
const context = await inflightContext;
const currentFunctionName = "id" + context.currentFunctionId++;
context.functions[currentFunctionName] = func;
const contextId = context.contextId;
const expression = `functions.${currentFunctionName}`;
try {
const session = <EvaluationSession>await v8Hooks.getSessionAsync();
const post = util.promisify(session.post);
const expression = `global.__inflightFunctions.${currentFunctionName}`;
// This cast will become unnecessary when we move to TS 3.1.6 or above. In that version they
// support typesafe '.call' calls.
const retType = <inspector.Runtime.EvaluateReturnType>await post.call(
session, "Runtime.evaluate", { expression });
const retType = await post.call(session, "Runtime.evaluate", { contextId, expression });
if (retType.exceptionDetails) {
throw new Error(`Error calling "Runtime.evaluate(${expression})": ` + retType.exceptionDetails.text);
throw new Error(`Error calling "Runtime.evaluate(${expression})" on context ${contextId}: ` + retType.exceptionDetails.text);
}
const remoteObject = retType.result;
@ -158,7 +188,7 @@ async function getRuntimeIdForFunctionAsync(func: Function): Promise<inspector.R
return remoteObject.objectId;
}
finally {
delete globalAny.__inflightFunctions[currentFunctionName];
delete context.functions[currentFunctionName];
}
}
@ -188,20 +218,13 @@ async function getValueForObjectId(objectId: inspector.Runtime.RemoteObjectId):
// memory as the bound 'this' value. Inside that function declaration, we can then access
// 'this' and assign it to a unique-id in a well known mapping table we have set up. As above,
// the unique-id is to prevent any issues with multiple in-flight asynchronous calls.
//
// We also lazily initialize this in case pulumi has been loaded through another module and has
// already initialize this global state.
const globalAny = <any>global;
if (!globalAny.__inflightCalls) {
globalAny.__inflightCalls = {};
globalAny.__currentCallId = 0;
}
const session = <CallFunctionSession>await v8Hooks.getSessionAsync();
const post = util.promisify(session.post);
const context = await inflightContext;
// Get an id for an unused location in the global table.
const tableId = "id" + globalAny.__currentCallId++;
const tableId = "id" + context.currentCallId++;
// Now, ask the runtime to call a fictitious method on the scopes-array object. When it
// does, it will get the actual underlying value for the scopes array and bind it to the
@ -214,7 +237,7 @@ async function getValueForObjectId(objectId: inspector.Runtime.RemoteObjectId):
session, "Runtime.callFunctionOn", {
objectId,
functionDeclaration: `function () {
global.__inflightCalls["${tableId}"] = this;
calls["${tableId}"] = this;
}`,
});
@ -223,13 +246,13 @@ async function getValueForObjectId(objectId: inspector.Runtime.RemoteObjectId):
+ retType.exceptionDetails.text);
}
if (!globalAny.__inflightCalls.hasOwnProperty(tableId)) {
if (!context.calls.hasOwnProperty(tableId)) {
throw new Error(`Value was not stored into table after calling "Runtime.callFunctionOn(${objectId})"`);
}
// Extract value and clear our table entry.
const val = globalAny.__inflightCalls[tableId];
delete globalAny.__inflightCalls[tableId];
const val = context.calls[tableId];
delete context.calls[tableId];
return val;
}