Skip to content

test(serve): stop unit tests resolving IAM roles over the network - #6496

Open
mohamedzeidan2021 wants to merge 1 commit into
masterfrom
fix/serve-unit-tests-live-iam
Open

mohamedzeidan2021 wants to merge 1 commit into
masterfrom
fix/serve-unit-tests-live-iam

Conversation

@mohamedzeidan2021

Copy link
Copy Markdown
Collaborator

Problem

ModelBuilder.__post_init__ resolves a default execution role when none is supplied, and resolve_and_validate_role does that with sts:GetCallerIdentity followed by a paginated iam:SimulatePrincipalPolicy. So every unit test that builds a ModelBuilder without role_arn makes live IAM calls — which succeed only because CI credentials happen to be present.

IAM throttles on an account-wide quota, so those builds fail with:

botocore.exceptions.ClientError: An error occurred (Throttling) when calling the
SimulatePrincipalPolicy operation (reached max retries: 4): Rate exceeded

…on whichever test happened to be running when the limit hit.

Scale, from production

Over all 531 builds of sagemaker-python-sdk-ci-health-unit-test-v3 with SUBMODULE=sagemaker-serve (CloudWatch logs retrieved for 123 of 123 failures):

Window since 2026-09-23
Decided builds 184 (142 pass / 42 fail)
Observed pass rate 77.2%
Throttling share of failures 37 of 42 (88%)
Pass rate with throttling removed 97.3%
Residual failures 5, all public-PyPI download flake
Genuine test failures 0

100% of throttling in that window — 37 of 37 builds, 90 of 90 per-env instances — comes from tests/unit/test_model_builder_methods.py, the one remaining file whose tests construct ModelBuilder without a role and without stubbing the resolver. All 23 of its unprotected tests have thrown it in production. Cost so far: roughly 62 build-hours, each needing a developer re-run.

The failures are not reproducible. Each build runs the suite three times (py310/py311/py312). Throttling failures hit only one of the three 89% of the time; genuine failures hit all three 100% of the time. Independently, of 36 commits built more than once, 22 produced both a pass and a fail — 17 of those were throttling, and none were genuine.

Why a conftest

This generalises the fixture #6272 added to test_model_builder_coverage_boost.py, which drove that file from 83% of throttling occurrences to zero. test_bedrock_model_builder.py and test_ai_inference_recommender/test_model_builder_recommendations.py carry the same fixture and have never thrown a throttle in 531 builds — a clean control for the mechanism. Putting it in tests/unit/conftest.py covers the package instead of one file at a time.

Two details:

  • The stub passes the provided role through rather than returning a fixed value, matching the real contract. Four of the five call sites pass a real role, and tests assert on what comes back.
  • resolve_and_validate_role is patched once per importing module. model_builder.py:63 and bedrock_model_builder.py:39 each bind it with a module-level from ... import, so each holds its own reference — patching the definition site in sagemaker.core.helper.iam_role_resolver would rebind neither.

Tests that patch the resolver themselves still win, since their patch is applied inside the fixture's and rebinds the attribute last.

Verification

Measured with deliberately invalid credentials and a transport-level request recorder with a positive control, so a silent failure to record cannot be mistaken for absence of traffic (it reports 23 requests from 23 tests without this change, 0 with it).

Before After
serve unit suite 67 failed, 2087 passed 44 failed, 2110 passed
Newly broken tests — 0
Tests issuing STS calls 24 1
STS/IAM errors in the run present 0

The single remaining STS caller is the telemetry daemon thread, attributed outside any test and unrelated to this change.

The 44 remaining failures are all s3:GetObject against the JumpStart cache. Those succeed in CI and cause no CI failures — they only fail locally because invalid credentials break S3 before IAM is reached. Left for a separate change.

Coverage held whole

  • serve/model_builder.py gains 5 covered lines, because 23 tests now run to completion instead of aborting on an AWS error.
  • 18 lines of sagemaker-core/helper/iam_role_resolver.py stop being covered by serve — they were reached only by making real IAM calls. 17 were already covered by that module's own tests.
  • The 18th (the early return None for role types with no config path, which serve was exercising incidentally via role_type="serving") is now covered deliberately by the new TestConfigDefaultRole in sagemaker-core/tests/unit/helper/test_iam_role_resolver.py. _resolve_config_default_role had no direct tests before.

Scope

Test files only — no production code changes.

Note for reviewers

The deeper defect is that constructing a ModelBuilder performs a paginated IAM call at all. This change stops the bleeding; making role resolution lazy would be the cure, and is worth considering separately.

ModelBuilder.__post_init__ resolves a default execution role when none is
supplied, and resolve_and_validate_role does that with sts:GetCallerIdentity
followed by a paginated iam:SimulatePrincipalPolicy. Every unit test that
builds a ModelBuilder without role_arn therefore makes live IAM calls, which
succeed only because CI credentials are present.

IAM throttles on an account-wide quota, so those builds fail with

  Throttling ... SimulatePrincipalPolicy (reached max retries: 4): Rate exceeded

on whichever test happened to be running. Over the 184 decided builds of
sagemaker-python-sdk-ci-health-unit-test-v3 with SUBMODULE=sagemaker-serve
since 2026-09-23, throttling accounts for 37 of 42 failures, and 100% of it
(37/37 builds, 90/90 per-env instances) comes from
tests/unit/test_model_builder_methods.py -- the one remaining file whose tests
construct ModelBuilder without a role and without stubbing the resolver. All
23 of its unprotected tests have thrown it in production.

The failures are not reproducible: each build runs the suite three times
(py310/py311/py312) and these hit only one of the three 89% of the time, while
genuine failures hit all three every time.

This generalises the fixture 5432663^..5a65c55 ("fix PR check tests", #6272)
added to test_model_builder_coverage_boost.py, which drove that file from 83%
of throttling occurrences to zero. test_bedrock_model_builder.py and
test_ai_inference_recommender/test_model_builder_recommendations.py carry the
same fixture and have never thrown a throttle in 531 builds. Moving it into
tests/unit/conftest.py covers the package rather than one file at a time.

Two details worth noting:

  - The stub passes the provided role through rather than returning a fixed
    value, matching the real contract. Four of the five call sites pass a real
    role, and tests assert on what comes back.
  - resolve_and_validate_role is patched once per importing module. model_builder
    and bedrock_model_builder each bind it with a module-level `from ... import`,
    so each holds its own reference and patching the definition site in
    sagemaker.core.helper.iam_role_resolver would rebind neither.

Tests that patch the resolver themselves still win, since their patch is applied
inside the fixture's and so rebinds the attribute last.

Verified with deliberately invalid credentials plus a transport-level request
recorder (with a positive control, so a silent failure to record cannot be read
as absence of traffic):

  - serve unit suite: 67 failed -> 44 failed, 2087 -> 2110 passed
  - newly broken tests: 0
  - tests issuing STS calls: 24 -> 1 (the remaining one is the telemetry
    daemon thread, attributed outside any test, and is unrelated)
  - STS/IAM errors in the run: 0

The 44 remaining failures are all s3:GetObject against the JumpStart cache.
Those succeed in CI, so they produce no CI failures and are left for a separate
change.

Coverage is held whole. serve/model_builder.py gains 5 covered lines because 23
tests now run to completion. 18 lines of sagemaker-core's iam_role_resolver stop
being covered by serve, having previously been reached only by making real IAM
calls; 17 were already covered by that module's own tests, and the new
TestConfigDefaultRole covers the last one (the early return for role types with
no config path, which serve was exercising incidentally via role_type="serving").

No production code changes.

@github-actions github-actions 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.

🤖 AI code review

This PR adds a package-level conftest.py for sagemaker-serve unit tests that stubs resolve_and_validate_role at both module import sites (model_builder and bedrock_model_builder) via an autouse fixture, keeping IAM/STS calls off the network to eliminate SimulatePrincipalPolicy throttling flakiness. It also adds unit tests for the new _config_path_for_role_type / _resolve_config_default_role helpers in iam_role_resolver. The change is test-only, well-reasoned, and consistent with prior precedent (PR #6272). I found no correctness issues: all serve call sites invoke the resolver with keyword arguments that the stub's **kwargs safely absorbs, the config-default short-circuit test patches the correct definition site, and the autouse-fixture ordering claim holds. No blocking findings.

Reviewed commit 120badd. Automated review; verify before acting.

This branch was successfully deployed

1 active deployment
auto-approve — 120badd7 Deployed Oct 16, 2026 by mohamedzeidan2021 via wait-for-approval #2056
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.

1 participant