Skip to content

Make Ec2InstanceProvider an async context manager; default impl uses aiobotocore - #15

Draft
art-dsit wants to merge 2 commits into
issue-12-cleanupfrom
async-provider-lifecycle
Draft

Make Ec2InstanceProvider an async context manager; default impl uses aiobotocore#15
art-dsit wants to merge 2 commits into
issue-12-cleanupfrom
async-provider-lifecycle

Conversation

@art-dsit

@art-dsit art-dsit commented May 20, 2026

Copy link
Copy Markdown
Collaborator

SLOP ALERT

Summary

Stacked on top of #14. Once #14 lands, the base will switch to main.

Reshapes the Ec2InstanceProvider contract to acknowledge that providers are inherently loop-bound — its methods are already async def, and any async provider that uses aiobotocore / httpx clients holds resources that bind to the running event loop.

  • Ec2InstanceProvider is now an async context manager. __aenter__ is where loop-bound resources get initialized; __aexit__ releases them. The sandbox enters the registered provider in task_init and exits it in task_cleanup, so loop-bound clients have a well-defined lifetime tied to inspect_ai's task lifecycle. Sync providers may implement both as no-ops.
  • DefaultEc2InstanceProvider is converted to use aiobotocore for its EC2 / SSM control-plane calls so it no longer blocks the event loop. Clients are created in __aenter__ and closed in __aexit__; methods raise a clear RuntimeError if called on an un-entered provider.

Why

Before the lifecycle change, an inspect_ai.eval() run that uses a custom async provider was fine on its own — but anything that touched the provider on a different event loop (e.g. a second eval in the same process, or pytest-asyncio's per-function loops) failed with "Event loop is closed". The async-context-manager shape makes the loop boundary explicit: every entry creates a fresh client, every exit releases it.

The default provider's sync-boto3-in-async-def pattern was also blocking the event loop on every EC2 / SSM call. Converting to aiobotocore removes that. The runtime sandbox methods (exec, read_file, write_file) still use sync boto3 against SSM/S3 — out of scope here.

What changes

  • Protocol: add __aenter__ / __aexit__ to Ec2InstanceProvider.
  • DefaultEc2InstanceProvider: aiobotocore-based, lifecycle-managed clients, partial-failure cleanup preserved.
  • Ec2SandboxEnvironment: new _active_provider ClassVar; task_init enters the registered provider, task_cleanup exits it in a finally; a _entered async-context-manager helper yields the active provider when present and enters an ad-hoc one when not (so test / CLI code works without a task lifecycle).
  • New aiobotocore dependency, plus mypy override.
  • Unit tests for DefaultEc2InstanceProvider rewritten to mock the aiobotocore "async with create_client(...)" shape.
  • New test_eval_cleanup_on_setup_failure integration test runs a real inspect_ai.eval() with a setup script that exits non-zero, then asserts no instances leaked. This was unstable before the lifecycle change.

Test plan

  • uv run ruff check, uv run ruff format --check, uv run mypy — all pass.
  • uv run pytest -m \"not req_aws\" — 15 unit tests pass (8 cleanup + 6 provider + 1 schema).
  • uv run pytest -m req_aws tests/ec2sandboxtest/integration/ — 5 integration tests pass against real EC2 (~8 minutes), including the failing-setup eval.
  • No leaked instances after running the full suite multiple times.

🤖 Generated with Claude Code

art-dsit and others added 2 commits May 20, 2026 10:35
sample_cleanup(interrupted=True) now returns early — matching the
docker / k8s / proxmox sandboxes — and task_cleanup sweeps any
leftover tracked instances at the end of the task. This covers
Ctrl-C, setup-script failures, and any other path that fires
sample_cleanup with interrupted=True.

- Process-global tracker (set of (instance_id, region)) registers
  in sample_init, deregisters in successful sample_cleanup, drains
  in task_cleanup. Keyed only by (id, region) since inspect_ai
  always calls task_cleanup with task_name="shutdown".
- task_cleanup respects --no-sandbox-cleanup: clears the tracker
  but leaves instances running, and surfaces a hint pointing at
  `inspect sandbox cleanup ec2`.
- task_cleanup tolerates per-item terminate failures (one bad call
  doesn't block the rest).
- DefaultEc2InstanceProvider.create_instance terminates the
  launched instance if the post-launch wait (instance-running /
  SSM-ready) fails, so a cloud-init / SSM timeout doesn't leak.
- Pytest gains a `req_aws` marker (with --strict-markers) so the
  AWS-touching modules can be deselected via -m "not req_aws".
  Existing AWS-touching modules marked; the redundant
  `has_aws_creds` helper is removed.
- New `tests/ec2sandboxtest/integration/` package with four
  cleanup-behaviour scenarios (happy path, interrupted →
  task_cleanup sweep, --no-sandbox-cleanup, multiple samples with
  mixed cleanup paths).
- New `test_cleanup_unit.py` with mock-based unit tests for the
  tracker + sample/task cleanup contracts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…aiobotocore

Ec2InstanceProvider is now an async context manager:
``__aenter__`` is the place to initialize any loop-bound resources
(``aiobotocore`` / ``httpx`` clients bind to the event loop they
were created in) and ``__aexit__`` releases them. The sandbox enters
the registered provider in ``task_init`` and exits it in
``task_cleanup``, so loop-bound clients have a lifetime tied to
inspect_ai's task lifecycle.

DefaultEc2InstanceProvider converted to use ``aiobotocore`` for its
EC2 / SSM control-plane calls so it no longer blocks the event loop
during run_instances / wait-for-running / wait-for-SSM / terminate /
describe-instances. Clients are created in ``__aenter__`` and
closed in ``__aexit__``; methods raise a clear ``RuntimeError`` if
called on an un-entered provider.

Sandbox internals:
- New ``_active_provider`` ClassVar plus a ``_entered`` async
  context manager that yields the active provider when present and
  enters an ad-hoc one when not — tests and CLI cleanup work
  without going through task_init/task_cleanup.
- ``task_cleanup`` exits the active provider in a ``finally`` so
  loop-bound clients are released even if the cleanup body raises.

Tests:
- Unit tests for ``DefaultEc2InstanceProvider`` rewritten to mock
  the aiobotocore "async with create_client(...)" shape.
- Fake provider in ``test_cleanup_unit.py`` gains async-context-
  manager methods.
- New ``test_eval_cleanup_on_setup_failure`` integration test runs
  a real ``inspect_ai.eval()`` with a setup script that exits
  non-zero, then asserts no instances leaked. This was unstable
  before the lifecycle change because the previous provider client
  would bind to one event loop and break when reused on another.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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