@go-to-k/cdkd 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Binary file
package/dist/index.js CHANGED
@@ -6993,8 +6993,96 @@ var IAMRoleProvider = class {
6993
6993
  }
6994
6994
  };
6995
6995
 
6996
+ // src/deployment/dag-executor.ts
6997
+ var DagExecutor = class {
6998
+ nodes = /* @__PURE__ */ new Map();
6999
+ logger = getLogger().child("DagExecutor");
7000
+ add(node) {
7001
+ this.nodes.set(node.id, node);
7002
+ }
7003
+ has(id) {
7004
+ return this.nodes.has(id);
7005
+ }
7006
+ size() {
7007
+ return this.nodes.size;
7008
+ }
7009
+ values() {
7010
+ return this.nodes.values();
7011
+ }
7012
+ async execute(concurrency, fn, cancelled = () => false) {
7013
+ let active = 0;
7014
+ const errors = [];
7015
+ return new Promise((resolve4, reject) => {
7016
+ const dispatch = () => {
7017
+ let changed = true;
7018
+ while (changed) {
7019
+ changed = false;
7020
+ for (const node of this.nodes.values()) {
7021
+ if (node.state !== "pending")
7022
+ continue;
7023
+ const hasFailedDep = [...node.dependencies].some((depId) => {
7024
+ const dep = this.nodes.get(depId);
7025
+ return dep && (dep.state === "failed" || dep.state === "skipped");
7026
+ });
7027
+ if (hasFailedDep) {
7028
+ node.state = "skipped";
7029
+ changed = true;
7030
+ this.logger.debug(`Skipped ${node.id}: dependency failed or was skipped`);
7031
+ }
7032
+ }
7033
+ }
7034
+ const ready = [];
7035
+ for (const node of this.nodes.values()) {
7036
+ if (node.state !== "pending")
7037
+ continue;
7038
+ const depsReady = [...node.dependencies].every((depId) => {
7039
+ const dep = this.nodes.get(depId);
7040
+ return !dep || dep.state === "completed";
7041
+ });
7042
+ if (depsReady)
7043
+ ready.push(node);
7044
+ }
7045
+ if (!cancelled()) {
7046
+ for (const node of ready) {
7047
+ if (active >= concurrency)
7048
+ break;
7049
+ node.state = "running";
7050
+ active++;
7051
+ fn(node).then(() => {
7052
+ node.state = "completed";
7053
+ }).catch((error) => {
7054
+ node.state = "failed";
7055
+ errors.push({ id: node.id, error });
7056
+ }).finally(() => {
7057
+ active--;
7058
+ dispatch();
7059
+ });
7060
+ }
7061
+ }
7062
+ if (active === 0) {
7063
+ if (errors.length > 0) {
7064
+ reject(errors[0].error);
7065
+ return;
7066
+ }
7067
+ const stillPending = [...this.nodes.values()].some((n) => n.state === "pending");
7068
+ if (stillPending && !cancelled()) {
7069
+ const pending = [...this.nodes.values()].filter((n) => n.state === "pending").map((n) => n.id);
7070
+ reject(
7071
+ new Error(
7072
+ `Deadlock detected: ${pending.length} node(s) stuck with unresolvable dependencies (${pending.join(", ")})`
7073
+ )
7074
+ );
7075
+ return;
7076
+ }
7077
+ resolve4();
7078
+ }
7079
+ };
7080
+ dispatch();
7081
+ });
7082
+ }
7083
+ };
7084
+
6996
7085
  // src/deployment/deploy-engine.ts
6997
- import pLimit from "p-limit";
6998
7086
  var InterruptedError = class extends Error {
6999
7087
  constructor() {
7000
7088
  super("Deployment interrupted by user (Ctrl+C)");
@@ -7127,6 +7215,7 @@ var DeployEngine = class _DeployEngine {
7127
7215
  template,
7128
7216
  currentState,
7129
7217
  changes,
7218
+ dag,
7130
7219
  executionLevels,
7131
7220
  stackName,
7132
7221
  parameterValues,
@@ -7159,13 +7248,16 @@ var DeployEngine = class _DeployEngine {
7159
7248
  }
7160
7249
  }
7161
7250
  /**
7162
- * Execute deployment by processing resources in DAG order
7251
+ * Execute deployment by processing resources via event-driven DAG dispatch.
7163
7252
  *
7164
- * Important: DELETE operations are executed in reverse dependency order,
7165
- * while CREATE/UPDATE follow normal dependency order.
7166
- */
7167
- async executeDeployment(template, currentState, changes, executionLevels, stackName, parameterValues, conditions, currentEtag, progress) {
7168
- const limit = pLimit(this.options.concurrency);
7253
+ * - CREATE/UPDATE follow forward dependency order (a node starts as soon as
7254
+ * ALL of its dependencies are completed — does not wait for unrelated
7255
+ * siblings in the same "level")
7256
+ * - DELETE follows reverse dependency order (a node starts as soon as all
7257
+ * resources that depend ON it have finished deleting)
7258
+ */
7259
+ async executeDeployment(template, currentState, changes, dag, executionLevels, stackName, parameterValues, conditions, currentEtag, progress) {
7260
+ const concurrency = this.options.concurrency;
7169
7261
  const newResources = { ...currentState.resources };
7170
7262
  const actualCounts = { created: 0, updated: 0, deleted: 0, skipped: 0 };
7171
7263
  const completedOperations = [];
@@ -7196,32 +7288,36 @@ var DeployEngine = class _DeployEngine {
7196
7288
  Array.from(changes.entries()).filter(([_, change]) => change.changeType === "DELETE").map(([logicalId]) => logicalId)
7197
7289
  );
7198
7290
  try {
7199
- for (let levelIndex = 0; levelIndex < executionLevels.length; levelIndex++) {
7200
- if (this.interrupted) {
7201
- throw new InterruptedError();
7202
- }
7203
- const levelNodes = executionLevels[levelIndex];
7204
- if (!levelNodes)
7291
+ const createUpdateIds = [];
7292
+ for (const [id, change] of changes.entries()) {
7293
+ if (deleteChanges.has(id))
7205
7294
  continue;
7206
- const level = levelNodes.filter((id) => {
7207
- if (deleteChanges.has(id))
7208
- return false;
7209
- const change = changes.get(id);
7210
- return !!change && change.changeType !== "NO_CHANGE";
7211
- });
7212
- if (level.length === 0)
7295
+ if (change.changeType === "NO_CHANGE")
7213
7296
  continue;
7297
+ createUpdateIds.push(id);
7298
+ }
7299
+ if (createUpdateIds.length > 0) {
7214
7300
  this.logger.info(
7215
- `Level ${levelIndex + 1}/${executionLevels.length} (${level.length} resources)`
7301
+ `Deploying ${createUpdateIds.length} resource(s) (DAG: ${executionLevels.length} levels, max parallel: ${concurrency})`
7216
7302
  );
7217
- const results = await Promise.allSettled(
7218
- level.map(
7219
- (logicalId) => limit(async () => {
7220
- const change = changes.get(logicalId);
7221
- if (!change || change.changeType === "NO_CHANGE") {
7222
- this.logger.debug(`Skipping ${logicalId} (no change)`);
7223
- return;
7224
- }
7303
+ const createUpdateExecutor = new DagExecutor();
7304
+ const provisionable = new Set(createUpdateIds);
7305
+ for (const id of createUpdateIds) {
7306
+ const allDeps = this.dagBuilder.getDirectDependencies(dag, id);
7307
+ const deps = new Set(allDeps.filter((d) => provisionable.has(d)));
7308
+ createUpdateExecutor.add({
7309
+ id,
7310
+ dependencies: deps,
7311
+ state: "pending",
7312
+ data: changes.get(id)
7313
+ });
7314
+ }
7315
+ try {
7316
+ await createUpdateExecutor.execute(
7317
+ concurrency,
7318
+ async (node) => {
7319
+ const logicalId = node.id;
7320
+ const change = node.data;
7225
7321
  const previousState = currentState.resources[logicalId] ? { ...currentState.resources[logicalId] } : void 0;
7226
7322
  try {
7227
7323
  await this.provisionResource(
@@ -7248,30 +7344,36 @@ var DeployEngine = class _DeployEngine {
7248
7344
  properties: newResources[logicalId]?.properties
7249
7345
  });
7250
7346
  saveStateAfterResource(logicalId);
7251
- })
7252
- )
7253
- );
7254
- await saveChain;
7255
- const failures = results.filter((r) => r.status === "rejected");
7256
- if (failures.length > 0) {
7257
- throw failures[0].reason;
7347
+ },
7348
+ () => this.interrupted
7349
+ );
7350
+ } finally {
7351
+ await saveChain;
7352
+ }
7353
+ if (this.interrupted && this.hasPending(createUpdateExecutor)) {
7354
+ throw new InterruptedError();
7258
7355
  }
7259
7356
  }
7260
7357
  if (deleteChanges.size > 0) {
7261
7358
  this.logger.info(`Deleting ${deleteChanges.size} resource(s)`);
7262
- const deletionLevels = this.buildDeletionLevels(deleteChanges, currentState);
7263
- for (let levelIndex = 0; levelIndex < deletionLevels.length; levelIndex++) {
7264
- if (this.interrupted) {
7265
- throw new InterruptedError();
7266
- }
7267
- const level = deletionLevels[levelIndex];
7268
- if (level.length === 0)
7269
- continue;
7270
- const deleteResults = await Promise.allSettled(
7271
- level.map(
7272
- (logicalId) => limit(async () => {
7273
- const change = changes.get(logicalId);
7274
- const previousState = currentState.resources[logicalId] ? { ...currentState.resources[logicalId] } : void 0;
7359
+ const deleteDeps = this.buildDeletionDependencies(deleteChanges, currentState);
7360
+ const deleteExecutor = new DagExecutor();
7361
+ for (const id of deleteChanges) {
7362
+ deleteExecutor.add({
7363
+ id,
7364
+ dependencies: deleteDeps.get(id) ?? /* @__PURE__ */ new Set(),
7365
+ state: "pending",
7366
+ data: changes.get(id)
7367
+ });
7368
+ }
7369
+ try {
7370
+ await deleteExecutor.execute(
7371
+ concurrency,
7372
+ async (node) => {
7373
+ const logicalId = node.id;
7374
+ const change = node.data;
7375
+ const previousState = currentState.resources[logicalId] ? { ...currentState.resources[logicalId] } : void 0;
7376
+ try {
7275
7377
  await this.provisionResource(
7276
7378
  logicalId,
7277
7379
  change,
@@ -7283,23 +7385,25 @@ var DeployEngine = class _DeployEngine {
7283
7385
  actualCounts,
7284
7386
  progress
7285
7387
  );
7286
- completedOperations.push({
7287
- logicalId,
7288
- changeType: "DELETE",
7289
- resourceType: change.resourceType,
7290
- previousState
7291
- });
7292
- saveStateAfterResource(logicalId);
7293
- })
7294
- )
7388
+ } catch (provisionError) {
7389
+ this.interrupted = true;
7390
+ throw provisionError;
7391
+ }
7392
+ completedOperations.push({
7393
+ logicalId,
7394
+ changeType: "DELETE",
7395
+ resourceType: change.resourceType,
7396
+ previousState
7397
+ });
7398
+ saveStateAfterResource(logicalId);
7399
+ },
7400
+ () => this.interrupted
7295
7401
  );
7402
+ } finally {
7296
7403
  await saveChain;
7297
- const deleteFailures = deleteResults.filter(
7298
- (r) => r.status === "rejected"
7299
- );
7300
- if (deleteFailures.length > 0) {
7301
- throw deleteFailures[0].reason;
7302
- }
7404
+ }
7405
+ if (this.interrupted && this.hasPending(deleteExecutor)) {
7406
+ throw new InterruptedError();
7303
7407
  }
7304
7408
  }
7305
7409
  } catch (error) {
@@ -7393,12 +7497,12 @@ var DeployEngine = class _DeployEngine {
7393
7497
  * - UPDATE → update back to previous properties
7394
7498
  * - DELETE → cannot rollback (resource already deleted), log warning
7395
7499
  *
7396
- * Resources created in the same DAG level may have dependencies between them
7397
- * (e.g., IAM Policy depends on IAM Role). When rolling back CREATEs (deleting),
7398
- * dependent resources must be deleted before their dependencies. This method
7399
- * sorts CREATE rollback operations using dependency information from state,
7400
- * then processes UPDATE/DELETE rollbacks, and finally processes sorted CREATE
7401
- * rollback deletions.
7500
+ * Resources completed concurrently in the dispatcher may have dependencies
7501
+ * between them (e.g., IAM Policy depends on IAM Role). When rolling back
7502
+ * CREATEs (deleting), dependent resources must be deleted before their
7503
+ * dependencies. This method sorts CREATE rollback operations using dependency
7504
+ * information from state, then processes UPDATE/DELETE rollbacks, and finally
7505
+ * processes sorted CREATE rollback deletions.
7402
7506
  */
7403
7507
  async performRollback(completedOperations, stateResources, _stackName) {
7404
7508
  if (completedOperations.length === 0) {
@@ -7431,7 +7535,7 @@ var DeployEngine = class _DeployEngine {
7431
7535
  * Sort CREATE rollback operations so that resources depending on others
7432
7536
  * are deleted first (reverse dependency order).
7433
7537
  *
7434
- * Uses state dependencies to build deletion levels, similar to buildDeletionLevels.
7538
+ * Uses state dependencies to determine reverse-dependency order, similar to buildDeletionDependencies.
7435
7539
  */
7436
7540
  sortRollbackCreates(createOps, stateResources) {
7437
7541
  const opMap = /* @__PURE__ */ new Map();
@@ -7853,17 +7957,30 @@ var DeployEngine = class _DeployEngine {
7853
7957
  "AWS::EC2::SecurityGroup": ["AWS::EC2::SecurityGroupIngress", "AWS::EC2::SecurityGroupEgress"]
7854
7958
  };
7855
7959
  /**
7856
- * Build deletion levels from state dependencies (reverse topological order).
7857
- * Resources that are depended upon by others are deleted LAST.
7960
+ * Build a per-resource map of "must be deleted before me" dependencies for
7961
+ * the DELETE phase, derived from state-recorded dependencies plus implicit
7962
+ * type-based ordering rules.
7963
+ *
7964
+ * For a resource X, the returned set contains every resource Y such that Y
7965
+ * must finish deleting before X starts — i.e., Y depends on X (or is otherwise
7966
+ * required to vanish first per implicit type rules).
7967
+ */
7968
+ /**
7969
+ * Returns true if the executor still has un-started pending nodes —
7970
+ * used to distinguish "SIGINT cancelled real work" from "SIGINT landed
7971
+ * after all nodes already completed" (the latter should not error).
7858
7972
  */
7859
- buildDeletionLevels(deleteIds, state) {
7973
+ hasPending(executor) {
7974
+ for (const node of executor.values()) {
7975
+ if (node.state === "pending")
7976
+ return true;
7977
+ }
7978
+ return false;
7979
+ }
7980
+ buildDeletionDependencies(deleteIds, state) {
7860
7981
  const dependedBy = /* @__PURE__ */ new Map();
7861
- const inDegree = /* @__PURE__ */ new Map();
7862
7982
  for (const id of deleteIds) {
7863
- if (!dependedBy.has(id))
7864
- dependedBy.set(id, /* @__PURE__ */ new Set());
7865
- if (!inDegree.has(id))
7866
- inDegree.set(id, 0);
7983
+ dependedBy.set(id, /* @__PURE__ */ new Set());
7867
7984
  }
7868
7985
  for (const id of deleteIds) {
7869
7986
  const resource = state.resources[id];
@@ -7872,38 +7989,11 @@ var DeployEngine = class _DeployEngine {
7872
7989
  for (const dep of resource.dependencies) {
7873
7990
  if (!deleteIds.has(dep))
7874
7991
  continue;
7875
- if (!dependedBy.has(dep))
7876
- dependedBy.set(dep, /* @__PURE__ */ new Set());
7877
7992
  dependedBy.get(dep).add(id);
7878
- inDegree.set(id, (inDegree.get(id) ?? 0) + 1);
7879
7993
  }
7880
7994
  }
7881
7995
  this.addImplicitDeleteDependencies(deleteIds, state, dependedBy);
7882
- const levels = [];
7883
- let remaining = new Set(deleteIds);
7884
- while (remaining.size > 0) {
7885
- const level = [];
7886
- for (const id of remaining) {
7887
- const dependents = dependedBy.get(id);
7888
- const hasPendingDependents = dependents ? [...dependents].some((d) => remaining.has(d)) : false;
7889
- if (!hasPendingDependents) {
7890
- level.push(id);
7891
- }
7892
- }
7893
- if (level.length === 0) {
7894
- this.logger.warn(
7895
- `Circular dependency detected in delete order, deleting remaining ${remaining.size} resources`
7896
- );
7897
- levels.push([...remaining]);
7898
- break;
7899
- }
7900
- levels.push(level);
7901
- remaining = new Set([...remaining].filter((id) => !level.includes(id)));
7902
- }
7903
- this.logger.debug(
7904
- `Delete order: ${levels.length} levels - ${levels.map((l, i) => `L${i + 1}(${l.length})`).join(", ")}`
7905
- );
7906
- return levels;
7996
+ return dependedBy;
7907
7997
  }
7908
7998
  /**
7909
7999
  * Add implicit delete dependency edges based on resource type relationships.