@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.
package/README.md CHANGED
@@ -78,7 +78,7 @@ Reproduce with `./tests/benchmark/run-benchmark.sh all`. See [tests/benchmark/RE
78
78
  │ cdkd Engine │
79
79
  │ - DAG Analysis │ Dependency graph construction
80
80
  │ - Diff Calc │ Compare with existing resources
81
- │ - Parallel Exec │ Deploy by levels
81
+ │ - Parallel Exec │ Event-driven dispatch
82
82
  └────────┬────────┘
83
83
 
84
84
  ┌────┴────┐
@@ -134,10 +134,11 @@ Reproduce with `./tests/benchmark/run-benchmark.sh all`. See [tests/benchmark/RE
134
134
  │ ├── Build DAG from template (Ref/Fn::GetAtt/DependsOn)
135
135
  │ ├── Calculate diff (CREATE/UPDATE/DELETE)
136
136
  │ ├── Resolve intrinsic functions (Ref, Fn::Sub, Fn::Join, etc.)
137
- │ ├── Execute by levels (parallel within each level):
137
+ │ ├── Execute via event-driven DAG dispatch (a resource starts as
138
+ │ │ soon as ALL of its own deps complete; no level barrier):
138
139
  │ │ ├── SDK Providers (direct API calls, preferred)
139
140
  │ │ └── Cloud Control API (fallback, async polling)
140
- │ ├── Save state after each level (partial state save)
141
+ │ ├── Save state after each successful resource (partial state save)
141
142
  │ └── Release lock
142
143
  └── synth does NOT publish assets or deploy (deploy only)
143
144
  ```
@@ -459,7 +460,7 @@ LambdaStack
459
460
  ✓ Deployed LambdaStack (4 resources, 7.2s)
460
461
  ```
461
462
 
462
- Resources without dependencies (ServiceRole and Table) are created in parallel.
463
+ Resources are dispatched as soon as their own dependencies complete (event-driven DAG). ServiceRole and Table run in parallel; DefaultPolicy starts the moment ServiceRole is done — without waiting for Table — and Handler starts the moment DefaultPolicy is done.
463
464
 
464
465
  ## Architecture
465
466
 
package/dist/cli.js CHANGED
@@ -25053,8 +25053,96 @@ function registerAllProviders(registry) {
25053
25053
  registry.register("AWS::S3Tables::Table", s3TablesProvider);
25054
25054
  }
25055
25055
 
25056
+ // src/deployment/dag-executor.ts
25057
+ var DagExecutor = class {
25058
+ nodes = /* @__PURE__ */ new Map();
25059
+ logger = getLogger().child("DagExecutor");
25060
+ add(node) {
25061
+ this.nodes.set(node.id, node);
25062
+ }
25063
+ has(id) {
25064
+ return this.nodes.has(id);
25065
+ }
25066
+ size() {
25067
+ return this.nodes.size;
25068
+ }
25069
+ values() {
25070
+ return this.nodes.values();
25071
+ }
25072
+ async execute(concurrency, fn, cancelled = () => false) {
25073
+ let active = 0;
25074
+ const errors = [];
25075
+ return new Promise((resolve4, reject) => {
25076
+ const dispatch = () => {
25077
+ let changed = true;
25078
+ while (changed) {
25079
+ changed = false;
25080
+ for (const node of this.nodes.values()) {
25081
+ if (node.state !== "pending")
25082
+ continue;
25083
+ const hasFailedDep = [...node.dependencies].some((depId) => {
25084
+ const dep = this.nodes.get(depId);
25085
+ return dep && (dep.state === "failed" || dep.state === "skipped");
25086
+ });
25087
+ if (hasFailedDep) {
25088
+ node.state = "skipped";
25089
+ changed = true;
25090
+ this.logger.debug(`Skipped ${node.id}: dependency failed or was skipped`);
25091
+ }
25092
+ }
25093
+ }
25094
+ const ready = [];
25095
+ for (const node of this.nodes.values()) {
25096
+ if (node.state !== "pending")
25097
+ continue;
25098
+ const depsReady = [...node.dependencies].every((depId) => {
25099
+ const dep = this.nodes.get(depId);
25100
+ return !dep || dep.state === "completed";
25101
+ });
25102
+ if (depsReady)
25103
+ ready.push(node);
25104
+ }
25105
+ if (!cancelled()) {
25106
+ for (const node of ready) {
25107
+ if (active >= concurrency)
25108
+ break;
25109
+ node.state = "running";
25110
+ active++;
25111
+ fn(node).then(() => {
25112
+ node.state = "completed";
25113
+ }).catch((error) => {
25114
+ node.state = "failed";
25115
+ errors.push({ id: node.id, error });
25116
+ }).finally(() => {
25117
+ active--;
25118
+ dispatch();
25119
+ });
25120
+ }
25121
+ }
25122
+ if (active === 0) {
25123
+ if (errors.length > 0) {
25124
+ reject(errors[0].error);
25125
+ return;
25126
+ }
25127
+ const stillPending = [...this.nodes.values()].some((n) => n.state === "pending");
25128
+ if (stillPending && !cancelled()) {
25129
+ const pending = [...this.nodes.values()].filter((n) => n.state === "pending").map((n) => n.id);
25130
+ reject(
25131
+ new Error(
25132
+ `Deadlock detected: ${pending.length} node(s) stuck with unresolvable dependencies (${pending.join(", ")})`
25133
+ )
25134
+ );
25135
+ return;
25136
+ }
25137
+ resolve4();
25138
+ }
25139
+ };
25140
+ dispatch();
25141
+ });
25142
+ }
25143
+ };
25144
+
25056
25145
  // src/deployment/deploy-engine.ts
25057
- import pLimit from "p-limit";
25058
25146
  var InterruptedError = class extends Error {
25059
25147
  constructor() {
25060
25148
  super("Deployment interrupted by user (Ctrl+C)");
@@ -25187,6 +25275,7 @@ var DeployEngine = class _DeployEngine {
25187
25275
  template,
25188
25276
  currentState,
25189
25277
  changes,
25278
+ dag,
25190
25279
  executionLevels,
25191
25280
  stackName,
25192
25281
  parameterValues,
@@ -25219,13 +25308,16 @@ var DeployEngine = class _DeployEngine {
25219
25308
  }
25220
25309
  }
25221
25310
  /**
25222
- * Execute deployment by processing resources in DAG order
25311
+ * Execute deployment by processing resources via event-driven DAG dispatch.
25223
25312
  *
25224
- * Important: DELETE operations are executed in reverse dependency order,
25225
- * while CREATE/UPDATE follow normal dependency order.
25226
- */
25227
- async executeDeployment(template, currentState, changes, executionLevels, stackName, parameterValues, conditions, currentEtag, progress) {
25228
- const limit = pLimit(this.options.concurrency);
25313
+ * - CREATE/UPDATE follow forward dependency order (a node starts as soon as
25314
+ * ALL of its dependencies are completed — does not wait for unrelated
25315
+ * siblings in the same "level")
25316
+ * - DELETE follows reverse dependency order (a node starts as soon as all
25317
+ * resources that depend ON it have finished deleting)
25318
+ */
25319
+ async executeDeployment(template, currentState, changes, dag, executionLevels, stackName, parameterValues, conditions, currentEtag, progress) {
25320
+ const concurrency = this.options.concurrency;
25229
25321
  const newResources = { ...currentState.resources };
25230
25322
  const actualCounts = { created: 0, updated: 0, deleted: 0, skipped: 0 };
25231
25323
  const completedOperations = [];
@@ -25256,32 +25348,36 @@ var DeployEngine = class _DeployEngine {
25256
25348
  Array.from(changes.entries()).filter(([_, change]) => change.changeType === "DELETE").map(([logicalId]) => logicalId)
25257
25349
  );
25258
25350
  try {
25259
- for (let levelIndex = 0; levelIndex < executionLevels.length; levelIndex++) {
25260
- if (this.interrupted) {
25261
- throw new InterruptedError();
25262
- }
25263
- const levelNodes = executionLevels[levelIndex];
25264
- if (!levelNodes)
25351
+ const createUpdateIds = [];
25352
+ for (const [id, change] of changes.entries()) {
25353
+ if (deleteChanges.has(id))
25265
25354
  continue;
25266
- const level = levelNodes.filter((id) => {
25267
- if (deleteChanges.has(id))
25268
- return false;
25269
- const change = changes.get(id);
25270
- return !!change && change.changeType !== "NO_CHANGE";
25271
- });
25272
- if (level.length === 0)
25355
+ if (change.changeType === "NO_CHANGE")
25273
25356
  continue;
25357
+ createUpdateIds.push(id);
25358
+ }
25359
+ if (createUpdateIds.length > 0) {
25274
25360
  this.logger.info(
25275
- `Level ${levelIndex + 1}/${executionLevels.length} (${level.length} resources)`
25361
+ `Deploying ${createUpdateIds.length} resource(s) (DAG: ${executionLevels.length} levels, max parallel: ${concurrency})`
25276
25362
  );
25277
- const results = await Promise.allSettled(
25278
- level.map(
25279
- (logicalId) => limit(async () => {
25280
- const change = changes.get(logicalId);
25281
- if (!change || change.changeType === "NO_CHANGE") {
25282
- this.logger.debug(`Skipping ${logicalId} (no change)`);
25283
- return;
25284
- }
25363
+ const createUpdateExecutor = new DagExecutor();
25364
+ const provisionable = new Set(createUpdateIds);
25365
+ for (const id of createUpdateIds) {
25366
+ const allDeps = this.dagBuilder.getDirectDependencies(dag, id);
25367
+ const deps = new Set(allDeps.filter((d) => provisionable.has(d)));
25368
+ createUpdateExecutor.add({
25369
+ id,
25370
+ dependencies: deps,
25371
+ state: "pending",
25372
+ data: changes.get(id)
25373
+ });
25374
+ }
25375
+ try {
25376
+ await createUpdateExecutor.execute(
25377
+ concurrency,
25378
+ async (node) => {
25379
+ const logicalId = node.id;
25380
+ const change = node.data;
25285
25381
  const previousState = currentState.resources[logicalId] ? { ...currentState.resources[logicalId] } : void 0;
25286
25382
  try {
25287
25383
  await this.provisionResource(
@@ -25308,30 +25404,36 @@ var DeployEngine = class _DeployEngine {
25308
25404
  properties: newResources[logicalId]?.properties
25309
25405
  });
25310
25406
  saveStateAfterResource(logicalId);
25311
- })
25312
- )
25313
- );
25314
- await saveChain;
25315
- const failures = results.filter((r) => r.status === "rejected");
25316
- if (failures.length > 0) {
25317
- throw failures[0].reason;
25407
+ },
25408
+ () => this.interrupted
25409
+ );
25410
+ } finally {
25411
+ await saveChain;
25412
+ }
25413
+ if (this.interrupted && this.hasPending(createUpdateExecutor)) {
25414
+ throw new InterruptedError();
25318
25415
  }
25319
25416
  }
25320
25417
  if (deleteChanges.size > 0) {
25321
25418
  this.logger.info(`Deleting ${deleteChanges.size} resource(s)`);
25322
- const deletionLevels = this.buildDeletionLevels(deleteChanges, currentState);
25323
- for (let levelIndex = 0; levelIndex < deletionLevels.length; levelIndex++) {
25324
- if (this.interrupted) {
25325
- throw new InterruptedError();
25326
- }
25327
- const level = deletionLevels[levelIndex];
25328
- if (level.length === 0)
25329
- continue;
25330
- const deleteResults = await Promise.allSettled(
25331
- level.map(
25332
- (logicalId) => limit(async () => {
25333
- const change = changes.get(logicalId);
25334
- const previousState = currentState.resources[logicalId] ? { ...currentState.resources[logicalId] } : void 0;
25419
+ const deleteDeps = this.buildDeletionDependencies(deleteChanges, currentState);
25420
+ const deleteExecutor = new DagExecutor();
25421
+ for (const id of deleteChanges) {
25422
+ deleteExecutor.add({
25423
+ id,
25424
+ dependencies: deleteDeps.get(id) ?? /* @__PURE__ */ new Set(),
25425
+ state: "pending",
25426
+ data: changes.get(id)
25427
+ });
25428
+ }
25429
+ try {
25430
+ await deleteExecutor.execute(
25431
+ concurrency,
25432
+ async (node) => {
25433
+ const logicalId = node.id;
25434
+ const change = node.data;
25435
+ const previousState = currentState.resources[logicalId] ? { ...currentState.resources[logicalId] } : void 0;
25436
+ try {
25335
25437
  await this.provisionResource(
25336
25438
  logicalId,
25337
25439
  change,
@@ -25343,23 +25445,25 @@ var DeployEngine = class _DeployEngine {
25343
25445
  actualCounts,
25344
25446
  progress
25345
25447
  );
25346
- completedOperations.push({
25347
- logicalId,
25348
- changeType: "DELETE",
25349
- resourceType: change.resourceType,
25350
- previousState
25351
- });
25352
- saveStateAfterResource(logicalId);
25353
- })
25354
- )
25448
+ } catch (provisionError) {
25449
+ this.interrupted = true;
25450
+ throw provisionError;
25451
+ }
25452
+ completedOperations.push({
25453
+ logicalId,
25454
+ changeType: "DELETE",
25455
+ resourceType: change.resourceType,
25456
+ previousState
25457
+ });
25458
+ saveStateAfterResource(logicalId);
25459
+ },
25460
+ () => this.interrupted
25355
25461
  );
25462
+ } finally {
25356
25463
  await saveChain;
25357
- const deleteFailures = deleteResults.filter(
25358
- (r) => r.status === "rejected"
25359
- );
25360
- if (deleteFailures.length > 0) {
25361
- throw deleteFailures[0].reason;
25362
- }
25464
+ }
25465
+ if (this.interrupted && this.hasPending(deleteExecutor)) {
25466
+ throw new InterruptedError();
25363
25467
  }
25364
25468
  }
25365
25469
  } catch (error) {
@@ -25453,12 +25557,12 @@ var DeployEngine = class _DeployEngine {
25453
25557
  * - UPDATE → update back to previous properties
25454
25558
  * - DELETE → cannot rollback (resource already deleted), log warning
25455
25559
  *
25456
- * Resources created in the same DAG level may have dependencies between them
25457
- * (e.g., IAM Policy depends on IAM Role). When rolling back CREATEs (deleting),
25458
- * dependent resources must be deleted before their dependencies. This method
25459
- * sorts CREATE rollback operations using dependency information from state,
25460
- * then processes UPDATE/DELETE rollbacks, and finally processes sorted CREATE
25461
- * rollback deletions.
25560
+ * Resources completed concurrently in the dispatcher may have dependencies
25561
+ * between them (e.g., IAM Policy depends on IAM Role). When rolling back
25562
+ * CREATEs (deleting), dependent resources must be deleted before their
25563
+ * dependencies. This method sorts CREATE rollback operations using dependency
25564
+ * information from state, then processes UPDATE/DELETE rollbacks, and finally
25565
+ * processes sorted CREATE rollback deletions.
25462
25566
  */
25463
25567
  async performRollback(completedOperations, stateResources, _stackName) {
25464
25568
  if (completedOperations.length === 0) {
@@ -25491,7 +25595,7 @@ var DeployEngine = class _DeployEngine {
25491
25595
  * Sort CREATE rollback operations so that resources depending on others
25492
25596
  * are deleted first (reverse dependency order).
25493
25597
  *
25494
- * Uses state dependencies to build deletion levels, similar to buildDeletionLevels.
25598
+ * Uses state dependencies to determine reverse-dependency order, similar to buildDeletionDependencies.
25495
25599
  */
25496
25600
  sortRollbackCreates(createOps, stateResources) {
25497
25601
  const opMap = /* @__PURE__ */ new Map();
@@ -25913,17 +26017,30 @@ var DeployEngine = class _DeployEngine {
25913
26017
  "AWS::EC2::SecurityGroup": ["AWS::EC2::SecurityGroupIngress", "AWS::EC2::SecurityGroupEgress"]
25914
26018
  };
25915
26019
  /**
25916
- * Build deletion levels from state dependencies (reverse topological order).
25917
- * Resources that are depended upon by others are deleted LAST.
26020
+ * Build a per-resource map of "must be deleted before me" dependencies for
26021
+ * the DELETE phase, derived from state-recorded dependencies plus implicit
26022
+ * type-based ordering rules.
26023
+ *
26024
+ * For a resource X, the returned set contains every resource Y such that Y
26025
+ * must finish deleting before X starts — i.e., Y depends on X (or is otherwise
26026
+ * required to vanish first per implicit type rules).
26027
+ */
26028
+ /**
26029
+ * Returns true if the executor still has un-started pending nodes —
26030
+ * used to distinguish "SIGINT cancelled real work" from "SIGINT landed
26031
+ * after all nodes already completed" (the latter should not error).
25918
26032
  */
25919
- buildDeletionLevels(deleteIds, state) {
26033
+ hasPending(executor) {
26034
+ for (const node of executor.values()) {
26035
+ if (node.state === "pending")
26036
+ return true;
26037
+ }
26038
+ return false;
26039
+ }
26040
+ buildDeletionDependencies(deleteIds, state) {
25920
26041
  const dependedBy = /* @__PURE__ */ new Map();
25921
- const inDegree = /* @__PURE__ */ new Map();
25922
26042
  for (const id of deleteIds) {
25923
- if (!dependedBy.has(id))
25924
- dependedBy.set(id, /* @__PURE__ */ new Set());
25925
- if (!inDegree.has(id))
25926
- inDegree.set(id, 0);
26043
+ dependedBy.set(id, /* @__PURE__ */ new Set());
25927
26044
  }
25928
26045
  for (const id of deleteIds) {
25929
26046
  const resource = state.resources[id];
@@ -25932,38 +26049,11 @@ var DeployEngine = class _DeployEngine {
25932
26049
  for (const dep of resource.dependencies) {
25933
26050
  if (!deleteIds.has(dep))
25934
26051
  continue;
25935
- if (!dependedBy.has(dep))
25936
- dependedBy.set(dep, /* @__PURE__ */ new Set());
25937
26052
  dependedBy.get(dep).add(id);
25938
- inDegree.set(id, (inDegree.get(id) ?? 0) + 1);
25939
26053
  }
25940
26054
  }
25941
26055
  this.addImplicitDeleteDependencies(deleteIds, state, dependedBy);
25942
- const levels = [];
25943
- let remaining = new Set(deleteIds);
25944
- while (remaining.size > 0) {
25945
- const level = [];
25946
- for (const id of remaining) {
25947
- const dependents = dependedBy.get(id);
25948
- const hasPendingDependents = dependents ? [...dependents].some((d) => remaining.has(d)) : false;
25949
- if (!hasPendingDependents) {
25950
- level.push(id);
25951
- }
25952
- }
25953
- if (level.length === 0) {
25954
- this.logger.warn(
25955
- `Circular dependency detected in delete order, deleting remaining ${remaining.size} resources`
25956
- );
25957
- levels.push([...remaining]);
25958
- break;
25959
- }
25960
- levels.push(level);
25961
- remaining = new Set([...remaining].filter((id) => !level.includes(id)));
25962
- }
25963
- this.logger.debug(
25964
- `Delete order: ${levels.length} levels - ${levels.map((l, i) => `L${i + 1}(${l.length})`).join(", ")}`
25965
- );
25966
- return levels;
26056
+ return dependedBy;
25967
26057
  }
25968
26058
  /**
25969
26059
  * Add implicit delete dependency edges based on resource type relationships.
@@ -27029,7 +27119,7 @@ function reorderArgs(argv) {
27029
27119
  }
27030
27120
  async function main() {
27031
27121
  const program = new Command8();
27032
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.1.0");
27122
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.2.0");
27033
27123
  program.addCommand(createBootstrapCommand());
27034
27124
  program.addCommand(createSynthCommand());
27035
27125
  program.addCommand(createDeployCommand());