Skip to content

[SLES-3039] fix(traces): send oversized trace payloads alone instead of blocking the queue - #1418

Open
clifordshelton wants to merge 6 commits into
mainfrom
cliford/sles-3039-split-oversized-trace-payloads
Open

clifordshelton wants to merge 6 commits into
mainfrom
cliford/sles-3039-split-oversized-trace-payloads

Conversation

@clifordshelton

@clifordshelton clifordshelton commented Oct 9, 2026 •

Copy link
Copy Markdown

Overview

A trace payload larger than the trace aggregator's 12 MB batch cap (MAX_CONTENT_SIZE_BYTES) could never be sent. TraceAggregator::get_batch put it back at the front of the queue and returned an empty batch, and get_batches stops at the first empty batch. Every later trace in that execution environment then queued behind it, and the extension's memory grew until the Lambda ran out of memory (Runtime.OutOfMemory).

This happens with very large single traces. A trace with tens of thousands of spans can be well over 12 MB once the extension adds its tags to every span, and since #902 the extension accepts trace requests up to 50 MiB.

This PR changes get_batch so a payload that is larger than the cap on its own is sent as a batch by itself instead of being put back. This matches the Go trace-agent (appendChunks in pkg/trace/writer/trace.go), which flushes its buffer and always sends an oversized payload on its own, so one payload can never block the queue. If the intake rejects it, it is dropped after the existing retries, as in the Go agent.

Why no splitting: an earlier revision of this PR also split oversized payloads in the extension. Following review, that was reverted so bottlecap keeps matching the Go agent. Very large traces should be split by the tracer with partial flushing (DD_TRACE_PARTIAL_FLUSH_ENABLED=true), which is how APM handles them.

Testing

  • Click here to see metrics during the various reproduction tests
    • Before Changes - click here.
      • You'll see OOM errors on the versions 100 and 98.
      • No OOM erros happened on 87 in compatibility mode
    • After Changes (earlier revision of this PR that split payloads, since reverted) - click here
      • The dev version (Show sup as 101-next) has no OOM errors and memory usage didn't rise sharply as before to an unsafe level
    • v100 with DD_TRACE_PARTIAL_FLUSH_ENABLED=true - click here
      • No OOM errors, and the large traces arrived complete
    • This PR (send oversized payloads alone, without partial flushing) - click here
      • No OOM errors. The large traces were dropped by the intake, which these metrics don't show
  • Unit test: test_get_batch_oversized_payload_does_not_block_queue: an oversized payload between two small ones is sent as its own batch and the payloads behind it still go out. cargo test --lib (684 passed), cargo clippy --all-targets -- -D warnings and cargo fmt --check are clean.
  • Reproduction in a sandbox Lambda: a Go provided.al2023 arm64 function behind SQS (datadog-lambda-go, periodically,1000 flush, 1792 MB), with ~1,300-span traces and occasional ~35k-span traces, at about 2 invocations per second per execution environment:
Result
v100 OOMs: extension memory grew ~7.6 MB per invocation to 1.5–1.7 GB; trace batching stopped after the first oversized payload
v100 with DD_TRACE_PARTIAL_FLUSH_ENABLED=true no OOMs, extension peak ~500 MB, largest payload 10.3 MB, the large traces arrived complete (35,003 spans)
This PR, without partial flushing no OOMs; every execution environment ran the full test (1,087–1,309 invocations), extension peak ~740 MB. Oversized payloads (up to 27.7 MB) were sent on their own, rejected by the intake and dropped after retries, so the large traces did not arrive, as with the Go agent (v87 compatibility mode delivered none of them either). Batching continued throughout (651 batches)

🤖 Generated with Claude Code

A trace payload larger than the 12 MB batch cap was put back at the front
of the trace queue by `get_batch` and never sent, so every later payload
in that sandbox queued behind it until the Lambda ran out of memory.
Large traces (tens of thousands of spans) exceed the cap once the
extension adds its tags to every span.

- Split oversized payloads before queuing them: chunks move into separate
  payloads, and a chunk that is still too large has its spans split into
  several chunks of the same trace, as tracer partial flushing does.
  Trace-level `_dd.p.*` tags are copied onto the first span of each piece.
  A single span larger than the cap is dropped with a warning.
- `get_batch` now sends a payload that is over the cap on its own as a
  batch by itself instead of putting it back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@datadog-prod-us1-5

datadog-prod-us1-5 Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Pipelines

❌ Errors

Your PR has failed checks. Please review the issues below and take necessary action before merging.

🚦 1 Pipeline job failed

Bottlecap (Rust) | Audit

View more details · View in GitHub Actions

Useful? React with 👍 / 👎

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 833e9b8 | Docs | View more details | Give us feedback!

@clifordshelton
clifordshelton marked this pull request as ready for review October 10, 2026 00:16
@clifordshelton
clifordshelton requested review from a team as code owners October 10, 2026 00:16
@clifordshelton
clifordshelton requested review from lym953 and a balanced review from Copilot October 10, 2026 00:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The sizing reserve can incorrectly drop a near-limit span that would fit in a payload.

1 open finding
What changed in this PR

Splits oversized enriched trace payloads to prevent queue blocking and Lambda OOMs.

Changes:

  • Splits payloads by chunks or spans before aggregation.
  • Sends every split payload through trace and OTLP pipelines.
  • Adds an aggregator fallback for indivisible oversized payloads.
File Description
bottlecap/​src/​traces/​payload_split.rs Implements payload splitting and tests.
bottlecap/​src/​traces/​trace_processor.rs Integrates splitting and returns multiple builders.
bottlecap/​src/​traces/​trace_aggregator.rs Prevents oversized payloads from blocking the queue.
bottlecap/​src/​traces/​mod.rs Exposes the splitting module.
bottlecap/​src/​otlp/​agent.rs Queues all generated payload pieces.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread bottlecap/src/traces/payload_split.rs Outdated
The room for copied `_dd.p.*` tags was reserved from every span's budget,
although only the first span of each piece gains them and spans that
already carry them gain nothing. A span within a few bytes of the limit,
such as a large root span, could be dropped even though it fit.

Charge each span only for the tags it lacks, and only when it starts a
new piece.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@lucaspimentel

Copy link
Copy Markdown
Member

@DataDog review

@datadog-prod-us1-5 datadog-prod-us1-5 Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bits Code Review: PASS

More details

Protobuf framing and copied trace tags are included in the split budget, and both ingestion paths enqueue every resulting piece. The queue fallback prevents an oversized entry from blocking later traces.

Was this helpful? React 👍 or 👎

Open Bits AI session

🤖 Bits Code Review · Commit a64cb43

@lucaspimentel lucaspimentel left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found one metadata-preservation issue in the splitting logic. The unchanged splitter's existing tests pass, but focused regression probes for whole-chunk and span splitting both reproduce the loss described below.


Review by an AI agent

Comment thread bottlecap/src/traces/payload_split.rs Outdated
clifordshelton and others added 2 commits October 13, 2026 12:00
Tracers set `_dd.tags.process` only on the first span of a payload's first
chunk, so after splitting, only the payload holding that span carried the
process tags. Promote them to the payload-level tags before splitting, as
the Agent does, so every output inherits them and their size is counted.
An existing payload-level value takes priority.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Match the Go trace-agent, which never splits a trace payload: an oversized
payload is sent on its own. Large traces should be split by the tracer
with partial flushing (`DD_TRACE_PARTIAL_FLUSH_ENABLED`), which keeps them
complete and well under the batch cap.

This removes the payload splitting, the trace-level and process tag
propagation, and the `process_traces` return type change. What remains is
the `get_batch` change: a payload larger than the cap on its own is sent
as a batch by itself instead of being put back at the front of the queue,
where it blocked every later payload until the Lambda ran out of memory.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@lym953 lym953 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could the splitting utils be reused for other Serverless agents? If so, we may considering moving them to serverless-components.

Comment thread bottlecap/src/traces/payload_split.rs Outdated
let mut pieces = Vec::new();
let mut current = Vec::new();
let mut current_len = 0;
let mut dropped = 0;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: rename as dropped_cnt for clarity

@clifordshelton clifordshelton changed the title [SLES-3039] fix(traces): split trace payloads larger than the batch cap [SLES-3039] fix(traces): send oversized trace payloads alone instead of blocking the queue Oct 13, 2026
@lucaspimentel

Copy link
Copy Markdown
Member

@DataDog review

@datadog-prod-us1-5 datadog-prod-us1-5 Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bits Code Review: PASS

More details

Oversized payloads are emitted as standalone batches, allowing subsequent payloads to drain while preserving ordinary batching behavior. The completed static review identified no reportable regression.

Was this helpful? React 👍 or 👎

Open Bits AI session

🤖 Bits Code Review · Commit cb1806a

@lucaspimentel lucaspimentel left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks!

@clifordshelton

Copy link
Copy Markdown
Author

/merge

@gh-worker-devflow-routing-ef8351

gh-worker-devflow-routing-ef8351 Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

View all feedbacks in Devflow UI.

2026-10-14 14:49:23 UTC ℹ️ Start processing command /merge


2026-10-14 14:49:28 UTC ℹ️ MergeQueue: pull request added to the queue

The expected merge time in main is approximately 2h (p90).


2026-10-14 16:13:25 UTC 🚨 MergeQueue: This merge request is in error

mergequeue build completed successfully, but the github api returned an error while merging the pr.
It's probably because:

  • target branch of PR is restricted to only allow up-to-date branches, but the pr is now outdated
Details

Error: PUT https://api.github.com/repos/DataDog/datadog-lambda-extension/pulls/1418/merge: 405 6 of 6 required status checks are expected. [] (Request ID: BDDE:E6328:1A07042:55C7D46:6ACFAA1A)

FullStacktrace:
activity error (type: github.GithubService_MergePullRequest, scheduledEventID: 53, startedEventID: 54, identity: 1@github-worker-6b5859d974-22rwl@): PUT https://api.github.com/repos/DataDog/datadog-lambda-extension/pulls/1418/merge: 405 6 of 6 required status checks are expected. [] (Request ID: BDDE:E6328:1A07042:55C7D46:6ACFAA1A) (type: GitFailure, retryable: false): PUT https://api.github.com/repos/DataDog/datadog-lambda-extension/pulls/1418/merge: 405 6 of 6 required status checks are expected. [] (type: ErrorResponse, retryable: true)

@clifordshelton

Copy link
Copy Markdown
Author

/merge

@gh-worker-devflow-routing-ef8351

gh-worker-devflow-routing-ef8351 Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

View all feedbacks in Devflow UI.

2026-10-14 16:14:32 UTC ℹ️ Start processing command /merge


2026-10-14 16:14:38 UTC ℹ️ MergeQueue: Pull request is not mergeable yet

It will be processed automatically as soon as GitHub reports it as mergeable. View in MergeQueue UI.

  • Run /code blockers to see what is blocking it.
  • Run /remove to cancel it.

2026-10-14 20:32:09 UTC ⚠️ MergeQueue: This merge request was unqueued

devflow unqueued this merge request: It did not become mergeable within the expected time

lym953 added a commit that referenced this pull request Oct 14, 2026
…USTSEC-2026-0285 waiver (#1425)

## Problem

The "Bottlecap (Rust) / Audit" check fails on every branch, for example
#1418. `main` fails the same way. On 2026-10-14, RustSec published three
advisories for `rustls` 0.23.43, the version in `Cargo.lock`. `cargo
audit` reports them as vulnerabilities and exits with an error.

| Advisory | Problem | Fixed in |
|---|---|---|
| RUSTSEC-2026-0346 | The `Debug` output of client session values shows
TLS secrets | 0.23.46 |
| RUSTSEC-2026-0348 | Brotli certificate decompression can allocate 1
GiB before authentication | 0.23.46 |
| RUSTSEC-2026-0349 | A forged ServerHello can make later connections
use only classical key exchange | 0.23.46 |

## Why this PR ignores the advisories

We cannot use `rustls` 0.23.46 yet. The block is the same as for
RUSTSEC-2026-0285:

- On crates.io, `rustls` 0.23.46 requires `aws-lc-rs` 1.18 or later, the
same as 0.23.45.
- `aws-lc-rs` 1.18 uses AWS-LC-FIPS 4.x, which has no FIPS 140-3
certification yet. Our FIPS builds must use a certified crypto module.
For this reason, #1389 reverted the bump to 0.23.45, and `Cargo.toml`
keeps `rustls` below 0.23.45.
- The non-FIPS builds do not use `aws-lc-rs`, but both builds use the
same `Cargo.lock`, so they get the same `rustls` version.

## Impact

- **RUSTSEC-2026-0346** applies only to a custom client session store
that formats session values with `Debug`. The bottlecap source has no
custom session store.
- **RUSTSEC-2026-0348** applies only when the `brotli` feature of
`rustls` is on. `Cargo.lock` has no `brotli` crate, so the feature is
off.
- **RUSTSEC-2026-0349** can cause a loss of post-quantum protection for
some connections. Keys are not disclosed, and authentication is not
bypassed.

## Fix

Add the three IDs to the two ignore lists for `cargo audit`:

- `bottlecap/.cargo/audit.toml`, which local runs read.
- The `ignore` input of the audit step in `rs_ci.yml`. CI reads only
this list.

## Testing

### Steps

1. In `bottlecap/` on `main`, run `cargo audit --ignore
RUSTSEC-2026-0285`. This is the command that CI runs.
2. On this branch, run `cargo audit` with no flags. This reads
`.cargo/audit.toml`.
3. On this branch, run `cargo audit --json` with the four `--ignore`
flags from `rs_ci.yml`.

### Result

| Check | Before | After |
|---|---|---|
| `cargo audit`, CI ignore list | 3 vulnerabilities, exit with error | 0
vulnerabilities |
| `cargo audit`, `.cargo/audit.toml` | Not run | 0 vulnerabilities |

`cargo-audit` 0.22.1 ran on macOS. The CI Audit job on this PR is the
check for Linux.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants