Skip to content

Commit db98646

Browse files
authored
fix(oabctl): wait for service drain on delete, retry on create (#1305)
* fix(oabctl): wait for service drain on delete, retry on create delete: after force-deleting the ECS service, poll until it reaches INACTIVE status (up to 60s) so a subsequent apply doesn't race against the draining service. apply: if create_service hits 'still Draining', retry with 5s backoff (up to 12 attempts / 60s) instead of failing immediately. * fix(review): address PR review findings - delete.rs: treat DescribeServices errors as retry (not gone) - delete.rs: increase polling interval from 1s to 5s (consistent with wait_for_stable) - delete.rs: standardize progress output to stderr - apply.rs: fix dead code — restructure loop so timeout path is reachable - apply.rs: fix stdout/stderr mixing in retry progress output * fix(review): use typed error matching, improve polling UX - apply.rs: Replace fragile string matching on 'still Draining' with typed error detection via ProvideErrorMetadata (code == InvalidParameterException + case-insensitive 'draining' in message). Consistent with ingress.rs pattern. - apply.rs: Add progress dots during retry sleep for better UX. - delete.rs: Reorder poll loop to check-before-sleep, avoiding unnecessary initial 5s delay when service transitions quickly. - delete.rs: Add progress dots during polling iterations. Addresses review findings from PR group review. --------- Co-authored-by: chaodu-agent <chaodu-agent@users.noreply.github.com>
1 parent 0f940ca commit db98646

2 files changed

Lines changed: 85 additions & 4 deletions

File tree

operator/src/apply.rs

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -524,10 +524,50 @@ async fn apply_ecs(
524524
create_req = create_req.service_registries(registry.build());
525525
}
526526

527-
create_req
528-
.send()
529-
.await
530-
.context("failed to create ECS service")?;
527+
// Retry with backoff if ECS reports "still Draining" (race with a
528+
// recent delete that hasn't fully completed yet).
529+
// Match on the typed error code (InvalidParameterException) rather than
530+
// raw message text to be resilient to SDK/API wording changes.
531+
use aws_sdk_ecs::error::ProvideErrorMetadata;
532+
let mut last_err = None;
533+
for attempt in 0..12 {
534+
match create_req.clone().send().await {
535+
Ok(_) => {
536+
if attempt > 0 {
537+
eprintln!(" ok");
538+
}
539+
last_err = None;
540+
break;
541+
}
542+
Err(e) => {
543+
let is_draining = e.code() == Some("InvalidParameterException")
544+
&& e.message()
545+
.unwrap_or_default()
546+
.to_lowercase()
547+
.contains("draining");
548+
if is_draining {
549+
if attempt == 0 {
550+
eprint!(" ⏳ Service still draining, retrying...");
551+
} else {
552+
eprint!(".");
553+
}
554+
last_err = Some(e);
555+
if attempt < 11 {
556+
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
557+
}
558+
} else {
559+
if attempt > 0 {
560+
eprintln!(" failed");
561+
}
562+
return Err(e).context("failed to create ECS service");
563+
}
564+
}
565+
}
566+
}
567+
if let Some(e) = last_err {
568+
eprintln!(" timed out");
569+
return Err(anyhow::anyhow!(e)).context("failed to create ECS service after retries");
570+
}
531571
println!(
532572
" ✓ {} created ({}, {}cpu/{}mem{})",
533573
m.metadata.name,

operator/src/delete.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,47 @@ pub async fn run(
8888
.context("failed to delete ECS service")?;
8989
println!(" ✓ ECS service deleted");
9090

91+
// 2a. Wait for the service to fully drain (INACTIVE status) so that
92+
// a subsequent `apply` doesn't hit "Unable to Start a service that
93+
// is still Draining".
94+
eprint!(" ⏳ Waiting for drain to complete...");
95+
for i in 0..12 {
96+
// Check status first, then sleep — avoids an unnecessary initial delay
97+
// when the service transitions quickly.
98+
let resp = ecs
99+
.describe_services()
100+
.cluster(cluster)
101+
.services(&service_name)
102+
.send()
103+
.await;
104+
let is_gone = match resp {
105+
Ok(r) => r
106+
.services()
107+
.first()
108+
.map(|s| s.status() == Some("INACTIVE"))
109+
.unwrap_or(true),
110+
Err(e) => {
111+
eprintln!("\n ⚠ describe_services error (retrying): {e}");
112+
false
113+
}
114+
};
115+
if is_gone {
116+
if i == 0 {
117+
eprintln!(" done (immediate)");
118+
} else {
119+
let elapsed = i * 5;
120+
eprintln!(" done ({elapsed}s)");
121+
}
122+
break;
123+
}
124+
if i == 11 {
125+
eprintln!(" timed out (service may still be draining)");
126+
} else {
127+
eprint!(".");
128+
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
129+
}
130+
}
131+
91132
// 2b. Best-effort ingress teardown: Cloud Map service + this API's
92133
// routes/integration/stage. No-op for bots that never had ingress. Never
93134
// blocks deletion — failures are logged only.

0 commit comments

Comments
 (0)