fix: sam local invoke leaks Docker container and temp directory on OOM - #9184
fix: sam local invoke leaks Docker container and temp directory on OOM#9184Adityaj0 wants to merge 1 commit into
Conversation
_on_invoke_done() called _check_exit_state(container) before _container_manager.stop(container) and _clean_decompressed_paths(). When a function is OOM-killed, _check_exit_state() raises ContainerFailureError, which propagated out of _on_invoke_done() before either cleanup step ran, leaking the stopped-but-not-removed container and the per-invocation decompressed-code temp directory on every OOM'd invocation. Wrap the check in try/finally so cleanup always runs regardless of whether _check_exit_state() raises. Fixes aws#9182
| self._check_exit_state(container) | ||
| finally: | ||
| if container: | ||
| self._container_manager.stop(container) |
There was a problem hiding this comment.
[RESOURCE_MANAGEMENT] The two cleanup steps run sequentially in the same finally block, so a failure in the first still skips the second — the same class of leak this PR fixes.
ContainerManager.stop() calls Container.stop() and Container.delete(), and both re-raise docker.errors.APIError unless the message matches the "removal of container ... is already in progress" special case (see samcli/local/docker/container.py). If that raises, self._clean_decompressed_paths() never runs, leaving the unzipped-code temp directory behind. It also replaces the in-flight ContainerFailureError, so an OOM'd invocation would surface an opaque Docker API error instead of "Container invocation failed due to maximum memory usage".
Nesting the cleanup makes both steps independent and preserves the original exception:
try:
if container:
self._check_exit_state(container)
finally:
try:
if container:
self._container_manager.stop(container)
finally:
self._clean_decompressed_paths()
Which issue(s) does this change fix?
Fixes #9182
Why is this change necessary?
LambdaRuntime._on_invoke_done()calls_check_exit_state(container)beforeself._container_manager.stop(container)andself._clean_decompressed_paths():_on_invoke_doneis invoked unconditionally frominvoke()'sfinallyblock, but has no try/except of its own. When a function is OOM-killed,_check_exit_state()raisesContainerFailureError, and that exception propagates immediately out of_on_invoke_done, skipping both_container_manager.stop(container)and_clean_decompressed_paths().Every OOM'd invocation of
sam local invoke(orstart-api/start-lambda) therefore leaves a stopped-but-not-removed Docker container and an unzipped-code temp directory on disk. This accumulates over repeated local-invoke testing, which is exactly the workflow most likely to trigger it (iterating onMemorySizetuning).How does this change work?
Wrap the exit-state check in a
try/finallyso the container is always stopped and the temp directory is always cleaned up, regardless of whether_check_exit_stateraises:The
ContainerFailureError(or any other exception from_check_exit_state) still propagates after cleanup runs — this only fixes cleanup, not the error-reporting behavior for OOM.What tests ran and what were the results?
Added a regression test,
test_on_invoke_done_stops_container_and_cleans_paths_even_when_check_exit_state_raises, intests/unit/local/lambdafn/test_runtime.py. It fails against the pre-fix code (stop()is never called when_check_exit_stateraises) and passes after the fix.Full unit suite (
tests/unit) passes: 9387 passed, 25 skipped.mypyon the changed file: clean.Checklist
make prpasses locally (unit tests + mypy on changed files; full localmake prtarget run viapytest tests/unit -qand scopedmypy)By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.