Repository navigation
test(serve): stop unit tests resolving IAM roles over the network - #6496
mohamedzeidan2021 wants to merge 1 commit into
Conversation
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.
There was a problem hiding this comment.
🤖 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.
Problem
ModelBuilder.__post_init__resolves a default execution role when none is supplied, andresolve_and_validate_roledoes that withsts:GetCallerIdentityfollowed by a paginatediam:SimulatePrincipalPolicy. So every unit test that builds aModelBuilderwithoutrole_arnmakes 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:
…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-v3withSUBMODULE=sagemaker-serve(CloudWatch logs retrieved for 123 of 123 failures):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 constructModelBuilderwithout 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.pyandtest_ai_inference_recommender/test_model_builder_recommendations.pycarry the same fixture and have never thrown a throttle in 531 builds — a clean control for the mechanism. Putting it intests/unit/conftest.pycovers the package instead of one file at a time.Two details:
resolve_and_validate_roleis patched once per importing module.model_builder.py:63andbedrock_model_builder.py:39each bind it with a module-levelfrom ... import, so each holds its own reference — patching the definition site insagemaker.core.helper.iam_role_resolverwould 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).
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:GetObjectagainst 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.pygains 5 covered lines, because 23 tests now run to completion instead of aborting on an AWS error.sagemaker-core/helper/iam_role_resolver.pystop being covered by serve — they were reached only by making real IAM calls. 17 were already covered by that module's own tests.return Nonefor role types with no config path, which serve was exercising incidentally viarole_type="serving") is now covered deliberately by the newTestConfigDefaultRoleinsagemaker-core/tests/unit/helper/test_iam_role_resolver.py._resolve_config_default_rolehad no direct tests before.Scope
Test files only — no production code changes.
Note for reviewers
The deeper defect is that constructing a
ModelBuilderperforms a paginated IAM call at all. This change stops the bleeding; making role resolution lazy would be the cure, and is worth considering separately.