Skip to content

Commit 5fda33a

Browse files
authored
fix: add optional wait-max-delay-seconds input to configure waiter polling (#839)
1 parent cbf54ec commit 5fda33a

6 files changed

Lines changed: 248 additions & 89 deletions

File tree

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Registers an Amazon ECS task definition and deploys it to an ECS service.
1212
- [Credentials and Region](#credentials-and-region)
1313
- [Permissions](#permissions)
1414
- [AWS CodeDeploy Support](#aws-codedeploy-support)
15+
- [Polling Configuration](#polling-configuration)
1516
- [Troubleshooting](#troubleshooting)
1617
- [License Summary](#license-summary)
1718
- [Security Disclosures](#security-disclosures)
@@ -388,6 +389,25 @@ This is particularly useful in cases where ECS or a previous task definition app
388389
wait-for-service-stability: true
389390
```
390391
392+
## Polling Configuration
393+
394+
By default when waiting for service stability or task completion, the AWS SDK uses exponential backoff which can result in delays up to 120 seconds between polling attempts. This means even after your service becomes stable, you may wait up to 2 minutes before the next poll detects it.
395+
396+
To use consistent polling intervals instead, set `wait-max-delay-seconds`:
397+
398+
```yaml
399+
- name: Deploy to Amazon ECS
400+
uses: aws-actions/amazon-ecs-deploy-task-definition@v2
401+
with:
402+
task-definition: task-definition.json
403+
service: my-service
404+
cluster: my-cluster
405+
wait-for-service-stability: true
406+
wait-max-delay-seconds: 15
407+
```
408+
409+
This configuration polls every 15 seconds instead of using exponential backoff.
410+
391411
## Retries
392412

393413
To automatically retry a failed task definition deployment, use the max-retries input. This controls how many times the action will attempt to register and deploy the task definition before failing.

action.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ inputs:
2222
wait-for-minutes:
2323
description: 'How long to wait for the ECS service to reach stable state, in minutes (default: 30 minutes, max: 6 hours). For CodeDeploy deployments, any wait time configured in the CodeDeploy deployment group will be added to this value.'
2424
required: false
25+
wait-max-delay-seconds:
26+
description: 'Maximum delay in seconds between polling attempts when waiting for service stability or task completion. If not set, AWS SDK uses exponential backoff up to 120 seconds. Set to 15 for consistent 15-second polling intervals.'
27+
required: false
2528
codedeploy-appspec:
2629
description: "The path to the AWS CodeDeploy AppSpec file, if the ECS service uses the CODE_DEPLOY deployment controller. Will default to 'appspec.yaml'."
2730
required: false

dist/index.js

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const IGNORED_TASK_DEFINITION_ATTRIBUTES = [
2727
];
2828

2929
// Method to run a stand-alone task with desired inputs
30-
async function runTask(ecs, clusterName, taskDefArn, waitForMinutes, enableECSManagedTags) {
30+
async function runTask(ecs, clusterName, taskDefArn, waitForMinutes, enableECSManagedTags, waitMaxDelaySeconds) {
3131
core.info('Running task')
3232

3333
const waitForTask = core.getInput('wait-for-task-stopped', { required: false }) || 'false';
@@ -102,7 +102,7 @@ async function runTask(ecs, clusterName, taskDefArn, waitForMinutes, enableECSMa
102102

103103
// Wait for task to end
104104
if (waitForTask && waitForTask.toLowerCase() === "true") {
105-
await waitForTasksStopped(ecs, clusterName, taskArns, waitForMinutes)
105+
await waitForTasksStopped(ecs, clusterName, taskArns, waitForMinutes, waitMaxDelaySeconds)
106106
await tasksExitCode(ecs, clusterName, taskArns)
107107
} else {
108108
core.debug('Not waiting for the task to stop');
@@ -151,18 +151,24 @@ function convertToManagedEbsVolumeObject(managedEbsVolume) {
151151
}
152152

153153
// Poll tasks until they enter a stopped state
154-
async function waitForTasksStopped(ecs, clusterName, taskArns, waitForMinutes) {
154+
async function waitForTasksStopped(ecs, clusterName, taskArns, waitForMinutes, waitMaxDelaySeconds) {
155155
if (waitForMinutes > MAX_WAIT_MINUTES) {
156156
waitForMinutes = MAX_WAIT_MINUTES;
157157
}
158158

159159
core.info(`Waiting for tasks to stop. Will wait for ${waitForMinutes} minutes`);
160160

161-
const waitTaskResponse = await waitUntilTasksStopped({
161+
const waiterConfig = {
162162
client: ecs,
163163
minDelay: WAIT_DEFAULT_DELAY_SEC,
164164
maxWaitTime: waitForMinutes * 60,
165-
}, {
165+
};
166+
167+
if (waitMaxDelaySeconds) {
168+
waiterConfig.maxDelay = waitMaxDelaySeconds;
169+
}
170+
171+
const waitTaskResponse = await waitUntilTasksStopped(waiterConfig, {
166172
cluster: clusterName,
167173
tasks: taskArns,
168174
});
@@ -197,7 +203,7 @@ async function tasksExitCode(ecs, clusterName, taskArns) {
197203
}
198204

199205
// Deploy to a service that uses the 'ECS' deployment controller
200-
async function updateEcsService(ecs, clusterName, service, taskDefArn, waitForService, waitForMinutes, forceNewDeployment, desiredCount, enableECSManagedTags, propagateTags) {
206+
async function updateEcsService(ecs, clusterName, service, taskDefArn, waitForService, waitForMinutes, forceNewDeployment, desiredCount, enableECSManagedTags, propagateTags, waitMaxDelaySeconds) {
201207
core.debug('Updating the service');
202208

203209
const serviceManagedEBSVolumeName = core.getInput('service-managed-ebs-volume-name', { required: false }) || '';
@@ -242,11 +248,18 @@ async function updateEcsService(ecs, clusterName, service, taskDefArn, waitForSe
242248
// Wait for service stability
243249
if (waitForService && waitForService.toLowerCase() === 'true') {
244250
core.debug(`Waiting for the service to become stable. Will wait for ${waitForMinutes} minutes`);
245-
await waitUntilServicesStable({
251+
252+
const waiterConfig = {
246253
client: ecs,
247254
minDelay: WAIT_DEFAULT_DELAY_SEC,
248255
maxWaitTime: waitForMinutes * 60
249-
}, {
256+
};
257+
258+
if (waitMaxDelaySeconds) {
259+
waiterConfig.maxDelay = waitMaxDelaySeconds;
260+
}
261+
262+
await waitUntilServicesStable(waiterConfig, {
250263
services: [service],
251264
cluster: clusterName
252265
});
@@ -381,7 +394,7 @@ function validateProxyConfigurations(taskDef){
381394
}
382395

383396
// Deploy to a service that uses the 'CODE_DEPLOY' deployment controller
384-
async function createCodeDeployDeployment(codedeploy, clusterName, service, taskDefArn, waitForService, waitForMinutes) {
397+
async function createCodeDeployDeployment(codedeploy, clusterName, service, taskDefArn, waitForService, waitForMinutes, waitMaxDelaySeconds) {
385398
core.debug('Updating AppSpec file with new task definition ARN');
386399

387400
let codeDeployAppSpecFile = core.getInput('codedeploy-appspec', { required : false });
@@ -460,11 +473,18 @@ async function createCodeDeployDeployment(codedeploy, clusterName, service, task
460473
totalWaitMin = MAX_WAIT_MINUTES;
461474
}
462475
core.debug(`Waiting for the deployment to complete. Will wait for ${totalWaitMin} minutes`);
463-
await waitUntilDeploymentSuccessful({
476+
477+
const waiterConfig = {
464478
client: codedeploy,
465479
minDelay: WAIT_DEFAULT_DELAY_SEC,
466480
maxWaitTime: totalWaitMin * 60
467-
}, {
481+
};
482+
483+
if (waitMaxDelaySeconds) {
484+
waiterConfig.maxDelay = waitMaxDelaySeconds;
485+
}
486+
487+
await waitUntilDeploymentSuccessful(waiterConfig, {
468488
deploymentId: createDeployResponse.deploymentId
469489
});
470490
} else {
@@ -486,6 +506,9 @@ async function run() {
486506
waitForMinutes = MAX_WAIT_MINUTES;
487507
}
488508

509+
const waitMaxDelaySecondsInput = core.getInput('wait-max-delay-seconds', { required: false });
510+
const waitMaxDelaySeconds = waitMaxDelaySecondsInput ? parseInt(waitMaxDelaySecondsInput) : null;
511+
489512
const forceNewDeployInput = core.getInput('force-new-deployment', { required: false }) || 'false';
490513
const forceNewDeployment = forceNewDeployInput.toLowerCase() === 'true';
491514
const desiredCount = parseInt((core.getInput('desired-count', {required: false})));
@@ -545,7 +568,7 @@ async function run() {
545568
core.debug(`shouldRunTask: ${shouldRunTask}`);
546569
if (shouldRunTask) {
547570
core.debug("Running ad-hoc task...");
548-
await runTask(ecs, clusterName, taskDefArn, waitForMinutes, enableECSManagedTags);
571+
await runTask(ecs, clusterName, taskDefArn, waitForMinutes, enableECSManagedTags, waitMaxDelaySeconds);
549572
}
550573

551574
// Update the service with the new task definition
@@ -569,12 +592,12 @@ async function run() {
569592
if (!serviceResponse.deploymentController || !serviceResponse.deploymentController.type || serviceResponse.deploymentController.type === 'ECS') {
570593
// Service uses the 'ECS' deployment controller, so we can call UpdateService
571594
core.debug('Updating service...');
572-
await updateEcsService(ecs, clusterName, service, taskDefArn, waitForService, waitForMinutes, forceNewDeployment, desiredCount, enableECSManagedTags, propagateTags);
595+
await updateEcsService(ecs, clusterName, service, taskDefArn, waitForService, waitForMinutes, forceNewDeployment, desiredCount, enableECSManagedTags, propagateTags, waitMaxDelaySeconds);
573596

574597
} else if (serviceResponse.deploymentController.type === 'CODE_DEPLOY') {
575598
// Service uses CodeDeploy, so we should start a CodeDeploy deployment
576599
core.debug('Deploying service in the default cluster');
577-
await createCodeDeployDeployment(codedeploy, clusterName, service, taskDefArn, waitForService, waitForMinutes);
600+
await createCodeDeployDeployment(codedeploy, clusterName, service, taskDefArn, waitForService, waitForMinutes, waitMaxDelaySeconds);
578601
} else {
579602
throw new Error(`Unsupported deployment controller: ${serviceResponse.deploymentController.type}`);
580603
}

index.js

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ const IGNORED_TASK_DEFINITION_ATTRIBUTES = [
2121
];
2222

2323
// Method to run a stand-alone task with desired inputs
24-
async function runTask(ecs, clusterName, taskDefArn, waitForMinutes, enableECSManagedTags) {
24+
async function runTask(ecs, clusterName, taskDefArn, waitForMinutes, enableECSManagedTags, waitMaxDelaySeconds) {
2525
core.info('Running task')
2626

2727
const waitForTask = core.getInput('wait-for-task-stopped', { required: false }) || 'false';
@@ -96,7 +96,7 @@ async function runTask(ecs, clusterName, taskDefArn, waitForMinutes, enableECSMa
9696

9797
// Wait for task to end
9898
if (waitForTask && waitForTask.toLowerCase() === "true") {
99-
await waitForTasksStopped(ecs, clusterName, taskArns, waitForMinutes)
99+
await waitForTasksStopped(ecs, clusterName, taskArns, waitForMinutes, waitMaxDelaySeconds)
100100
await tasksExitCode(ecs, clusterName, taskArns)
101101
} else {
102102
core.debug('Not waiting for the task to stop');
@@ -145,18 +145,24 @@ function convertToManagedEbsVolumeObject(managedEbsVolume) {
145145
}
146146

147147
// Poll tasks until they enter a stopped state
148-
async function waitForTasksStopped(ecs, clusterName, taskArns, waitForMinutes) {
148+
async function waitForTasksStopped(ecs, clusterName, taskArns, waitForMinutes, waitMaxDelaySeconds) {
149149
if (waitForMinutes > MAX_WAIT_MINUTES) {
150150
waitForMinutes = MAX_WAIT_MINUTES;
151151
}
152152

153153
core.info(`Waiting for tasks to stop. Will wait for ${waitForMinutes} minutes`);
154154

155-
const waitTaskResponse = await waitUntilTasksStopped({
155+
const waiterConfig = {
156156
client: ecs,
157157
minDelay: WAIT_DEFAULT_DELAY_SEC,
158158
maxWaitTime: waitForMinutes * 60,
159-
}, {
159+
};
160+
161+
if (waitMaxDelaySeconds) {
162+
waiterConfig.maxDelay = waitMaxDelaySeconds;
163+
}
164+
165+
const waitTaskResponse = await waitUntilTasksStopped(waiterConfig, {
160166
cluster: clusterName,
161167
tasks: taskArns,
162168
});
@@ -191,7 +197,7 @@ async function tasksExitCode(ecs, clusterName, taskArns) {
191197
}
192198

193199
// Deploy to a service that uses the 'ECS' deployment controller
194-
async function updateEcsService(ecs, clusterName, service, taskDefArn, waitForService, waitForMinutes, forceNewDeployment, desiredCount, enableECSManagedTags, propagateTags) {
200+
async function updateEcsService(ecs, clusterName, service, taskDefArn, waitForService, waitForMinutes, forceNewDeployment, desiredCount, enableECSManagedTags, propagateTags, waitMaxDelaySeconds) {
195201
core.debug('Updating the service');
196202

197203
const serviceManagedEBSVolumeName = core.getInput('service-managed-ebs-volume-name', { required: false }) || '';
@@ -236,11 +242,18 @@ async function updateEcsService(ecs, clusterName, service, taskDefArn, waitForSe
236242
// Wait for service stability
237243
if (waitForService && waitForService.toLowerCase() === 'true') {
238244
core.debug(`Waiting for the service to become stable. Will wait for ${waitForMinutes} minutes`);
239-
await waitUntilServicesStable({
245+
246+
const waiterConfig = {
240247
client: ecs,
241248
minDelay: WAIT_DEFAULT_DELAY_SEC,
242249
maxWaitTime: waitForMinutes * 60
243-
}, {
250+
};
251+
252+
if (waitMaxDelaySeconds) {
253+
waiterConfig.maxDelay = waitMaxDelaySeconds;
254+
}
255+
256+
await waitUntilServicesStable(waiterConfig, {
244257
services: [service],
245258
cluster: clusterName
246259
});
@@ -375,7 +388,7 @@ function validateProxyConfigurations(taskDef){
375388
}
376389

377390
// Deploy to a service that uses the 'CODE_DEPLOY' deployment controller
378-
async function createCodeDeployDeployment(codedeploy, clusterName, service, taskDefArn, waitForService, waitForMinutes) {
391+
async function createCodeDeployDeployment(codedeploy, clusterName, service, taskDefArn, waitForService, waitForMinutes, waitMaxDelaySeconds) {
379392
core.debug('Updating AppSpec file with new task definition ARN');
380393

381394
let codeDeployAppSpecFile = core.getInput('codedeploy-appspec', { required : false });
@@ -454,11 +467,18 @@ async function createCodeDeployDeployment(codedeploy, clusterName, service, task
454467
totalWaitMin = MAX_WAIT_MINUTES;
455468
}
456469
core.debug(`Waiting for the deployment to complete. Will wait for ${totalWaitMin} minutes`);
457-
await waitUntilDeploymentSuccessful({
470+
471+
const waiterConfig = {
458472
client: codedeploy,
459473
minDelay: WAIT_DEFAULT_DELAY_SEC,
460474
maxWaitTime: totalWaitMin * 60
461-
}, {
475+
};
476+
477+
if (waitMaxDelaySeconds) {
478+
waiterConfig.maxDelay = waitMaxDelaySeconds;
479+
}
480+
481+
await waitUntilDeploymentSuccessful(waiterConfig, {
462482
deploymentId: createDeployResponse.deploymentId
463483
});
464484
} else {
@@ -480,6 +500,9 @@ async function run() {
480500
waitForMinutes = MAX_WAIT_MINUTES;
481501
}
482502

503+
const waitMaxDelaySecondsInput = core.getInput('wait-max-delay-seconds', { required: false });
504+
const waitMaxDelaySeconds = waitMaxDelaySecondsInput ? parseInt(waitMaxDelaySecondsInput) : null;
505+
483506
const forceNewDeployInput = core.getInput('force-new-deployment', { required: false }) || 'false';
484507
const forceNewDeployment = forceNewDeployInput.toLowerCase() === 'true';
485508
const desiredCount = parseInt((core.getInput('desired-count', {required: false})));
@@ -539,7 +562,7 @@ async function run() {
539562
core.debug(`shouldRunTask: ${shouldRunTask}`);
540563
if (shouldRunTask) {
541564
core.debug("Running ad-hoc task...");
542-
await runTask(ecs, clusterName, taskDefArn, waitForMinutes, enableECSManagedTags);
565+
await runTask(ecs, clusterName, taskDefArn, waitForMinutes, enableECSManagedTags, waitMaxDelaySeconds);
543566
}
544567

545568
// Update the service with the new task definition
@@ -563,12 +586,12 @@ async function run() {
563586
if (!serviceResponse.deploymentController || !serviceResponse.deploymentController.type || serviceResponse.deploymentController.type === 'ECS') {
564587
// Service uses the 'ECS' deployment controller, so we can call UpdateService
565588
core.debug('Updating service...');
566-
await updateEcsService(ecs, clusterName, service, taskDefArn, waitForService, waitForMinutes, forceNewDeployment, desiredCount, enableECSManagedTags, propagateTags);
589+
await updateEcsService(ecs, clusterName, service, taskDefArn, waitForService, waitForMinutes, forceNewDeployment, desiredCount, enableECSManagedTags, propagateTags, waitMaxDelaySeconds);
567590

568591
} else if (serviceResponse.deploymentController.type === 'CODE_DEPLOY') {
569592
// Service uses CodeDeploy, so we should start a CodeDeploy deployment
570593
core.debug('Deploying service in the default cluster');
571-
await createCodeDeployDeployment(codedeploy, clusterName, service, taskDefArn, waitForService, waitForMinutes);
594+
await createCodeDeployDeployment(codedeploy, clusterName, service, taskDefArn, waitForService, waitForMinutes, waitMaxDelaySeconds);
572595
} else {
573596
throw new Error(`Unsupported deployment controller: ${serviceResponse.deploymentController.type}`);
574597
}

0 commit comments

Comments
 (0)