UNPKG

@adpt/core

Version:
896 lines 36.6 kB
"use strict"; /* * 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