@adpt/core
Version:
AdaptJS core library
896 lines • 36.6 kB
JavaScript
;
/*
* Copyright 2019 Unbounded Systems, LLC
*
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const utils_1 = require("@adpt/utils");
const async_lock_1 = tslib_1.__importDefault(require("async-lock"));
const debug_1 = tslib_1.__importDefault(require("debug"));
const graphlib_1 = require("graphlib");
const lodash_1 = require("lodash");
const p_queue_1 = tslib_1.__importDefault(require("p-queue"));
const p_timeout_1 = tslib_1.__importDefault(require("p-timeout"));
const util_1 = require("util");
const dom_1 = require("../dom");
const error_1 = require("../error");
const handle_1 = require("../handle");
const jsx_1 = require("../jsx");
const deploy_types_1 = require("./deploy_types");
const deploy_types_private_1 = require("./deploy_types_private");
const deployed_when_queue_1 = require("./deployed_when_queue");
const relation_utils_1 = require("./relation_utils");
const relations_1 = require("./relations");
const status_tracker_1 = require("./status_tracker");
const debugExecute = debug_1.default("adapt:deploy:execute");
const debugExecuteDetail = debug_1.default("adapt:detail:deploy:execute");
async function createExecutionPlan(options) {
const { actions, builtElements, diff } = options;
const plan = new ExecutionPlanImpl(options);
diff.added.forEach((e) => plan.addElem(e, deploy_types_1.DeployStatus.Deployed));
diff.commonNew.forEach((e) => plan.addElem(e, deploy_types_1.DeployStatus.Deployed));
diff.deleted.forEach((e) => plan.addElem(e, deploy_types_1.DeployStatus.Destroyed));
builtElements.forEach((e) => plan.addElem(e, deploy_types_1.DeployStatus.Deployed));
actions.forEach((a) => plan.addAction(a));
return plan;
}
exports.createExecutionPlan = createExecutionPlan;
function getWaitInfo(goalStatus, e, helpers) {
const hand = handle_1.isHandle(e) ? e : e.props.handle;
const elem = hand.mountedOrig;
if (elem === undefined)
throw new error_1.InternalError("element has no mountedOrig!");
if (elem === null)
throw new error_1.ElementNotInDom();
const dependsOn = elem.dependsOn(goalStatus, helpers);
if (dependsOn && !deploy_types_1.isDependsOn(dependsOn)) {
throw new utils_1.UserError(`Component '${elem.componentName}' dependsOn ` +
`method returned a value that is not a DependsOn object. ` +
`[Element id: ${elem.id}] returned: ${util_1.inspect(dependsOn)}`);
}
const wi = {
description: dependsOn ? dependsOn.description : elem.componentName,
deployedWhen: (gs) => elem.deployedWhen(gs, helpers),
};
if (dependsOn)
wi.dependsOn = dependsOn;
return wi;
}
exports.getWaitInfo = getWaitInfo;
class ExecutionPlanImpl {
constructor(options) {
this.graph = new graphlib_1.Graph({ compound: true });
this.nextWaitId = 0;
this.waitInfoIds = new WeakMap();
this.complete = new Map();
this.getId = (obj, create = false) => {
const id = this.getIdInternal(obj, create);
if (!id)
throw new Error(`ID not found (${idOrObjInfo(obj)})`);
return id;
};
this.getNode = (idOrObj) => {
const node = this.getNodeInternal(idOrObj);
if (!node)
throw new Error(`Node not found (${idOrObjInfo(idOrObj)})`);
return node;
};
this.hasNode = (idOrObj) => {
return this.getNodeInternal(idOrObj) != null;
};
/*
* Class-internal methods
*/
this.getIdInternal = (obj, create = false) => {
const elId = (e) => "E:" + e.id;
const wiId = (w) => {
let id = this.waitInfoIds.get(w);
if (!id) {
if (!create)
return undefined;
id = "W:" + this.nextWaitId++;
this.waitInfoIds.set(w, id);
}
return id;
};
if (jsx_1.isMountedElement(obj))
return elId(obj);
if (deploy_types_private_1.isWaitInfo(obj))
return wiId(obj);
if (jsx_1.isMountedElement(obj.element))
return elId(obj.element);
if (deploy_types_private_1.isWaitInfo(obj.waitInfo))
return wiId(obj.waitInfo);
throw new error_1.InternalError(`Invalid object in getId (${obj})`);
};
this.getNodeInternal = (idOrObj) => {
const id = typeof idOrObj === "string" ? idOrObj :
this.getIdInternal(idOrObj);
if (!id)
return undefined;
return this.graph.node(id) || this.complete.get(id);
};
this.getEdgeInternal = (idOrObj1, idOrObj2) => {
const n1 = this.getNodeInternal(idOrObj1);
const n2 = this.getNodeInternal(idOrObj2);
if (!n1 || !n2)
return undefined;
const id1 = this.getIdInternal(n1);
const id2 = this.getIdInternal(n2);
if (!id1 || !id2)
return undefined;
if (!this.graph.hasEdge(id1, id2))
return undefined;
// TODO: Should probably just store this info on the edge itself
if (n1.hardDeps && n1.hardDeps.has(n2))
return { hard: true };
return {};
};
this.goalStatus = options.goalStatus;
this.deployment = options.deployment;
this.deployOpID = options.deployOpID;
this.helpers = new DeployHelpersFactory(this, this.deployment);
}
/*
* Public interfaces
*/
check() {
const cycleGroups = graphlib_1.alg.findCycles(this.graph);
if (cycleGroups.length > 0) {
const cycles = cycleGroups.map(printCycleGroups).join("\n");
if (debugExecute.enabled) {
debugExecute(`Execution plan dependencies:\n${this.print()}`);
}
throw new utils_1.UserError(`There are circular dependencies present in this deployment:\n${cycles}`);
}
}
/*
* Semi-private interfaces (for use by this file)
*/
addElem(el, goalStatus) {
const element = toBuiltElem(el);
if (!element || this.hasNode(element))
return;
const helpers = this.helpers.create(element);
const waitInfo = getWaitInfo(goalStatus, element, helpers);
const node = { element, goalStatus, waitInfo };
this.addNode(node);
this.addWaitInfo(node, goalStatus);
}
addAction(action) {
if (action.type === deploy_types_1.ChangeType.none)
return undefined;
const node = {
goalStatus: changeTypeToGoalStatus(action.type),
waitInfo: {
description: action.detail,
action: action.act,
actingFor: action.changes,
activeAction: true,
deployedWhen: () => true,
logAction: true,
}
};
this.addNode(node);
action.changes.forEach((c) => {
if (c.type === deploy_types_1.ChangeType.none)
return;
this.addElem(c.element, changeTypeToGoalStatus(c.type));
const elNode = this.getNode(c.element);
elNode.waitInfo.activeAction = true;
elNode.waitInfo.description = action.detail;
const leader = this.groupLeader(c.element);
if (leader === node)
return;
if (leader) {
throw new Error(`More than one Action referenced Element '${c.element.id}'. ` +
`An Element may only be affected by one Action`);
}
this.setGroup(c.element, node);
});
return node;
}
addWaitInfo(nodeOrWI, goalStatus) {
let node;
let waitInfo;
if (deploy_types_private_1.isWaitInfo(nodeOrWI)) {
node = { goalStatus, waitInfo: nodeOrWI };
waitInfo = nodeOrWI;
this.addNode(node);
}
else {
node = nodeOrWI;
waitInfo = node.waitInfo;
}
if (waitInfo.dependsOn) {
const hands = relation_utils_1.relatedHandles(waitInfo.dependsOn);
hands.forEach((h) => {
if (!h.associated) {
// TODO: Add info about the handle, like traceback for
// where it was created.
throw new utils_1.UserError(`A Component dependsOn method returned a DependsOn ` +
`object '${waitInfo.description}' that contains ` +
`a Handle that is not associated with any Element`);
}
const el = toBuiltElem(h);
if (el) {
// If el has already been added, its goal
// status won't change.
this.addElem(el, goalStatus);
this.addEdge(node, el);
}
});
}
return node;
}
/**
* Now used only in unit test. Should eventually be removed.
*/
updateElemWaitInfo(refresh = false) {
this.nodes.forEach((n) => {
const el = n.element;
if (el == null)
return;
if (n.waitInfo != null && !refresh)
throw new error_1.InternalError(`Expected EPNode.waitInfo to be null`);
const helpers = this.helpers.create(el);
n.waitInfo = getWaitInfo(n.goalStatus, el, helpers);
if (!deploy_types_private_1.isEPNodeWI(n))
return;
this.addWaitInfo(n, n.goalStatus);
});
}
addNode(node) {
if (this.hasNode(node))
return;
this.graph.setNode(this.getId(node, true), node);
}
addHardDep(obj, dependsOn) {
this.addEdge(obj, dependsOn, true);
}
removeNode(node) {
const id = this.getId(node);
const preds = this.predecessors(node);
preds.forEach((p) => this.removeHardDepInternal(p, node));
this.graph.removeNode(id);
this.complete.set(id, node);
}
predecessors(n) {
const preds = this.graph.predecessors(this.getId(n));
if (preds == null)
throw new error_1.InternalError(`Requested node that's not in graph id=${this.getId(n)}`);
return preds.map(this.getNode);
}
successors(n) {
const succs = this.graph.successors(this.getId(n));
if (succs == null)
throw new error_1.InternalError(`Requested node that's not in graph id=${this.getId(n)}`);
return succs.map(this.getNode);
}
groupLeader(n) {
const leader = this.graph.parent(this.getId(n));
return leader ? this.getNode(leader) : undefined;
}
groupFollowers(n) {
const fols = this.graph.children(this.getId(n)) || [];
return fols.map(this.getNode);
}
setGroup(n, leader) {
const nId = this.getId(n);
n = this.getNode(n);
const oldLeader = this.groupLeader(n);
if (oldLeader)
throw new error_1.InternalError(`Node '${nId}' already in group '${this.getId(oldLeader)}'`);
// When n becomes part of a group, the group leader adopts the
// dependencies of all the other members of the group. For example,
// starting with deps:
// el1 -> el2
// Now call setGroup(el1, lead) and the result should be:
// el1 -> lead -> el2
this.successors(n).forEach((succ) => {
const e = this.getEdgeInternal(n, succ);
if (!e) {
throw new error_1.InternalError(`Internal consistency check failed. ` +
`node has a successor, but no edge`);
}
this.removeEdgeInternal(n, succ);
this.addEdgeInternal(leader, succ, e.hard === true);
});
this.addEdgeInternal(n, leader, true);
this.graph.setParent(nId, this.getId(leader));
}
get nodes() {
return this.graph.nodes().map(this.getNode);
}
get elems() {
return this.nodes
.map((n) => n.element)
.filter(utils_1.notNull);
}
get leaves() {
return this.graph.sinks().map(this.getNode);
}
toDependencies() {
const detail = (n) => {
const w = n.waitInfo;
if (w)
return w.description;
else if (n.element)
return n.element.id;
return "unknown";
};
const getDeps = (node, id) => {
const hardDeps = node.hardDeps ?
[...node.hardDeps].map((n) => this.getId(n)) : [];
const hardSet = new Set(hardDeps);
const succIds = this.graph.successors(id);
if (!succIds)
throw new error_1.InternalError(`id '${id}' not found`);
const deps = succIds.map((sId) => {
const isHard = hardSet.delete(sId);
return { id: sId, type: isHard ? "hard" : "soft" };
});
if (hardSet.size !== 0) {
throw new error_1.InternalError(`Internal consistency check failed: ` +
`not all hardDeps are successors`);
}
const entry = { detail: detail(node), deps };
if (node.element)
entry.elementId = node.element.id;
return entry;
};
const ret = {};
const ids = graphlib_1.alg.isAcyclic(this.graph) ?
graphlib_1.alg.topsort(this.graph) : this.graph.nodes();
// Insert starting with leaves for a more human-readable ordering
for (let i = ids.length - 1; i >= 0; i--) {
const id = ids[i];
const node = this.getNode(id);
ret[id] = getDeps(node, id);
}
return ret;
}
print() {
const epDeps = this.toDependencies();
const depIDs = Object.keys(epDeps);
if (depIDs.length === 0)
return "<empty>";
const succs = (id) => {
const list = epDeps[id] && epDeps[id].deps;
if (!list || list.length === 0)
return " <none>";
return list.map((s) => ` ${name(s.id)} [${s.type[0]}]`).join("\n");
};
const name = (id) => {
const w = this.getNode(id).waitInfo;
if (w)
id += ` (${w.description})`;
return id;
};
const printDeps = (ids) => ids
.map((id) => ` ${name(id)}\n${succs(id)}`);
const byGoal = {};
const insert = (id, goal) => {
const l = byGoal[goal] || [];
l.push(id);
byGoal[goal] = l;
};
for (const id of depIDs) {
insert(id, this.getNode(id).goalStatus);
}
const lines = [];
for (const goal of Object.keys(byGoal).sort()) {
let gName = goal;
try {
gName = deploy_types_1.goalToInProgress(goal);
}
catch (e) { /* */ }
lines.push(`${gName}:`, ...printDeps(byGoal[goal]));
}
return lines.join("\n");
}
/**
* The direction of the dependency has to be reversed for Destroy
* so that things are destroyed in "reverse order" (actually by
* walking the graph in the opposite order). But a single graph
* contains some things that are being Deployed and some that are
* being Destroyed.
* The arguments to the function (obj, dependsOn) identify two EPNodes.
* Each of those two EPNodes could have goalStatus Deployed or Destroyed,
* so there are 4 possible combinations:
* A) Deployed, Deployed
* This is the simple case where `dependsOn` should be Deployed
* before `obj` is Deployed. The edge is `obj` -> `dependsOn`.
* B) Destroyed, Destroyed
* Also simple. If `dependsOn` must be Deployed before `obj`, then
* it's reversed for Destroyed and `obj` must be Destroyed before
* `dependsOn`. The edge is `dependsOn` -> `obj`.
* C) Destroyed, Deployed
* The valid way this can happen when used with an actual old DOM
* and new DOM is that `obj` is from the old DOM. The new DOM does
* not contain this node and therefore *cannot* have a dependency
* on it. The dependency here can be ignored safely. No edge.
* D) Deployed, Destroyed
* This doesn't make sense right now because there's not really a
* way for a "living" component in the new DOM to get a reference
* to something being deleted from the old DOM. This is currently
* an error.
*/
addEdge(obj, dependsOn, hardDep = false) {
obj = this.getNode(obj);
dependsOn = this.getNode(dependsOn);
let a;
let b;
const goals = `${obj.goalStatus},${dependsOn.goalStatus}`;
switch (goals) {
case "Deployed,Deployed":
a = obj;
b = dependsOn;
break;
case "Destroyed,Destroyed":
a = dependsOn;
b = obj;
break;
case "Destroyed,Deployed": return; // Intentionally no edge
case "Deployed,Destroyed":
default:
throw new error_1.InternalError(`Unable to create dependency for ` +
`invalid goal pair '${goals}'`);
}
// If a is in a group, all outbound dependencies are attached to
// the group leader (and "a" will already have a dependency on the
// group leader from when it joined the group).
const leader = this.groupLeader(a);
// If there's a leader, re-make the check for dissimilar goalStatus
// but with the leader.
if (leader && leader.goalStatus !== b.goalStatus) {
if (leader.goalStatus === deploy_types_1.GoalStatus.Destroyed) {
throw new error_1.InternalError(`Unable to create dependency for ` +
`leader being Destroyed on dependency being Deployed`);
}
return; // Intentionally no edge
}
this.addEdgeInternal(leader || a, b, hardDep);
}
addEdgeInternal(obj, dependsOn, hardDep) {
const objId = this.getId(obj);
this.graph.setEdge(objId, this.getId(dependsOn));
if (hardDep)
this.addHardDepInternal(this.getNode(objId), this.getNode(dependsOn));
}
removeEdgeInternal(obj, dependsOn) {
const objId = this.getId(obj);
this.graph.removeEdge(objId, this.getId(dependsOn));
this.removeHardDepInternal(this.getNode(objId), this.getNode(dependsOn));
}
addHardDepInternal(obj, dependsOn) {
if (obj.hardDeps == null)
obj.hardDeps = new Set();
obj.hardDeps.add(dependsOn);
}
removeHardDepInternal(obj, dependsOn) {
if (obj.hardDeps != null)
obj.hardDeps.delete(dependsOn);
}
}
exports.ExecutionPlanImpl = ExecutionPlanImpl;
function isExecutionPlanImpl(val) {
return lodash_1.isObject(val) && val instanceof ExecutionPlanImpl;
}
exports.isExecutionPlanImpl = isExecutionPlanImpl;
function debugExecId(id, ...args) {
debugExecute(`* ${id.padEnd(26)}`, ...args);
}
function debugExecDetailId(id, ...args) {
debugExecuteDetail(`* ${id.padEnd(26)}`, ...args);
}
const defaultExecuteOptions = {
concurrency: Infinity,
dryRun: false,
pollDelayMs: 1000,
timeoutMs: 0,
};
async function execute(options) {
const opts = Object.assign({}, defaultExecuteOptions, options);
const plan = opts.plan;
const timeoutTime = opts.timeoutMs ? Date.now() + opts.timeoutMs : 0;
if (!isExecutionPlanImpl(plan))
throw new error_1.InternalError(`plan is not an ExecutionPlanImpl`);
const deployOpID = plan.deployOpID;
const nodeStatus = await status_tracker_1.createStatusTracker({
deployment: plan.deployment,
deployOpID,
dryRun: opts.dryRun,
goalStatus: plan.goalStatus,
nodes: plan.nodes,
taskObserver: opts.taskObserver,
});
plan.helpers.nodeStatus = nodeStatus;
//TODO: Remove?
debugExecute(`\nExecution plan:\n${plan.print()}`);
try {
while (true) {
const stepNum = nodeStatus.stepID ? nodeStatus.stepID.deployStepNum : "DR";
const stepStr = `${deployOpID}.${stepNum}`;
debugExecute(`\n\n-----------------------------\n\n` +
`**** Starting execution step ${stepStr}`);
await executePass(Object.assign({}, opts, { nodeStatus, timeoutTime }));
const { stateChanged } = await opts.processStateUpdates();
const ret = await nodeStatus.complete(stateChanged);
debugExecute(`**** execution step ${stepStr} status: ${ret.deploymentStatus}\nSummary:`, util_1.inspect(ret), "\n", nodeStatus.debug(plan.getId), "\n-----------------------------\n\n");
// Keep polling until we're done or the state changes, which means
// we should do a re-build.
if (ret.deploymentStatus === deploy_types_1.DeployOpStatus.StateChanged ||
deploy_types_1.isFinalStatus(ret.deploymentStatus)) {
debugExecute(`**** Execution completed`);
return ret;
}
await utils_1.sleep(opts.pollDelayMs);
}
}
catch (err) {
err = utils_1.ensureError(err);
opts.logger.error(`Deploy operation failed: ${err.message}`);
let stateChanged = false;
try {
const upd = await opts.processStateUpdates();
if (upd.stateChanged)
stateChanged = true;
}
catch (err2) {
err2 = utils_1.ensureError(err2);
opts.logger.error(`Error processing state updates during error handling: ${err2.message}`);
}
debugExecute(`**** Execution failed:`, util_1.inspect(err));
if (err.name === "TimeoutError") {
//TODO : Mark all un-deployed as timed out
for (const n of plan.nodes) {
await nodeStatus.set(n, deploy_types_1.DeployStatus.Failed, err);
}
return nodeStatus.complete(stateChanged);
}
else {
throw err;
}
}
}
exports.execute = execute;
async function executePass(opts) {
const { dryRun, logger, nodeStatus, plan } = opts;
if (!isExecutionPlanImpl(plan))
throw new error_1.InternalError(`plan is not an ExecutionPlanImpl`);
const locks = new async_lock_1.default();
const queue = new p_queue_1.default({ concurrency: opts.concurrency });
let stopExecuting = false;
const dwQueue = new deployed_when_queue_1.DeployedWhenQueue(debugExecDetailId);
const fatalErrors = [];
// If an action is on behalf of some Elements, those nodes take on
// the status of the action in certain cases.
const signalActingFor = async (node, stat, err) => {
const w = node.waitInfo;
if (!w || !w.actingFor || !shouldNotifyActingFor(stat))
return;
await Promise.all(w.actingFor.map(async (c) => {
const n = plan.getNode(c.element);
if (!nodeStatus.isActive(n))
return;
const s = err ? err :
stat === deploy_types_1.DeployStatusExt.Deploying ? deploy_types_1.DeployStatusExt.ProxyDeploying :
stat === deploy_types_1.DeployStatusExt.Destroying ? deploy_types_1.DeployStatusExt.ProxyDestroying :
stat;
await updateStatus(n, s, c.detail);
}));
};
const signalPreds = async (n, stat) => {
if (!deploy_types_1.isFinalStatus(stat))
return;
plan.predecessors(n).forEach(queueRun);
};
const fatalError = (err) => {
stopExecuting = true;
fatalErrors.push(utils_1.ensureError(err));
};
const queueRun = (n) => queue.add(() => run(n)).catch(fatalError);
const run = async (n) => {
const id = plan.getId(n);
await locks.acquire(id, () => runLocked(n, id));
};
const runLocked = async (n, id) => {
let errorLogged = false;
try {
if (stopExecuting)
return debugExecId(id, `TIMED OUT: Can't start task`);
const stat = nodeStatus.get(n);
if (deploy_types_1.isFinalStatus(stat))
return debugExecId(id, `Already complete`);
if (!(isWaiting(stat) || deploy_types_1.isInProgress(stat))) {
throw new error_1.InternalError(`Unexpected node status ${stat}: ${id}`);
}
if (!dependenciesMet(n, id))
return;
debugExecId(id, ` Dependencies met`);
const w = n.waitInfo;
if (w) {
if (!deploy_types_1.isInProgress(stat)) {
await updateStatus(n, deploy_types_1.goalToInProgress(n.goalStatus)); // now in progress
if (w.action) {
debugExecId(id, `ACTION: Doing ${w.description}`);
if (w.logAction)
logger.info(`Doing ${w.description}`);
try {
if (!dryRun)
await w.action();
}
catch (err) {
logger.error(`--Error while ${w.description}\n${err}\n----------`);
errorLogged = true;
throw err;
}
}
}
const wStat = await w.deployedWhen(n.goalStatus);
if (wStat !== true) {
const statStr = relation_utils_1.waitStatusToString(wStat);
debugExecId(id, `NOT COMPLETE: ${w.description}: ${statStr}`);
nodeStatus.output(n, statStr);
dwQueue.enqueue(n, id, wStat);
return;
}
debugExecId(id, `COMPLETE: ${w.description}`);
}
else {
debugExecId(id, ` No wait info`);
// Go through normal state transition to
// trigger correct downstream events to TaskObservers.
await updateStatus(n, deploy_types_1.goalToInProgress(n.goalStatus));
}
await updateStatus(n, n.goalStatus);
plan.removeNode(n);
}
catch (err) {
err = utils_1.ensureError(err);
debugExecId(id, `FAILED: ${err}`);
await updateStatus(n, err);
if (!errorLogged) {
logger.error(`Error while ${deploy_types_1.goalToInProgress(n.goalStatus).toLowerCase()} ` +
`${nodeDescription(n)}: ${utils_1.formatUserError(err)}`);
}
if (err.name === "InternalError")
throw err;
}
finally {
if (n.element)
dwQueue.completed(n.element, queueRun);
}
};
const updateStatus = async (n, stat, description) => {
if (stopExecuting)
return false;
const { err, deployStatus } = lodash_1.isError(stat) ?
{ err: stat, deployStatus: deploy_types_1.DeployStatus.Failed } :
{ err: undefined, deployStatus: stat };
debugExecId(plan.getId(n), `STATUS: ${deployStatus}${err ? ": " + err : ""}`);
const changed = await nodeStatus.set(n, deployStatus, err, description);
if (changed) {
await signalActingFor(n, deployStatus, err);
await signalPreds(n, deployStatus);
}
return changed;
};
const mkIdStr = (ids) => ids.join(" > ");
const softDepsReady = (n, ids) => {
// If this node is being Deployed, just look at its own WaitInfo
if (n.goalStatus === deploy_types_1.DeployStatus.Deployed) {
return waitIsReady(n, false, ids);
}
// But if the node is being Destroyed, we instead evaluate all of our
// successors' WaitInfos, each in the inverse direction.
const succs = plan.successors(n);
debugExecDetailId(mkIdStr(ids), ` Evaluating: ${succs.length} successors`);
for (const s of succs) {
// TODO: There probably needs to be a check here comparing
// goalStatus for s and n, similar to addEdge.
const sId = plan.getId(s);
if (!waitIsReady(s, true, [...ids, sId]))
return false;
}
return true;
};
const waitIsReady = (n, invert, ids) => {
const w = n.waitInfo;
let dep = w && w.dependsOn;
if (invert && dep)
dep = relation_utils_1.relationInverse(dep);
if (debugExecute.enabled) {
const idStr = mkIdStr(ids);
const desc = !w ? "no soft dep" :
dep ? `soft dep (${w.description}) - Relation${invert ? " (inverted)" : ""}: ${relation_utils_1.relationToString(dep)}` :
`no soft dep (${w.description})`;
debugExecDetailId(idStr, ` Evaluating: ${desc}`);
if (!dep)
return true;
const relStatus = relation_utils_1.relationIsReadyStatus(dep);
debugExecId(idStr, ` Relation status:`, relStatus === true ? "READY" : relStatus);
return relStatus === true;
}
return dep ? relation_utils_1.relationIsReady(dep) : true;
};
const dependenciesMet = (n, id) => {
const hardDeps = n.hardDeps || new Set();
debugExecDetailId(id, ` Evaluating: ${hardDeps.size} hard deps`);
for (const d of hardDeps) {
if (!nodeIsDeployed(d, id, nodeStatus)) {
debugExecId(id, `NOTYET: hard deps`);
return false;
}
}
if (!softDepsReady(n, [id])) {
debugExecId(id, `NOTYET: soft dep`);
return false;
}
const followers = plan.groupFollowers(n);
debugExecDetailId(id, ` Evaluating: ${followers.length} followers`);
for (const f of followers) {
const fStat = nodeStatus.get(f);
const fId = plan.getId(f);
if (!isWaiting(fStat)) {
throw new error_1.InternalError(`Invalid status ${fStat} for follower ${fId}`);
}
if (!softDepsReady(f, [id, fId])) {
debugExecId(id, `NOTYET: followers`);
return false;
}
}
return true;
};
/*
* Main execute code path
*/
try {
// Queue the leaf nodes that have no dependencies
plan.leaves.forEach(queueRun);
// Then wait for all promises to resolve
let pIdle = queue.onIdle();
if (opts.timeoutMs && opts.timeoutTime) {
const msg = `Deploy operation timed out after ${opts.timeoutMs / 1000} seconds`;
const timeLeft = opts.timeoutTime - Date.now();
if (timeLeft <= 0)
throw new p_timeout_1.default.TimeoutError(msg);
pIdle = p_timeout_1.default(pIdle, timeLeft, msg);
}
await pIdle;
}
catch (err) {
fatalError(err);
}
if (fatalErrors.length > 1)
throw new utils_1.MultiError(fatalErrors);
else if (fatalErrors.length === 1)
throw fatalErrors[0];
}
exports.executePass = executePass;
function shouldNotifyActingFor(status) {
switch (status) {
case deploy_types_1.DeployStatus.Deploying:
case deploy_types_1.DeployStatus.Destroying:
//case DeployStatus.Retrying:
case deploy_types_1.DeployStatus.Failed:
return true;
default:
return false;
}
}
function isWaiting(stat) {
return (stat === deploy_types_1.DeployStatusExt.Waiting ||
stat === deploy_types_1.DeployStatusExt.ProxyDeploying ||
stat === deploy_types_1.DeployStatusExt.ProxyDestroying);
}
function changeTypeToGoalStatus(ct) {
switch (ct) {
case deploy_types_1.ChangeType.none:
case deploy_types_1.ChangeType.create:
case deploy_types_1.ChangeType.modify:
case deploy_types_1.ChangeType.replace:
return deploy_types_1.DeployStatus.Deployed;
case deploy_types_1.ChangeType.delete:
return deploy_types_1.DeployStatus.Destroyed;
default:
throw new error_1.InternalError(`Bad ChangeType '${ct}'`);
}
}
function printCycleGroups(group) {
if (group.length < 1)
throw new error_1.InternalError(`Cycle group with no members`);
const c = [...group, group[0]];
return " " + c.join(" -> ");
}
function toBuiltElemOrWaitInfo(val) {
return deploy_types_private_1.isWaitInfo(val) ? val : toBuiltElem(val);
}
function toBuiltElem(val) {
if (jsx_1.isMountedElement(val)) {
if (val.built())
return val;
val = val.props.handle;
}
if (!handle_1.isHandle(val)) {
throw new Error(`Attempt to convert an invalid object to Element or WaitInfo: ${util_1.inspect(val)}`);
}
const elem = val.nextMounted((el) => jsx_1.isMountedElement(el) && el.built());
if (elem === undefined)
throw new error_1.InternalError("Handle has no built Element!");
return elem;
}
function nodeIsDeployed(n, id, tracker) {
const sStat = tracker.get(n);
if (sStat === n.goalStatus)
return true; // Dependency met
if (sStat === deploy_types_1.DeployStatusExt.Failed) {
throw new utils_1.UserError(`A dependency failed to deploy successfully`);
}
if (isWaiting(sStat) || deploy_types_1.isInProgress(sStat))
return false;
throw new error_1.InternalError(`Invalid status ${sStat} for ${id}`);
}
function nodeDescription(n) {
if (n.waitInfo)
return n.waitInfo.description;
if (n.element)
return `${n.element.componentName} (id=${n.element.id})`;
return "Unknown node";
}
function idOrObjInfo(idOrObj) {
return typeof idOrObj === "string" ? idOrObj :
deploy_types_private_1.isWaitInfo(idOrObj) ? idOrObj.description :
jsx_1.isMountedElement(idOrObj) ? idOrObj.id :
idOrObj.element ? idOrObj.element.id :
idOrObj.waitInfo ? idOrObj.waitInfo.description :
"unknown";
}
class DeployHelpersFactory {
constructor(plan, deployment) {
this.plan = plan;
this.nodeStatus_ = null;
this.isDeployed = (d) => {
if (deploy_types_1.isRelation(d))
return relation_utils_1.relationIsReady(d);
const elOrWait = toBuiltElemOrWaitInfo(d);
if (elOrWait === null)
return true; // Handle built to null - null is deployed
const n = this.plan.getNode(elOrWait);
return nodeIsDeployed(n, this.plan.getId(n), this.nodeStatus);
};
this.makeDependsOn = (current) => (hands) => {
const toEdge = (h) => relations_1.Edge(current, h, this.isDeployed);
return relations_1.And(...utils_1.toArray(hands).map(toEdge));
};
this.create = (elem) => ({
elementStatus: this.elementStatus,
isDeployed: this.isDeployed,
dependsOn: this.makeDependsOn(elem.props.handle),
});
this.elementStatus = dom_1.makeElementStatus();
}
get nodeStatus() {
if (this.nodeStatus_ == null) {
throw new Error(`Cannot get nodeStatus except during plan execution`);
}
return this.nodeStatus_;
}
set nodeStatus(t) {
this.nodeStatus_ = t;
}
}
//# sourceMappingURL=execution_plan.js.map