Skip to content

fix(ecs,lambda): a transient image pull failure no longer fails the launch - #2516

Merged
vieiralucas merged 6 commits into
mainfrom
fix/batch-array-e2e-flake
Sep 13, 2026
Merged

vieiralucas merged 6 commits into
mainfrom
fix/batch-array-e2e-flake

Conversation

@vieiralucas

@vieiralucas vieiralucas commented Sep 13, 2026 •

Copy link
Copy Markdown
Member

Summary

The batch_real_execution e2e tests flake on nearly every CI run, on main and on every PR: array_job_runs_every_child_and_parent_succeeds needs a retry in almost every E2E general partition, and depends_on_job_waits_for_its_dependency, timeout_fails_an_overrunning_job, retry_strategy_reattempts_a_failing_job and both submit_job_* tests show up too. On #2510 the array test lost all three tries.

Cause

A bare docker pull always contacts the registry, even when the image is already cached. Anonymous pulls from public.ecr.aws are rate limited per source IP, and the Batch tests launch several alpine containers at once from a shared GitHub runner address. The registry answers 429 Too Many Requests, the ECS task stops with TaskFailedToStart, and the Batch job goes FAILED.

Confirmed rather than inferred:

  • 25 concurrent pulls of the cached alpine:3.20 from one machine: one came back 429 Too Many Requests.
  • Running the suite with a container CLI that answers every pull with that 429 and passes everything else to docker: on main's pull path, 6 of 7 tests fail (exactly the ones that flake in CI), each child's statusReason reading image pull failed: ... 429 Too Many Requests. With this change, 7 of 7 pass.

Fix

fakecloud_core::container_image::pull_image, used by the ECS task runtime (so Batch) and by Lambda PackageType=Image function starts and prewarms (the same unconditional pull lived in both places; RDS already falls back):

Pull result Outcome
succeeds use it
transient failure, image cached use the cached image, log the pull error
transient failure, nothing cached retry with backoff (1/2/4/8s, 5 attempts), then fail
refused (not found, access denied) fail at once with the registry's error, even with a stale cached copy

Transient = throttling (429 Too Many Requests / toomanyrequests / Rate exceeded), a registry 5xx, or a network timeout or dropped connection. A refused pull stays final on purpose: an image deleted from ECR, or one a repository policy denies, must not keep launching from a local copy. Neither Fargate nor Lambda has a per-host image cache, so on AWS that pull never succeeds. A missing image still fails fast, so ecs_run_task_missing_ecr_repo_fails_fast is unaffected.

Diagnosability

The Batch e2e wait helper now asserts the expected terminal status itself. On a mismatch it prints each job's status, statusReason and exit code, including every array child. Before, a failure read only left: "FAILED" right: "SUCCEEDED" and the server's stderr is not captured by CI, which is why this sat unexplained.

Surfaces

  • Docs: website/content/docs/services/ecs.md (task execution step 1) and lambda.md (gotchas) describe the pull behavior.
  • No API, SDK, conformance or count change: this is runtime behavior behind existing operations.

Test plan

  • 8 unit tests in fakecloud-core driving a fake container CLI: success, throttled with cache, 503 with cache, throttled then success, attempt cap, missing image not retried, refused pull (not found / access denied) failing despite a cached copy, and transient-vs-refused classification.
  • batch_real_execution with real Docker: 7/7, and 7/7 with every pull forced to 429. Reverting the ECS pull path under the same forced 429: 6/7 fail with the 429 in statusReason (the fix is non-vacuous).
  • Diagnostics verified by flipping one expectation: the panic lists the parent and all three children with status, reason and exit code.
  • cargo clippy --workspace --all-targets -- -D warnings clean; cargo fmt --check clean.
  • ecr_cross_service needs Linux (the daemon must reach the local registry); CI runs it.

Summary by cubic

Fixes batch_real_execution e2e flakes caused by transient image pull failures like 429 Too Many Requests from public.ecr.aws; these no longer fail ECS and Lambda PackageType=Image launches.

  • On a transient pull failure (throttling, registry 5xx, network timeout), the locally cached image is used when available; otherwise the pull is retried with backoff. The cache check runs with the same DOCKER_CONFIG as the pull, so it inspects the same daemon.
  • Not-found and access-denied pulls still fail immediately, even with a stale cached copy. Errors are classified after stripping the image name and registry host, so a repository named like a marker can't flip the outcome.
  • The Batch e2e wait helper now reports status, status reason, and exit code for every job and array child on unexpected terminal states.

Written for commit 147a0b1. Summary will update on new commits.

Review in cubic

A bare `docker pull` always contacts the registry, even for an image
already in the local cache, so any registry error failed the launch.
Anonymous pulls from public.ecr.aws are rate limited per source IP, and
a burst of task launches -- or a shared CI runner address -- gets
`429 Too Many Requests`. The ECS task then stopped with
TaskFailedToStart and every Batch job on it went FAILED.

That is the cause of the batch_real_execution e2e flakes seen on nearly
every CI run: array, depends_on, timeout, retry and submit tests all
launch alpine containers concurrently. Reproduced by running the suite
with a container CLI that answers every pull with a 429: the old pull
path fails 6 of 7 tests with the 429 in statusReason, the new one
passes all 7.

The ECS agent's default ECS_IMAGE_PULL_BEHAVIOR uses the cached image
when a pull fails and retries pulls with backoff. The new
fakecloud_core::container_image::pull_image does the same and is used
by the ECS task runtime (so Batch) and by Lambda PackageType=Image
function starts and prewarms:

- pull succeeds -> use it
- pull fails, image cached -> use the cached image, log the pull error
- pull rate limited, nothing cached -> retry with backoff (5 attempts)
- any other failure with nothing cached -> fail at once with the
  registry's error, so a missing image still fails fast

The Batch e2e wait helper now asserts the expected terminal status
itself and, on a mismatch, prints each job's status, statusReason and
exit code, including every array child, so a CI failure names its cause.
A refused pull -- an image deleted from the registry, or one a repository
policy denies -- must fail the launch even when an earlier launch left a
copy in the local cache. Neither Fargate nor Lambda has a per-host image
cache, so on AWS such a pull never succeeds. Restrict both the cache
fallback and the retry to transient failures: throttling, registry 5xx,
and network timeouts or dropped connections.
@vieiralucas
vieiralucas requested a lite review from Copilot September 13, 2026 14:44

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 7 files

You’re at about 94% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread crates/fakecloud-core/src/container_image.rs
Comment thread crates/fakecloud-core/src/container_image.rs Outdated
Comment thread crates/fakecloud-e2e/tests/batch_real_execution.rs Outdated
…ent failure

- Check refusal wording (not found, denied, unauthorized, forbidden) first,
  on the unmodified message, so a refused pull of a repository named like
  a throttling code (toomanyrequests) is not read as transient and does not
  launch a stale cached copy. A repository name cannot contain a space, so
  it can only spell the one-word markers.
- Treat any 5xx status as transient, parsed from the status position,
  instead of four fixed reason phrases.
- The Batch e2e diagnostics no longer index an empty DescribeJobs result.
…e cache on the same daemon

- Remove the image's own name (the reference, and each trailing path of its
  repository) from the error before matching, whole names only. A throttled
  pull of a repository named like a refusal (acme/access-denied-page) is
  transient again, and a refused pull of one named like a throttle code
  stays refused.
- Run the cache check with the pull's DOCKER_CONFIG. The config selects the
  Docker context, so without it the check could consult a different daemon
  than the one that pulls and runs the image.
…ssifying it

URLs in the error quote the registry host on its own, so a host named
like a refusal (denied.example) would have hidden a 429 from it.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 2 files (changes from recent commits).

You’re at about 95% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread crates/fakecloud-core/src/container_image.rs Outdated
…med reference

The fake CLI pull always used alpine:3.20, so the marker-named repository
test never exercised name removal. The fake now takes the reference, and a
pull-level test covers a throttled pull of acme/access-denied-page falling
back to the cache, which fails if the name is not removed first.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants