Repository navigation
Conversation
…s JoinSet The listener that serves the tracer's start-invocation and end-invocation calls spawns each call's work into a JoinSet, which keeps every finished task until it is joined. The set was joined only at shutdown, so it grew by two tasks per invocation for the life of the execution environment. Remove finished tasks before each spawn. Fixes #1496 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
🟢 Approval recommended
No active review issues are reported for the lifecycle-listener task cleanup change.
No open issues 🎉
What changed in this PR
Fixes lifecycle-listener memory growth during warm Lambda invocations with universal instrumentation, addressing the retained tasks described in #1496.
Changes:
- Removes completed tasks from the listener’s
JoinSetbefore spawning start- and end-invocation work. - Logs task errors during cleanup while preserving shutdown handling for running tasks.
- Adds a regression test covering task retention across 100 start/end request pairs.
| File | Description |
|---|---|
bottlecap/src/lifecycle/listener.rs |
Adds completed-task cleanup to both invocation handlers and tests that retained task counts remain bounded. |
Review effort: Balanced
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
With universal instrumentation on, the extension's memory grows on every warm invocation and is never freed. Universal instrumentation is the default for Go. A customer reported in #1496 that several 1024 MB Go functions reach their memory limit after an execution environment serves about 10,000 to 20,000 invocations. They saw the growth on v94 and v101. With
DD_UNIVERSAL_INSTRUMENTATION=false, the memory stays flat.Cause
With universal instrumentation, the tracer calls the extension's
/lambda/start-invocationand/lambda/end-invocationendpoints once each per invocation. The extension's listener for these calls puts the work for each call into a TokioJoinSet. AJoinSetkeeps each finished task until something joins it, and the listener joins only at shutdown. So the set grows by two tasks per invocation for the life of the execution environment. #796 added this in August 2025.This PR
Before each new task, the listener removes the tasks that have finished, with
JoinSet::try_join_next(). The listener still joins the tasks that are running at shutdown.The proxy in front of the Lambda Runtime API has the same pattern. A separate PR fixes it.
Testing
Unit test
Steps
cargo test --lib lifecycle::listener.Result
The new test sends 100 start and end pairs through the listener and counts the tasks left in the set.
Lambda
Steps
datadog-lambda-gov1.32.0,provided.al2023, arm64, 1024 MB, reserved concurrency 1. Use their settings: payload capture on, X-Ray merge on, X-Ray active tracing.VmRSSof thedatadog-agentprocess from/proc/<pid>/statusin the handler.Result
mainwithout this PRmainwith this PRThe two
mainbuilds are the same commit, so the difference between them comes from this PR. With this PR, the extension's RSS stays between 76 and 89 MB from invocation 2,000 to 6,000. With universal instrumentation off, it stays between 73 and 83 MB over the same range. Only the Go tracer was tested.Open questions
🤖 Generated with Claude Code