Skip to content

fix(bb-data): retry migrations while a scale-to-zero cluster resumes - #451

Merged
sharonyajain merged 11 commits into
aws-devtools-labs:mainfrom
fossamagna:fix/bb-data-migration-retry-resuming-cluster
Sep 6, 2026
Merged

sharonyajain merged 11 commits into
aws-devtools-labs:mainfrom
fossamagna:fix/bb-data-migration-retry-resuming-cluster

Conversation

@fossamagna

Copy link
Copy Markdown
Contributor

Problem

A Database configured with minCapacity: 0 (Aurora Serverless v2 scale-to-zero) auto-pauses after ~5 minutes idle. The first Data API call of a deploy wakes the cluster and fails with DatabaseResumingException while it resumes, so the migration custom resource failed and the stack rolled back. Deterministic rather than a race: any deploy made more than ~5 minutes after the last database activity hit it.

Issue #, if available: #450

Changes

The migration Lambda already backed off (1s → 30s, 8 attempts) while Aurora was coming up, but two layers kept the resume error out of that path, so a name-only fix in the Lambda would not have worked:

  • DataApiEngine classified it as QueryFailed. Errors carrying no SQLState are classified by SDK exception name, and DatabaseResumingException was not in that list. It now maps to ConnectionFailed, alongside ServiceUnavailableException and InternalServerErrorException. Application code that queries a paused cluster sees the same, more accurate name — transient and safe to retry, unlike QueryFailed.
  • The Lambda's predicate only matched raw SDK error names, which the engine has already rewritten by the time the error arrives. It now retries on ConnectionFailed. The raw SDK names are kept for errors raised outside the engine, and the retry log line now names the error, since its absence was the reporter's diagnostic signal.

Worth calling out for review:

  • Retrying on ConnectionFailed also covers SQLState 08xxx connection failures, which were previously not retried. In a migration Lambda that seems right — a transient connection failure is exactly what the backoff is for — but the cost is that a genuinely unreachable database now takes ~2 minutes to fail the deploy instead of failing immediately.
  • Reclassifying DatabaseResumingException is a behavior change for application code: a query against a paused cluster now throws ConnectionFailedException instead of QueryFailedException. No types change, and the README documents the new meaning.
  • Backoff limits are unchanged (~2 minutes total). The verification below used 3 of the 8 attempts.
  • withRetry and its predicate are now exported for testing. migration-lambda.ts is a Lambda entry point, not part of the package's exports map, so this does not widen the public API surface (npm run check:api confirms the reports are unchanged).

Validation

Unit tests, written to fail first:

  • data-api-engine.test.ts — the resume error maps to ConnectionFailed.
  • migration-lambda.test.ts — three predicate cases plus a withRetry loop test.

The Lambda-side tests drive the error through a stub Data API client into the real DataApiEngine.execute, rather than asserting on a hand-built SDK error. That matters here: a raw-error test passes without the engine-side change while the bug survives.

No e2e test covers this combination. Local e2e runs on PGlite, so it cannot reach the Data API path at all, and a sandbox repro requires a minCapacity: 0 cluster idled past its auto-pause window. Instead it was verified manually against a real deployed stack (ap-northeast-1, Aurora PostgreSQL 16.13, minCapacity: 0) whose cluster had been paused for 30+ minutes (ServerlessDatabaseCapacity at 0), by adding a no-op migration to change migrationsHash:

06:05:51  [migration-lambda] Event: {"RequestType":"Update","migrationsHash":"57c5906d2974d89f"}
06:05:54  [migration-lambda] Aurora not ready (ConnectionFailedException), retry 1/8 in 1000ms
06:05:59  [migration-lambda] Aurora not ready (ConnectionFailedException), retry 2/8 in 2000ms
06:06:04  [migration-lambda] Aurora not ready (ConnectionFailedException), retry 3/8 in 4000ms
06:06:08  [migrations] Applied: 003_verify_450_noop.sql

The stack reached UPDATE_COMPLETE instead of rolling back. Note the error surfaces as ConnectionFailedException, confirming the engine-side classification is the part that carries the signal.

Local gates: npm run build, npm run lint, npm run lint:deps, npm run check:api, npm test, npm run test:e2e:local all pass.

Checklist

  • PR description included
  • Tests are changed or added
  • Relevant documentation is changed or added (and PR referenced)

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

…ws-devtools-labs#450)

A Database with minCapacity: 0 auto-pauses after ~5 minutes idle. The first
Data API call of a deploy wakes the cluster and fails with
DatabaseResumingException while it resumes, so the migration custom resource
failed and the stack rolled back. Deterministic, not a race: any deploy made
more than ~5 minutes after the last database activity hit it.

The migration Lambda already backed off (1s -> 30s, 8 attempts) while Aurora
was coming up, but two layers kept the resume error out of that path:

- DataApiEngine classified it as QueryFailed. Errors with no SQLState are
  classified by SDK exception name, and DatabaseResumingException was not in
  that list. It now maps to ConnectionFailed, alongside
  ServiceUnavailableException and InternalServerErrorException, so application
  code querying a paused cluster also sees a transient, retryable name.
- The Lambda's predicate only matched raw SDK error names, which the engine has
  already rewritten by the time the error arrives. It now retries on
  ConnectionFailed; the raw SDK names stay for errors raised outside the engine,
  and the retry log line names the error (its absence was the reporter's
  diagnostic signal).

Backoff limits are unchanged. The tests drive the error through the real engine
rather than hand-building an SDK error, because a raw-error test passes without
the engine-side fix while the bug survives.
@fossamagna
fossamagna requested a review from a team as a code owner August 28, 2026 06:16
@changeset-bot

changeset-bot Bot commented Aug 28, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2a8af10

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@aws-blocks/bb-data Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

pranavosu
pranavosu previously approved these changes Aug 28, 2026
Comment thread packages/bb-data/src/migration-lambda.ts
Comment thread packages/bb-data/src/migration-lambda.ts Outdated
@sarayev

sarayev commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Overall LGTM, two minor comments that are not blockers. Approving.

sarayev
sarayev previously approved these changes Sep 1, 2026
…ls-labs#451)

Adds the three cases review asked for, all against the error as the engine
actually throws it:

- withRetry rethrows a non-retryable error on the first attempt, unwrapped, so
  CloudFormation surfaces the real SQL error rather than a timeout.
- An 08xxx connection failure is retryable. This is deliberate: the trade is
  that a genuinely unreachable database takes ~2 minutes to fail the deploy.
- The raw DatabaseResumingException / BadRequestException names are retryable
  without the engine. Nothing routes around DataApiEngine today, so these
  branches are defensive; the test documents that intent rather than leaving
  them untested.

Also drops an unused `mock` import that predates this branch.

No production code changes, so no changeset.
@fossamagna
fossamagna dismissed stale reviews from sarayev and pranavosu via 6fd055b September 1, 2026 15:15
@fossamagna

Copy link
Copy Markdown
Contributor Author

@sarayev @pranavosu Sorry for the extra round — your approval got dismissed by a follow-up push. Could you re-approve when you get a chance?

sarayev
sarayev previously approved these changes Sep 3, 2026

@sarayev sarayev left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM 👍

Thanks for addressing the comments!

Comment thread packages/bb-data/src/engines/data-api-engine.ts Outdated
Comment thread packages/bb-data/src/migration-lambda.ts
Comment thread packages/bb-data/src/migration-lambda.ts
…ames (aws-devtools-labs#451)

`DatabaseResumingException` was spelled out in both the engine's classifier and
the migration Lambda's fallback predicate, so a change to one copy would silently
drop the other back to `QueryFailed` and regress aws-devtools-labs#450 at deploy time. Both sites
now read `TRANSIENT_DATA_API_ERROR_NAMES` from `errors.ts`, which keeps error
classification knowledge in one file. The constant is internal: the entry points
re-export `DatabaseErrors` by name, so the public API report is unchanged.

One behavior delta: the Lambda's fallback now also accepts raw
`ServiceUnavailableException` / `InternalServerErrorException`, since it shares
the engine's set. Nothing routes around the engine today, and treating those as
retryable matches what the engine already does with them. The raw-name test
covers all three.

`BadRequestException` deliberately stays a separate check — the engine maps it to
`QueryFailed`, so it does not belong in a transient set.

Note this does not protect against AWS renaming an exception; that would still
fall through to `QueryFailed`. It removes the drift-between-copies failure mode.
…ws-devtools-labs#451)

`Communications link failure` is a JDBC message on a PostgreSQL-only BB, so it
reads like dead code, but it is the only check that catches a not-yet-ready
writer: the engine rewrites that error's name to `QueryFailed` and leaves the
message intact. The comment records that so it does not get removed as
PostgreSQL-irrelevant.
…evtools-labs#451)

`withRetry` re-throws the last error once the retryable attempts run out — the
trailing `throw new Error('unreachable')` only satisfies the return type — so
say so in the JSDoc. The error is re-thrown unwrapped, which is what puts the
real failure in the CloudFormation message rather than a generic timeout.
@sharonyajain
sharonyajain merged commit ed158cf into aws-devtools-labs:main Sep 6, 2026
28 checks passed
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.

5 participants