dcl-ops-lib 9.7.0 → 9.8.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.
@@ -76,14 +76,7 @@ export type FargateTaskOptions = {
76
76
  appAutoscaling?: {
77
77
  maxCapacity: number;
78
78
  minCapacity?: number;
79
- policy?: {
80
- metricName: string;
81
- targetValue: number;
82
- metricDimensionValue: pulumi.Output<string>;
83
- statistic: "Average" | "Minimum" | "Maximum";
84
- scaleOutCooldown: number;
85
- scaleInCooldown: number;
86
- }[];
79
+ policy?: AppAutoscalingPolicy[];
87
80
  scheduleAction?: {
88
81
  minCapacity: number;
89
82
  maxCapacity: number;
@@ -99,6 +92,41 @@ export type FargateTaskOptions = {
99
92
  };
100
93
  enableExecuteCommand?: boolean;
101
94
  };
95
+ /**
96
+ * One ECS Service Application-Auto-Scaling target-tracking policy.
97
+ *
98
+ * Tracks `metricName` (today only `ApproximateNumberOfMessagesVisible` is
99
+ * understood as an SQS queue depth — anything else falls through with no
100
+ * `customizedMetricSpecification` set) against `targetValue`. Required
101
+ * cooldowns are enforced by AWS.
102
+ *
103
+ * Choose ONE of:
104
+ * - `metricDimensionValue`: a single SQS queue URL. The policy tracks that
105
+ * queue's depth directly.
106
+ * - `metricDimensionValues`: 2+ SQS queue URLs. The policy uses CloudWatch
107
+ * metric math to track the SUM of their depths under one signal — useful
108
+ * when several queues feed the same worker pool and the desired pod count
109
+ * must reflect total pending work, not the max of any one queue. (AWS's
110
+ * default with N parallel target-tracking policies is max-of-all, which
111
+ * undercounts when multiple queues have load simultaneously.)
112
+ *
113
+ * Setting both fields, or neither, throws at apply time.
114
+ */
115
+ export type AppAutoscalingPolicy = {
116
+ metricName: string;
117
+ targetValue: number;
118
+ /** Single-queue form. Mutually exclusive with `metricDimensionValues`. */
119
+ metricDimensionValue?: pulumi.Output<string>;
120
+ /**
121
+ * Multi-queue (sum) form. Mutually exclusive with `metricDimensionValue`.
122
+ * Must contain ≥1 entry; with a single entry, prefer `metricDimensionValue`
123
+ * for clarity.
124
+ */
125
+ metricDimensionValues?: pulumi.Output<string>[];
126
+ statistic: "Average" | "Minimum" | "Maximum";
127
+ scaleOutCooldown: number;
128
+ scaleInCooldown: number;
129
+ };
102
130
  /**
103
131
  *
104
132
  * @param serviceName A name for this service. For example, "builder-api"
@@ -152,14 +180,7 @@ export type InternalServiceOptions = {
152
180
  appAutoscaling?: {
153
181
  maxCapacity: number;
154
182
  minCapacity?: number;
155
- policy?: {
156
- metricName: string;
157
- targetValue: number;
158
- metricDimensionValue: pulumi.Output<string>;
159
- statistic: "Average" | "Minimum" | "Maximum";
160
- scaleOutCooldown: number;
161
- scaleInCooldown: number;
162
- }[];
183
+ policy?: AppAutoscalingPolicy[];
163
184
  scheduleAction?: {
164
185
  maxCapacity: number;
165
186
  minCapacity: number;
@@ -430,26 +430,78 @@ function setAutoscaling(service, serviceName, config) {
430
430
  scalableDimension: "ecs:service:DesiredCount",
431
431
  serviceNamespace: "ecs",
432
432
  });
433
+ // The dcl-ops-lib SQS queue convention is to pass `queue.id` (the queue's
434
+ // URL, e.g., https://sqs.us-east-1.amazonaws.com/.../my-queue) as the
435
+ // metric dimension value. CloudWatch wants just the queue NAME ("my-queue")
436
+ // in the dimension, so we trim everything up to and including the last `/`.
437
+ const extractQueueName = (queueUrl) => queueUrl.apply((value) => value.split("/").pop());
433
438
  if (config.appAutoscaling.policy) {
434
439
  config.appAutoscaling.policy.forEach((item, index) => {
435
- const CSM_ApproximateNumberOfMessagesVisible = {
436
- metricName: item.metricName,
437
- namespace: "AWS/SQS",
438
- dimensions: [
439
- {
440
- name: "QueueName",
441
- value: item.metricDimensionValue.apply((value) => {
442
- return value.split("/").pop();
443
- }),
444
- },
445
- ],
446
- statistic: item.statistic,
447
- unit: "Count",
448
- };
440
+ // Validate the singular/plural choice. Done per-item so the user sees
441
+ // the failure on the offending policy entry rather than a vague top-
442
+ // level error after Pulumi has already started planning resources.
443
+ const hasSingle = item.metricDimensionValue !== undefined;
444
+ const hasMulti = item.metricDimensionValues !== undefined &&
445
+ item.metricDimensionValues.length > 0;
446
+ if (hasSingle && hasMulti) {
447
+ throw new Error(`appAutoscaling.policy[${index}] sets both metricDimensionValue and metricDimensionValues; pick one.`);
448
+ }
449
+ if (!hasSingle && !hasMulti) {
450
+ throw new Error(`appAutoscaling.policy[${index}] must set either metricDimensionValue or metricDimensionValues (non-empty).`);
451
+ }
449
452
  let TTS_CustomizedMetricSpecification = undefined;
450
453
  if (item.metricName === "ApproximateNumberOfMessagesVisible") {
451
- TTS_CustomizedMetricSpecification =
452
- CSM_ApproximateNumberOfMessagesVisible;
454
+ if (hasMulti) {
455
+ // Metric-math form: SUM the queue depths into one signal. Each
456
+ // queue contributes a `mN` metric stat with returnData=false, and
457
+ // a final `e0` expression `m0 + m1 + ...` is the value the scaling
458
+ // target tracks. AWS evaluates the expression at policy time.
459
+ const dims = item.metricDimensionValues;
460
+ const metricStats = dims.map((queueUrl, i) => ({
461
+ id: `m${i}`,
462
+ metricStat: {
463
+ metric: {
464
+ metricName: item.metricName,
465
+ namespace: "AWS/SQS",
466
+ dimensions: [
467
+ {
468
+ name: "QueueName",
469
+ value: extractQueueName(queueUrl),
470
+ },
471
+ ],
472
+ },
473
+ stat: item.statistic,
474
+ unit: "Count",
475
+ },
476
+ returnData: false,
477
+ }));
478
+ const expression = dims.map((_, i) => `m${i}`).join(" + ");
479
+ TTS_CustomizedMetricSpecification = {
480
+ metrics: [
481
+ ...metricStats,
482
+ {
483
+ id: "e0",
484
+ expression,
485
+ returnData: true,
486
+ },
487
+ ],
488
+ };
489
+ }
490
+ else {
491
+ // Single-queue form (existing behavior, byte-identical output).
492
+ TTS_CustomizedMetricSpecification = {
493
+ metricName: item.metricName,
494
+ namespace: "AWS/SQS",
495
+ dimensions: [
496
+ {
497
+ name: "QueueName",
498
+ value: extractQueueName(item.metricDimensionValue),
499
+ },
500
+ ],
501
+ statistic: item.statistic,
502
+ unit: "Count",
503
+ };
504
+ }
453
505
  }
454
506
  return new aws.appautoscaling.Policy(`ecs-autoscaling-policy-${(0, stack_1.getStackScopedName)(serviceName)}-${index.toString()}`, // distinct policies by id
455
507
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dcl-ops-lib",
3
- "version": "9.7.0",
3
+ "version": "9.8.0",
4
4
  "scripts": {
5
5
  "build": "tsc && cp bin/* . && node test.js",
6
6
  "test": "tsc -p tsconfig.test.json && ENVIRONMENT=dev node bin-test/rateLimiting.test.js",