ec2-instance-running-scheduler 0.1.9 → 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
@@ -4,22 +4,23 @@
4
4
  [![npm](https://img.shields.io/npm/v/ec2-instance-running-scheduler?style=flat-square)](https://www.npmjs.com/package/ec2-instance-running-scheduler)
5
5
  [![GitHub Workflow Status (branch)](https://img.shields.io/github/actions/workflow/status/gammarers-aws-cdk-constructs/ec2-instance-running-scheduler/release.yml?branch=main&label=release&style=flat-square)](https://github.com/gammarers-aws-cdk-constructs/ec2-instance-running-scheduler/actions/workflows/release.yml)
6
6
  [![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/gammarers-aws-cdk-constructs/ec2-instance-running-scheduler?sort=semver&style=flat-square)](https://github.com/gammarers-aws-cdk-constructs/ec2-instance-running-scheduler/releases)
7
-
8
7
  [![View on Construct Hub](https://constructs.dev/badge?package=ec2-instance-running-scheduler)](https://constructs.dev/packages/ec2-instance-running-scheduler)
9
8
 
10
- AWS CDK construct that starts and stops EC2 instances on a cron schedule using **EventBridge Scheduler** and a **Durable Execution Lambda**. The handler discovers instances with the **Resource Groups Tagging API**, runs start/stop and **polls until the instance reaches the target state** (durable `step` / `wait`), processes **multiple instances in parallel** (bounded concurrency), and posts **Slack** summary and per-instance thread messages using a secret from **Secrets Manager**. The Lambda emits **structured application logs** (invocation, EC2 transitions, Slack steps, completion) alongside JSON platform logs.
9
+ AWS CDK construct library that starts and stops EC2 instances on a cron schedule using **EventBridge Scheduler** and a **Durable Execution Lambda**. The handler discovers instances with the **Resource Groups Tagging API**, issues start/stop, **polls until each instance reaches a stable target state** (durable `step` / `wait`), processes **multiple instances in parallel** (bounded concurrency), and posts **Slack** summary and per-instance thread messages using a secret from **Secrets Manager**. The Lambda emits **structured application logs** alongside JSON platform logs.
11
10
 
12
11
  ## Features
13
12
 
14
13
  - **Tag-based targeting** – Select EC2 instances by tag key and values (e.g. `Schedule` / `YES`) via `tag:GetResources`.
15
14
  - **EventBridge Scheduler** – Separate cron rules for start and stop, with per-rule timezone (`aws-cdk-lib` `TimeZone`).
16
15
  - **Durable Lambda** – One Lambda with AWS Lambda Durable Execution (`step`, `wait`, `map`, child contexts per instance) for long-running workflows without Step Functions.
17
- - **Stable-state polling** – After start/stop, the function waits and re-describes instances until `running` (start mode) or `stopped` (stop mode), or until a terminal error.
18
- - **Slack notifications** – Required for a successful run: parent message plus threaded updates per instance; credentials come from Secrets Manager (`token` and `channel` JSON). The secret name is passed as **`SLACK_SECRET_NAME`** on the function (from `secrets.slackSecretName`) and validated at runtime via **safe-env-getter**.
19
- - **Structured logging** – The handler uses the durable execution **`ctx.logger`** for traceable JSON application logs (e.g. invocation, describe/start/stop/wait loops, target count, Slack posts, errors before instance state failures).
16
+ - **Stable-state polling** – After start/stop, the function waits (20 seconds between attempts) and re-describes instances until `running` (start mode) or `stopped` (stop mode).
17
+ - **Configurable polling limits** – Per-instance **max loop count** and **max elapsed time** via `resourcePolling` (default: 90 loops / 1800 seconds). Failures use explicit `ResourcePollingFailed:*` messages instead of running until the Durable execution timeout (construct default: 2 hours).
18
+ - **Validated environment variables** – The bundled handler parses required and polling env vars at invocation using strict decimal integer rules; polling limits must be **positive integers** (`> 0`).
19
+ - **Slack notifications** – Parent message plus threaded updates per instance; credentials from Secrets Manager JSON (`token`, `channel`). The construct sets **`SLACK_SECRET_NAME`** on the function.
20
+ - **Structured logging** – Durable execution **`ctx.logger`** for traceable JSON application logs (invocation, describe/start/stop/wait loops, polling limit errors, Slack steps, completion).
20
21
  - **Scheduling toggle** – Enable or disable both schedules without removing the stack (`enableScheduling`).
21
22
  - **Configurable schedules** – Optional cron overrides for start and stop (`minute`, `hour`, `week`, `timezone`); sensible defaults if omitted.
22
- - **IAM and observability** – EC2 and tagging API permissions, Slack secret read grant, **Parameters and Secrets Lambda Extension** (configured on the function for secret access patterns), JSON logging with configurable system/application levels, and a dedicated log group (construct defaults).
23
+ - **IAM and observability** – EC2 and tagging API permissions, Slack secret read grant, **Parameters and Secrets Lambda Extension**, JSON logging, and a dedicated log group (construct defaults).
23
24
 
24
25
  ## Installation
25
26
 
@@ -74,10 +75,14 @@ new EC2InstanceRunningScheduler(stack, 'EC2InstanceRunningScheduler', {
74
75
  week: 'MON-FRI',
75
76
  },
76
77
  enableScheduling: true,
78
+ resourcePolling: {
79
+ maxLoopCount: 120,
80
+ maxElapsedSeconds: 3600,
81
+ },
77
82
  });
78
83
  ```
79
84
 
80
- Use the **stack** `EC2InstanceRunningScheduleStack` when deploying the scheduler as its own stack. It accepts the **same scheduler options** as the construct (plus standard `StackProps` such as `env`).
85
+ Use the **stack** `EC2InstanceRunningScheduleStack` when deploying the scheduler as its own stack. It accepts the same **targeting, schedules, secrets, and enable flag** as the construct (plus standard `StackProps` such as `env`). For **`resourcePolling`**, use the construct directly or extend the stack in your app.
81
86
 
82
87
  ```typescript
83
88
  import * as cdk from 'aws-cdk-lib';
@@ -110,19 +115,32 @@ new EC2InstanceRunningScheduleStack(app, 'EC2InstanceRunningScheduleStack', {
110
115
  });
111
116
  ```
112
117
 
113
- EventBridge Scheduler invokes the Lambda with `Params.TagKey`, `Params.TagValues`, and `Params.Mode` (`Start` or `Stop`); the construct wires this for you. The function environment includes **`SLACK_SECRET_NAME`** set to `secrets.slackSecretName`.
118
+ EventBridge Scheduler invokes the Lambda with `Params.TagKey`, `Params.TagValues`, and `Params.Mode` (`Start` or `Stop`); the construct wires this for you. The function environment includes:
119
+
120
+ | Variable | Source | Purpose |
121
+ |----------|--------|---------|
122
+ | `SLACK_SECRET_NAME` | `secrets.slackSecretName` | Secrets Manager secret for Slack (required) |
123
+ | `PROCESS_RESOURCE_MAX_LOOP_COUNT` | `resourcePolling.maxLoopCount` (default `90`) | Max describe/poll iterations per instance |
124
+ | `PROCESS_RESOURCE_MAX_ELAPSED_SECONDS` | `resourcePolling.maxElapsedSeconds` (default `1800`) | Max wall-clock seconds polling one instance |
125
+
126
+ When you set polling limits via `resourcePolling`, the construct writes them as decimal integer strings. If you override these variables manually, each value must be a **strict decimal integer** (e.g. `"120"` is valid; `"0x10"`, `"3.14"`, or `"10abc"` are rejected) and **greater than zero**. Invalid or missing `SLACK_SECRET_NAME` causes the handler to fail at the start of an invocation.
114
127
 
115
128
  ## Options
116
129
 
117
- These options apply to **`EC2InstanceRunningScheduler`** and to **`EC2InstanceRunningScheduleStack`** (stack props include them alongside `StackProps`).
130
+ ### EC2InstanceRunningScheduler
118
131
 
119
132
  | Option | Type | Required | Description |
120
133
  |--------|------|----------|-------------|
121
134
  | `targetResource` | `TargetResource` | Yes | Tag key and values used to select EC2 instances. |
122
- | `secrets` | `Secrets` | Yes | Identifies the Secrets Manager secret used for Slack (`slackSecretName`). |
135
+ | `secrets` | `Secrets` | Yes | Secrets Manager secret for Slack (`slackSecretName`). |
123
136
  | `startSchedule` | `Schedule` | No | Cron for starting instances (default: `50 7 ? * MON-FRI *` in `Etc/UTC`). |
124
137
  | `stopSchedule` | `Schedule` | No | Cron for stopping instances (default: `5 19 ? * MON-FRI *` in `Etc/UTC`). |
125
138
  | `enableScheduling` | `boolean` | No | Whether both scheduler rules are enabled (default: `true`). |
139
+ | `resourcePolling` | `ResourcePollingLimits` | No | Per-instance polling caps (see below). |
140
+
141
+ ### EC2InstanceRunningScheduleStack
142
+
143
+ Includes `targetResource`, `secrets`, `startSchedule`, `stopSchedule`, `enableScheduling`, and standard `StackProps`. Does **not** expose `resourcePolling`; use `EC2InstanceRunningScheduler` when you need custom polling limits.
126
144
 
127
145
  ### TargetResource
128
146
 
@@ -138,13 +156,22 @@ These options apply to **`EC2InstanceRunningScheduler`** and to **`EC2InstanceRu
138
156
 
139
157
  ### Secrets
140
158
 
141
- - `slackSecretName` – Name of the AWS Secrets Manager secret. The Lambda expects JSON with **`token`** (Slack bot token) and **`channel`** (channel ID or name for `chat.postMessage`). The construct sets **`SLACK_SECRET_NAME`** on the function to this value; the handler reads and validates it at runtime (via **safe-env-getter**).
159
+ - `slackSecretName` – Name of the AWS Secrets Manager secret. The Lambda expects JSON with **`token`** (Slack bot token) and **`channel`** (channel ID or name for `chat.postMessage`).
160
+
161
+ ### ResourcePollingLimits
162
+
163
+ Written to `PROCESS_RESOURCE_MAX_LOOP_COUNT` and `PROCESS_RESOURCE_MAX_ELAPSED_SECONDS` on the running scheduler Lambda.
164
+
165
+ - `maxLoopCount` – Maximum describe/poll loop iterations per instance (default: **90**). Must be a positive integer when set.
166
+ - `maxElapsedSeconds` – Maximum wall-clock seconds spent polling one instance (default: **1800**, 30 minutes). Must be a positive integer when set.
167
+
168
+ When a limit is exceeded during polling, the handler throws an error with prefix `ResourcePollingFailed:` (`MaxLoopCountExceeded`, `MaxElapsedTimeExceeded`, or `UnexpectedInstanceState` for unknown EC2 states).
142
169
 
143
170
  ## Requirements
144
171
 
145
172
  - **Node.js** ≥ 20.0.0 (for developing or synthesizing CDK apps that depend on this package).
146
173
  - **aws-cdk-lib** ^2.232.0 and **constructs** ^10.5.1 (peer dependencies).
147
- - **AWS** – EventBridge Scheduler; Lambda with **Durable Execution** (requires **Node.js 22+** in the deployment region—runtime is the latest Node.js available in the region per the construct), a **live alias**, **Parameters and Secrets Lambda Extension**; EC2 (describe/start/stop); Resource Groups Tagging API; Secrets Manager. The deployed function uses **arm64**, Durable Execution–compatible IAM and settings, and a bundled handler that loads secrets via **aws-lambda-secret-fetcher**.
174
+ - **AWS** – EventBridge Scheduler; Lambda with **Durable Execution** (Node.js **24.x** runtime in the construct; Durable Execution requires a supported Node.js runtime in your region), a **live alias**, **Parameters and Secrets Lambda Extension**; EC2 (`DescribeInstances`, `StartInstances`, `StopInstances`); Resource Groups Tagging API (`tag:GetResources`); Secrets Manager. The deployed function uses **arm64**, Durable Execution IAM policies, a 2-hour Durable execution timeout (construct default), and a bundled handler that loads secrets via **aws-lambda-secret-fetcher**.
148
175
 
149
176
  ## License
150
177
 
@@ -20017,7 +20017,22 @@ var require_lib3 = __commonJS({
20017
20017
  "use strict";
20018
20018
  Object.defineProperty(exports2, "__esModule", { value: true });
20019
20019
  exports2.SafeEnvGetter = exports2.SafeEnvType = exports2.SafeEnvGetterValidationError = exports2.SafeEnvGetterError = void 0;
20020
+ var STRICT_INTEGER_PATTERN = /^-?\d+$/;
20021
+ var parseStrictInteger = (raw) => {
20022
+ const trimmed = raw.trim();
20023
+ if (trimmed === "" || !STRICT_INTEGER_PATTERN.test(trimmed)) {
20024
+ return void 0;
20025
+ }
20026
+ const n = Number(trimmed);
20027
+ if (!Number.isFinite(n) || !Number.isInteger(n)) {
20028
+ return void 0;
20029
+ }
20030
+ return n;
20031
+ };
20020
20032
  var SafeEnvGetterError = class extends Error {
20033
+ /**
20034
+ * @param message - Error message.
20035
+ */
20021
20036
  constructor(message) {
20022
20037
  super(message);
20023
20038
  this.name = "SafeEnvGetterError";
@@ -20027,6 +20042,10 @@ var require_lib3 = __commonJS({
20027
20042
  var SafeEnvGetterValidationError = class _SafeEnvGetterValidationError extends SafeEnvGetterError {
20028
20043
  /**
20029
20044
  * Formats validation errors into a human-readable error message.
20045
+ *
20046
+ * @template K - Environment variable key type.
20047
+ * @param errors - Validation error entries to format.
20048
+ * @returns Multi-line summary listing each key and message.
20030
20049
  */
20031
20050
  static format(errors) {
20032
20051
  const lines = errors.map((e) => `- ${e.key}: ${e.message}${e.raw == null ? "" : ` (raw="${e.raw}")`}`);
@@ -20035,6 +20054,8 @@ ${lines.join("\n")}`;
20035
20054
  }
20036
20055
  /**
20037
20056
  * Creates a new validation error from one or more `SafeEnvError` entries.
20057
+ *
20058
+ * @param errors - One or more structured validation errors.
20038
20059
  */
20039
20060
  constructor(errors) {
20040
20061
  const msg = _SafeEnvGetterValidationError.format(errors);
@@ -20046,9 +20067,12 @@ ${lines.join("\n")}`;
20046
20067
  };
20047
20068
  exports2.SafeEnvGetterValidationError = SafeEnvGetterValidationError;
20048
20069
  exports2.SafeEnvType = {
20049
- /** Spec for a string value. */
20070
+ /** Spec for a string value (returned as-is). */
20050
20071
  String: { type: "string" },
20051
- /** Spec for a numeric value. */
20072
+ /**
20073
+ * Spec for a strict decimal integer.
20074
+ * Rejects whitespace-only, hex, `Infinity`, `NaN`, and non-integer forms.
20075
+ */
20052
20076
  Number: { type: "number" },
20053
20077
  /** Spec for a boolean value (1/true/yes/on → true). */
20054
20078
  Boolean: { type: "boolean" },
@@ -20073,8 +20097,8 @@ ${lines.join("\n")}`;
20073
20097
  }
20074
20098
  switch (spec.type) {
20075
20099
  case "number": {
20076
- const n = Number(raw);
20077
- if (Number.isNaN(n)) {
20100
+ const n = parseStrictInteger(raw);
20101
+ if (n === void 0) {
20078
20102
  throw new SafeEnvGetterValidationError([
20079
20103
  { key, message: `Env ${key}: expected number, got "${raw}"`, raw, kind: "invalid_number" }
20080
20104
  ]);
@@ -20113,8 +20137,8 @@ ${lines.join("\n")}`;
20113
20137
  continue;
20114
20138
  }
20115
20139
  if (spec.type === "number") {
20116
- const n = Number(raw);
20117
- if (Number.isNaN(n)) {
20140
+ const n = parseStrictInteger(raw);
20141
+ if (n === void 0) {
20118
20142
  errors.push({ key, message: `Env ${key}: expected number, got "${raw}"`, raw, kind: "invalid_number" });
20119
20143
  continue;
20120
20144
  }
@@ -20145,7 +20169,9 @@ ${lines.join("\n")}`;
20145
20169
  return envs;
20146
20170
  };
20147
20171
  exports2.SafeEnvGetter = {
20172
+ /** Reads and parses a single environment variable. See {@link getEnv}. */
20148
20173
  getEnv,
20174
+ /** Reads and parses multiple environment variables. See {@link getEnvs}. */
20149
20175
  getEnvs
20150
20176
  };
20151
20177
  }
@@ -23677,10 +23703,73 @@ var import_client_ec2 = require("@aws-sdk/client-ec2");
23677
23703
  var import_client_resource_groups_tagging_api = require("@aws-sdk/client-resource-groups-tagging-api");
23678
23704
  var import_web_api = __toESM(require_dist5());
23679
23705
  var import_aws_lambda_secret_fetcher = __toESM(require_lib2());
23706
+ var import_safe_env_getter2 = __toESM(require_lib3());
23707
+
23708
+ // src/funcs/running-scheduler-polling-env.ts
23680
23709
  var import_safe_env_getter = __toESM(require_lib3());
23681
23710
 
23711
+ // src/funcs/running-scheduler-polling-config.ts
23712
+ var PROCESS_RESOURCE_MAX_LOOP_COUNT_ENV = "PROCESS_RESOURCE_MAX_LOOP_COUNT";
23713
+ var PROCESS_RESOURCE_MAX_ELAPSED_SECONDS_ENV = "PROCESS_RESOURCE_MAX_ELAPSED_SECONDS";
23714
+
23682
23715
  // src/funcs/running-scheduler-predicates.ts
23716
+ var DEFAULT_RESOURCE_POLLING_LIMITS = {
23717
+ maxLoopCount: 90,
23718
+ maxElapsedSeconds: 1800
23719
+ };
23683
23720
  var isDesiredStableState = (mode, currentState) => mode === "Start" && currentState === "running" || mode === "Stop" && currentState === "stopped";
23721
+ var isTransitioningState = (mode, currentState) => mode === "Start" && currentState === "pending" || mode === "Stop" && (currentState === "stopping" || currentState === "shutting-down");
23722
+ var getPollingAbortReason = (loopCount, startedAtMs, nowMs, limits) => {
23723
+ if (loopCount >= limits.maxLoopCount) {
23724
+ return "MaxLoopCountExceeded";
23725
+ }
23726
+ const elapsedSeconds = (nowMs - startedAtMs) / 1e3;
23727
+ if (elapsedSeconds >= limits.maxElapsedSeconds) {
23728
+ return "MaxElapsedTimeExceeded";
23729
+ }
23730
+ return void 0;
23731
+ };
23732
+ var formatResourcePollingFailure = (reason, context) => {
23733
+ const common = `identifier=${context.identifier} mode=${context.mode} currentState=${context.currentState} loopCount=${context.loopCount}`;
23734
+ if (reason === "MaxLoopCountExceeded") {
23735
+ return `ResourcePollingFailed:MaxLoopCountExceeded: ${common} maxLoopCount=${context.limits.maxLoopCount}`;
23736
+ }
23737
+ if (reason === "MaxElapsedTimeExceeded") {
23738
+ return `ResourcePollingFailed:MaxElapsedTimeExceeded: ${common} maxElapsedSeconds=${context.limits.maxElapsedSeconds}`;
23739
+ }
23740
+ return `ResourcePollingFailed:UnexpectedInstanceState: ${common}`;
23741
+ };
23742
+
23743
+ // src/funcs/running-scheduler-polling-env.ts
23744
+ var assertPositiveEnvInt = (key, value) => {
23745
+ if (value <= 0) {
23746
+ const raw = process.env[key];
23747
+ throw new Error(`Invalid ${key}: must be a positive integer (got "${raw ?? ""}")`);
23748
+ }
23749
+ return value;
23750
+ };
23751
+ var parseResourcePollingLimitsFromEnv = () => {
23752
+ const parsed = import_safe_env_getter.SafeEnvGetter.getEnvs({
23753
+ [PROCESS_RESOURCE_MAX_LOOP_COUNT_ENV]: [
23754
+ import_safe_env_getter.SafeEnvType.Number,
23755
+ { default: DEFAULT_RESOURCE_POLLING_LIMITS.maxLoopCount }
23756
+ ],
23757
+ [PROCESS_RESOURCE_MAX_ELAPSED_SECONDS_ENV]: [
23758
+ import_safe_env_getter.SafeEnvType.Number,
23759
+ { default: DEFAULT_RESOURCE_POLLING_LIMITS.maxElapsedSeconds }
23760
+ ]
23761
+ });
23762
+ return {
23763
+ maxLoopCount: assertPositiveEnvInt(
23764
+ PROCESS_RESOURCE_MAX_LOOP_COUNT_ENV,
23765
+ parsed[PROCESS_RESOURCE_MAX_LOOP_COUNT_ENV]
23766
+ ),
23767
+ maxElapsedSeconds: assertPositiveEnvInt(
23768
+ PROCESS_RESOURCE_MAX_ELAPSED_SECONDS_ENV,
23769
+ parsed[PROCESS_RESOURCE_MAX_ELAPSED_SECONDS_ENV]
23770
+ )
23771
+ };
23772
+ };
23684
23773
 
23685
23774
  // src/funcs/running-scheduler.lambda.ts
23686
23775
  var STATE_LIST = [
@@ -23692,7 +23781,7 @@ var getStateDisplay = (current) => {
23692
23781
  const found = STATE_LIST.find((s) => s.state === current);
23693
23782
  return found ? { emoji: found.emoji, name: found.name } : void 0;
23694
23783
  };
23695
- var processOneResource = async (ctx, targetResource, params, resourceIndex) => {
23784
+ var processOneResource = async (ctx, targetResource, params, resourceIndex, pollingLimits) => {
23696
23785
  const parts = targetResource.split("/");
23697
23786
  const identifier = parts[parts.length - 1] ?? "unknown";
23698
23787
  const arnParts = targetResource.split(":");
@@ -23704,11 +23793,36 @@ var processOneResource = async (ctx, targetResource, params, resourceIndex) => {
23704
23793
  identifier,
23705
23794
  region,
23706
23795
  account,
23707
- mode: params.Mode
23796
+ mode: params.Mode,
23797
+ maxLoopCount: pollingLimits.maxLoopCount,
23798
+ maxElapsedSeconds: pollingLimits.maxElapsedSeconds
23708
23799
  });
23800
+ const startedAtMs = await ctx.step(`${stepPrefix}-polling-started-at`, async () => Date.now());
23709
23801
  let loopCount = 0;
23710
23802
  let currentState = "";
23711
23803
  do {
23804
+ const abortReason = await ctx.step(
23805
+ `${stepPrefix}-poll-limit-check-${loopCount}`,
23806
+ async () => getPollingAbortReason(loopCount, startedAtMs, Date.now(), pollingLimits)
23807
+ );
23808
+ if (abortReason) {
23809
+ const message = formatResourcePollingFailure(abortReason, {
23810
+ identifier,
23811
+ mode: params.Mode,
23812
+ currentState: currentState || "unknown",
23813
+ loopCount,
23814
+ limits: pollingLimits
23815
+ });
23816
+ ctx.logger.error("processOneResource: polling limit exceeded", {
23817
+ identifier,
23818
+ abortReason,
23819
+ loopCount,
23820
+ currentState,
23821
+ maxLoopCount: pollingLimits.maxLoopCount,
23822
+ maxElapsedSeconds: pollingLimits.maxElapsedSeconds
23823
+ });
23824
+ throw new Error(message);
23825
+ }
23712
23826
  currentState = await ctx.step(`${stepPrefix}-describe-${loopCount}`, async () => {
23713
23827
  const ec2 = new import_client_ec2.EC2Client({});
23714
23828
  const out = await ec2.send(new import_client_ec2.DescribeInstancesCommand({ InstanceIds: [identifier] }));
@@ -23750,8 +23864,7 @@ var processOneResource = async (ctx, targetResource, params, resourceIndex) => {
23750
23864
  continue;
23751
23865
  }
23752
23866
  if (!isDesiredStableState(mode, currentState)) {
23753
- const transitioning = mode === "Start" && currentState === "pending" || mode === "Stop" && (currentState === "stopping" || currentState === "shutting-down");
23754
- if (transitioning) {
23867
+ if (isTransitioningState(mode, currentState)) {
23755
23868
  ctx.logger.info("processOneResource: wait while transitioning", {
23756
23869
  identifier,
23757
23870
  loopCount,
@@ -23763,13 +23876,20 @@ var processOneResource = async (ctx, targetResource, params, resourceIndex) => {
23763
23876
  loopCount += 1;
23764
23877
  continue;
23765
23878
  }
23879
+ const message = formatResourcePollingFailure("UnexpectedInstanceState", {
23880
+ identifier,
23881
+ mode,
23882
+ currentState,
23883
+ loopCount,
23884
+ limits: pollingLimits
23885
+ });
23766
23886
  ctx.logger.error("processOneResource: unexpected state", {
23767
23887
  identifier,
23768
23888
  mode,
23769
23889
  currentState,
23770
23890
  loopCount
23771
23891
  });
23772
- throw new Error(`instance status fail: mode=${mode} currentState=${currentState}`);
23892
+ throw new Error(message);
23773
23893
  }
23774
23894
  } while (!isDesiredStableState(params.Mode, currentState));
23775
23895
  ctx.logger.info("processOneResource: reached desired stable state", {
@@ -23796,7 +23916,8 @@ var handler = withDurableExecution(async (event, ctx) => {
23796
23916
  if (!params?.TagKey || !params?.TagValues || !params?.Mode) {
23797
23917
  throw new Error("Invalid event: Params.TagKey, Params.TagValues, Params.Mode are required.");
23798
23918
  }
23799
- const slackSecretName = import_safe_env_getter.SafeEnvGetter.getEnv("SLACK_SECRET_NAME");
23919
+ const pollingLimits = parseResourcePollingLimitsFromEnv();
23920
+ const slackSecretName = import_safe_env_getter2.SafeEnvGetter.getEnv("SLACK_SECRET_NAME");
23800
23921
  const slackSecretValue = await ctx.step("fetch-slack-secret", async () => {
23801
23922
  ctx.logger.info("running-scheduler: fetching Slack secret", { secretName: slackSecretName });
23802
23923
  return import_aws_lambda_secret_fetcher.secretFetcher.getSecretValue(slackSecretName);
@@ -23847,7 +23968,7 @@ var handler = withDurableExecution(async (event, ctx) => {
23847
23968
  // ),
23848
23969
  async (mapCtx, targetResource, index) => {
23849
23970
  return mapCtx.runInChildContext(`resource-${index}`, async (childCtx) => {
23850
- const result = await processOneResource(childCtx, targetResource, params, index);
23971
+ const result = await processOneResource(childCtx, targetResource, params, index, pollingLimits);
23851
23972
  childCtx.logger.info("running-scheduler: posting thread Slack message", {
23852
23973
  index,
23853
23974
  identifier: result.identifier,