Repository navigation
Conversation
…g parallel runs When running integration tests in parallel across a pool of regions (`--parallel-regions`), the scheduler assigned each test to any region in the pool and ignored the per-test `regions` allowlist declared in the test's integ manifest. Region-constrained tests (for example, tests using instance types or services only offered in some regions) could therefore be deployed in a disallowed region and fail. Schedule each test only into pool regions that fall within its declared `regions` allowlist. A region-constrained test is taken before an unconstrained one when a worker picks up work, so it is not starved by an unconstrained test grabbing its only eligible region. A test whose allowlist does not intersect the pool at all is reported as an error rather than being run in a disallowed region or silently skipped. Tests that declare no `regions` are unaffected and may run in any pool region.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #2102 +/- ##
=======================================
Coverage 92.08% 92.08%
=======================================
Files 83 83
Lines 13246 13246
Branches 1886 1886
=======================================
Hits 12197 12197
Misses 1011 1011
Partials 38 38
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Automated review A maintainer will still review this — treat the notes below as a starting point. 📌 Commit This PR fixes a scheduling bug in The change is well-scoped, additive to the diagnostic model, and backward-compatible for tests that declare no 🔴 0 blocking · 🟡 0 recommended · ⚪ 0 optional Generated automatically. React 👍 or 👎 to tell us whether this review helped, so we can improve these reviews. |
There was a problem hiding this comment.
See the review summary comment for the overview; the notes below are inline.
| let testCases; | ||
| try { | ||
| testCases = IntegManifestReader.fromPath(IntegTest.hydrate(test).snapshotDir).tests.testCases; | ||
| } catch { | ||
| // No readable manifest => treat as unconstrained, preserving prior behavior | ||
| return undefined; |
There was a problem hiding this comment.
🟡 Recommended — The try/catch around IntegManifestReader.fromPath(...) swallows every read/parse failure and returns undefined (unconstrained), so the test is then schedulable into any pool region. IntegManifestReader distinguishes two cases: a missing manifest (a legacy or brand-new test, the case the comment intends to preserve) and a present-but-invalid manifest. Collapsing them means a test whose integ.json is corrupt silently loses its regions restriction and can deploy to a region it meant to exclude — the exact failure this change exists to prevent, reintroduced for the malformed-manifest case (AGENTS.md#error-handling).
Suggested change: Catch only the missing-manifest error (e.g. NoManifestError) and return undefined for it, letting a manifest-load/parse error propagate — or report it as a TEST_ERROR for that test — so a broken manifest is not silently treated as "no region restriction."
…ng region restriction Addresses review feedback on aws#2102. - Distinguish a missing manifest (`NoManifestError` -> a legacy or brand-new test, correctly unconstrained) from a present-but-invalid manifest. A corrupt manifest is now reported as a `TEST_ERROR` for that test rather than being silently treated as "no region restriction" (which would reintroduce the bug this change prevents). - Give a test whose test cases declare disjoint `regions` its own clear message instead of an empty-bracket region list. Adds unit tests and fixtures for both cases.
|
Thanks for the review. Addressed in
Full package build (compile + unit tests + eslint) passes. |
| /** | ||
| * Index of the last element matching `predicate`, or -1 if none match. | ||
| */ | ||
| function lastIndexMatching<A>(xs: A[], predicate: (x: A) => boolean): number { |
There was a problem hiding this comment.
what's wrong with Array.prototype.findLastIndex()?
There was a problem hiding this comment.
Agreed it would read cleaner. The catch is the lib target: findLastIndex is an ES2023 array method, and this package compiles against es2022 (via the shared toolkitLibTsCompilerOptions), so it doesn't typecheck here — tsc returns TS2550 suggesting es2023. I kept a small local helper to avoid a repo-wide lib bump.
Happy to switch to the stdlib method if you'd prefer — that'd mean adding es2023.array to this package's lib in .projenrc.ts (it's currently shared with toolkit-lib, so I'd give integ-runner its own compiler options). Let me know which you'd rather have.
| printResults({ | ||
| reason: DiagnosticReason.TEST_ERROR, | ||
| testName: testNameFromInfo(test), | ||
| message: 'test cases declare disjoint `regions` allowlists, so no single region can run them all; reconcile the regions declared across the test cases', |
There was a problem hiding this comment.
please make this error more human friendly. even I needed to think 3 times what that means.
There was a problem hiding this comment.
Reworded in eee93b7 to:
This test's test cases each allow a different set of regions with no region in common, so there is no single region that can run all of them. Update the test so its test cases share at least one allowed region.
| printResults({ | ||
| reason: DiagnosticReason.TEST_ERROR, | ||
| testName: testNameFromInfo(test), | ||
| message: `could not read integ manifest to determine allowed regions: ${e instanceof Error ? e.message : String(e)}`, |
There was a problem hiding this comment.
Nit: there's probably not much to improve here, but still feel this could be more human friendly and less technical
There was a problem hiding this comment.
Reworded in eee93b7 to:
Could not read this test's snapshot (integ.json) to find out which regions it supports: <underlying error>
| printResults({ | ||
| reason: DiagnosticReason.TEST_ERROR, | ||
| testName: testNameFromInfo(test), | ||
| message: `test restricts deployment to regions [${declared.join(', ')}], none of which are in the deployment region pool [${options.regions.join(', ')}]; add one of these regions to --parallel-regions to run this test`, |
There was a problem hiding this comment.
Nit: there's probably not much to improve here, but still feel this could be more human friendly and less technical
There was a problem hiding this comment.
Reworded in eee93b7 to:
This test can only run in these regions: <allowed>. None of them are available in this run (<pool>). Add one of the test's regions to --parallel-regions to run it.
| function declaredRegions(test: IntegTestInfo): string[] | undefined { | ||
| let testCases; | ||
| try { | ||
| testCases = IntegManifestReader.fromPath(IntegTest.hydrate(test).snapshotDir).tests.testCases; |
There was a problem hiding this comment.
I think this has a correctness bug: If I understand this correctly, the code is reading the regions from the committed snapshot, not the latest test case code. By definition the snapshot might be stale and thus attempt to schedule the test in an incorrect region. Because the test case code isn't consulted at all, it will attempt this incorrect deployment and then probably fail with an obscure error.
I think this we might be better of piggy-backing on messages going back and forth from the scheduler and the worker:
- the scheduler schedules the test for a region
- the test checks the region it has been scheduled in is actually supported
- if YES, proceed
- if NO, send a message back to the scheduler with the supported regions
- the scheduler reacts to the message coming back and re-schedules the tests for a supported region (or fails the test if none available)
Of course this will introduce an extra round-trip for very region-limited test cases. We can decide if reading the snapshotted manifest is a good approximation for the regions likely to be allowed. The scheduler would read from the snapshot and speculatively pick one of those regions. Though the same can then happen in reverse: A snapshotted region is not actually allowed and then we get the extra round-trip. Not sure if this optimisation is worth it then.
There was a problem hiding this comment.
Good catch — you're right that the snapshot can be stale relative to the test source, and the authoritative regions are only known after synth. I implemented the round-trip you described so you can see the shape and weigh in (ae80728):
- Scheduler still does a speculative first pick from the snapshot's
integ.json— so for the common case (snapshot in sync) there's no extra round-trip. - Worker validates after synth, before deploy: in
runIntegTestCase, right afteractual.synthForDeployment(), if the assigned region isn't in the freshly-synthesized app'sregions, the runner throwsWrongRegionError. Nothing is deployed in the wrong region. - Report back + reschedule: the worker emits a
WRONG_REGIONdiagnostic carrying the freshsupportedRegions; the scheduler updates that test's eligible regions from the authoritative data and reschedules it — reusing the exact machinery that already handlesNOT_BOOTSTRAPPED(diagnostic →onhandler → retry queue). It converges because the retry only goes to a now-known-supported region, so the worker won't re-throw. - The per-test-case intersection is now a shared
intersectTestCaseRegionshelper used by both the snapshot read and the fresh-synth check, so the two paths agree on semantics.
So the extra round-trip happens only when the snapshot and the synthesized app disagree — which is exactly the stale case you flagged. This matches the "snapshot as a good approximation, reconcile on mismatch" option from your comment.
Two things I'd like your read on:
- Is this the shape you had in mind, or would you rather the scheduler not speculate at all (e.g. a cheaper pre-pass)? I kept speculation because synthesizing every test up front to learn its regions would serialize what is meant to be parallel.
- The worker throws before deploy, so there's no wasted deployment — but a region-limited test that the snapshot placed wrong does pay one synth + reschedule. Given how rarely snapshots are stale for
regions, that seemed an acceptable cost. Agree?
Tests added: the intersection helper, the worker emitting WRONG_REGION (direct and wrapped-as-cause), and the scheduler rescheduling a stale-snapshot test (snapshot says us-east-1, fresh synth supports only us-east-2) into its supported region.
There was a problem hiding this comment.
Nice, this is pretty much what I expected to happen.
1+2) That's the same issue to me. It's your call to make. I don't know what's more likely. Can you do some back-of-the-napkin maths on what the actual cost of each option is? I'm quite sure the proposed solution is better though. So in principle I agree, ideally we have more due diligence.
My Agent also found one other thing that seems like a legit find:
One thing still needs to change: the up-front checks in
runIntegrationTestsInParallelstill fail tests based on the committed snapshot. When the snapshot's regions don't overlap the pool, its test cases have no region in common, orinteg.jsoncan't be parsed, the test gets a finalTEST_ERRORbefore anything is synthesized. That's the same stale-data problem in a different place. For example, if a PR changesregions: ['ap-northeast-3']to['us-east-1']and we run with--parallel-regions us-east-1, the test fails with "None of them are available in this run", even though the new code would run fine.Since the worker now has the authoritative answer, I'd treat the snapshot only as a scheduling hint. In those three cases, queue the test as unconstrained and let
WRONG_REGIONsort it out. If the fresh regions really don't fit the pool, the test is still reported, just one synth later. Because the runner check becomes the source of truth, it also needs a test of its own. Right now the worker tests mockrunIntegTestCaseand the scheduler test uses the mock worker, so nothing checks thatIntegTestRunnerthrowsWrongRegionErrorbefore it deploys.
… feedback) Addresses review nits on aws#2102: reword the three region-scheduling error messages (unreadable snapshot, disjoint test-case regions, no eligible region in the pool) to be clearer and less technical. No behavior change.
… app and reschedule Addresses review feedback on aws#2102 (stale-snapshot correctness). The scheduler assigns a region from the committed snapshot's integ.json, which can be stale relative to the test source. A test could still be deployed in a region it no longer supports. Add a worker-side guard that runs after synthesis (where the authoritative region allowlist is known) and before deployment: if the assigned region is not in the freshly synthesized app's regions, the runner throws WrongRegionError and the worker emits a WRONG_REGION diagnostic carrying the supported regions. The scheduler reacts by updating the test's eligible regions from that authoritative data and rescheduling it, reusing the same retry machinery as NOT_BOOTSTRAPPED. Snapshot-based scheduling remains the speculative first pick, so the extra round-trip only happens when the snapshot and the synthesized app disagree. - Factor the per-test-case region intersection into the shared `intersectTestCaseRegions` helper (used by both the scheduler's snapshot read and the worker's fresh-synth check). - Add WRONG_REGION diagnostic reason, `supportedRegions` on Diagnostic, and the WrongRegionError type. Adds unit tests for the intersection helper, the worker emitting WRONG_REGION (direct and wrapped-as-cause), and the scheduler rescheduling a stale-snapshot test into its supported region.
Closes #2101
Reason for this change
The parallel integration-test scheduler assigned each test to a region from the
--parallel-regionspool round-robin and ignored the per-testregionsallowlist declared ininteg.json. Region-constrained tests could be deployed in a disallowed region and fail (see #2101 for a realinf2/ap-northeast-3failure).Description of changes
runIntegrationTestsInParallel(lib/workers/integ-test-worker.ts) now:regionsfrom its integ manifest (intersection across test cases when more than one is constrained);TEST_ERRORwith an actionable message, instead of running it in a disallowed region or silently skipping it.Tests that declare no
regionsare unaffected and may run in any pool region.Behavior change
A region-constrained test whose allowed regions are not in the active
--parallel-regionspool now fails fast (with a message telling the user to add one of its regions to--parallel-regions) instead of running — incorrectly — in a region it excluded. This is the intended meaning ofregions, but is a visible change for anyone who previously relied on the allowlist being ignored.Description of how you validated changes
Added unit tests in
test/workers/integ-worker.test.ts:Full package build (
projen build) passes: compile, all unit tests, eslint, and package/bundle.Checklist
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license