Skip to content

perf(core): build the boto3 clients on first use, not at import - #31

Merged
neosun100 merged 1 commit into
mainfrom
perf/lazy-boto3-clients
Aug 4, 2026
Merged

perf(core): build the boto3 clients on first use, not at import#31
neosun100 merged 1 commit into
mainfrom
perf/lazy-boto3-clients

Conversation

@neosun100

Copy link
Copy Markdown
Contributor

A fifth sweep read the suite's own --durations output. Every subprocess test was paying a flat ~4.5s that was not work:

import sentinel_harness      4.50s at 7% CPU     (not computing — waiting)
python -c "pass"             0.03s

-X importtime put 4.15s of self time in sentinel_harness.core: two module-level boto3.client(...) calls.

My first attribution was wrong, and my own control test caught it

I assumed service-model parsing (153 operations). The control asserted "eager construction costs > 2s" and failed, reporting 0.09s — because inside pytest the credential chain is already satisfied by fake keys and short-circuits. Measured properly, in matched environments:

environment cost
no credentials 4.46s — the full chain, ending at IMDS
fake credentials 0.31s — first provider hits
AWS_EC2_METADATA_DISABLED=true 0.32s

The cost is the instance-metadata probe timing out. So importing this library made it reach for 169.254.169.254 — the exact address its own INV-EGRESS family exists to refuse.

That reframes the finding: it is a correctness and least-surprise defect that happened to show up as slowness.

Three consequences, the third mattering most

  1. 26 subprocess tests × 4.5s ≈ 115s of a 202s suite, building clients nothing used.
  2. Importing a library read ~/.aws/ and probed the network. A library must not.
  3. It shaped the tests: real clients existed before any test ran, so the only way to substitute a fake was to patch the module global afterwards — which ~48 files do.

The fix

_LazyClient defers construction to first attribute access. Deliberately a proxy, not a function, so nothing about the call sites or the ~48 patching tests changes.

before after
import sentinel_harness 4.50s 0.26s (17×)
full suite 202s 107s (1.9×)
scenario + CLI E2E layer 107s 8.9s (12×)

set_region also stays lazy: the CLI's --region flag runs before any AWS work — including for the offline detection commands that never touch AWS — so building there would put the cost back on every command's startup path.

Two of my own mistakes, both recorded in the code

  • The first proxy used __slots__ and broke test_gateway.py::test_scenario_named_supervisor_imports_without_aws, which patches a method on the client rather than replacing it. That is a legitimate pattern — it proves a scenario import makes no AWS call — so the proxy must support it. __slots__ removed, with the reason.
  • Any was used in annotations without importing it. It ran only because from __future__ import annotations makes annotations strings; mypy gates core.py, so it is now imported properly.

Testing

tests/test_lazy_clients.py pins both properties: import is under budget, no client is built at import, and boto3.client is forbidden during import of the package + gateway + registry_live + mcp_server. Plus six transparency tests covering every pattern the ~48 existing files rely on.

Mutation-tested 4/4 — reverting to eager construction, making set_region eager, breaking the cache, and hardcoding the region are each caught.

  • 3725 passed / 6 skipped in BOTH fixed and random order
  • installed-wheel E2E 8/8 · scenario E2E 25/25 · README CLI E2E 13/13
  • IaC E2E: tsc exit 0 · 8/8 stack tests · cdk synth 9 stacks
  • ruff clean · both mypy gates clean · make ci green

A fifth sweep read the suite's own `--durations` output. Every subprocess test paid a flat
~4.5s that was not work:

    import sentinel_harness      4.50s at 7% CPU     (not computing — waiting)
    python -c "pass"             0.03s

`-X importtime` put 4.15s of self time in `sentinel_harness.core`: two module-level
`boto3.client(...)` calls.

MY FIRST ATTRIBUTION WAS WRONG, and the control test I wrote is what caught it. I assumed
service-model parsing (153 operations). That test asserted "eager construction costs > 2s"
and FAILED, reporting 0.09s — because inside pytest the credential chain is already
satisfied by fake keys and short-circuits. Measured properly, in matched environments:

    no credentials                   4.46s    the full chain, ending at IMDS
    fake credentials                 0.31s    the first provider hits
    AWS_EC2_METADATA_DISABLED=true   0.32s

The cost is the **instance-metadata probe timing out**. So importing this library made it
reach for 169.254.169.254 — the exact address its own INV-EGRESS family exists to refuse.
That reframes the finding: it is a correctness and least-surprise defect that happened to
show up as slowness.

Three consequences, the third mattering most:
  1. 26 subprocess tests x 4.5s ~ 115s of a 202s suite, building clients nothing used.
  2. Importing a library read ~/.aws/ and probed the network. A library must not.
  3. It shaped the tests: real clients existed before any test ran, so the only way to
     substitute a fake was to patch the module global afterwards — which ~48 files do.

`_LazyClient` defers construction to first attribute access. Deliberately a proxy, not a
function, so nothing about the call sites or the ~48 patching tests changes.

    import sentinel_harness       4.50s -> 0.26s   (17x)
    full suite                     202s -> 107s    (1.9x)
    scenario + CLI E2E layer       107s -> 8.9s    (12x)

`set_region` also stays lazy: the CLI's --region flag runs before any AWS work, including
for the offline detection commands that never touch AWS, so building there would put the
cost back on every command's startup.

Two of my own mistakes, both recorded in the code:
  - The first proxy used `__slots__` and broke
    `test_gateway.py::test_scenario_named_supervisor_imports_without_aws`, which patches a
    method ON the client rather than replacing it. That is a legitimate pattern — it proves
    a scenario import makes no AWS call — so the proxy must support it. `__slots__` removed.
  - `Any` was used in annotations without importing it. It ran only because
    `from __future__ import annotations` makes annotations strings; mypy gates core.py, so
    it is now imported properly.

tests/test_lazy_clients.py pins both properties: import is under budget, no client is built
at import, and boto3.client is FORBIDDEN during import of the package + gateway +
registry_live + mcp_server. Plus six transparency tests covering every pattern the ~48
existing files rely on. Mutation-tested 4/4: reverting to eager construction, making
set_region eager, breaking the cache, and hardcoding the region are each caught.

Tested: 3725 passed / 6 skipped in BOTH fixed and random order; installed-wheel E2E 8/8;
scenario E2E 25/25; README CLI E2E 13/13; IaC E2E tsc 0 / 8-of-8 / synth 9 stacks; ruff
clean; both mypy gates clean; make ci green.
@neosun100
neosun100 merged commit 06538e4 into main Aug 4, 2026
14 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.

1 participant