Skip to content

test: process-level fault-injection harness and the contract tests that pass today - #1473

Open
duncanista wants to merge 3 commits into
jordan.gonzalez/test-fixtures/featurefrom
jordan.gonzalez/fault-harness/feature
Open

duncanista wants to merge 3 commits into
jordan.gonzalez/test-fixtures/featurefrom
jordan.gonzalez/fault-harness/feature

Conversation

@duncanista

@duncanista duncanista commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

TL;DR: Tests that run the real bottlecap binary against a fake Lambda API and an intake that fails on purpose. This PR adds the harness and the 10 checks that pass today.

Why: Our worst bugs (hangs past the invocation deadline, SIGKILL at shutdown, lost data on intake errors) show up only at the process level, so unit tests cannot catch them. Each later fix adds its own test here.

Overview

Jira: SVLS-9978

Stacked on #1472 (jordan.gonzalez/test-fixtures/feature).

Contract tests that run the real bottlecap binary against a fake Lambda control plane and a
Datadog intake that fails on demand. Each test asserts what Lambda or the customer sees:
/next before the deadline, exit on SHUTDOWN, no panic. See bottlecap/tests/HARNESS.md.

  • tests/common/: fake Extensions and Telemetry API, fault intake (blackhole, SYN blackhole,
    slow, 5xx, 429, close mid body, size limits), process wrapper with RSS and CPU, port guard.
  • 4 test binaries with the 10 tests that pass on main today: invoke then shutdown exits
    cleanly; a 503 storm does not hold /next; an oversized trace is dropped and later traces
    flow; an intake that closes mid body does not hold /next; 4 config fields with a wrong type
    cost only that field; ports 8124 and 8126 held by another process.
  • bottlecap/.config/nextest.toml: the 4 binaries run one test at a time (fixed-ports
    group), because the binary binds fixed ports.

No production code and no Cargo change. The tests for the open bugs come in a later PR, each
ignored with the backlog item that fixes it.

Size: 20 core lines, 2184 lines in total.

Testing

  • applies on the previous PR of the stack: pass
  • cargo fmt --all -- --check: pass
  • cargo clippy --workspace --all-targets --features default -- -D warnings: pass
  • cargo clippy --workspace --all-targets --features default,test-mode -- -D warnings: pass
  • cargo nextest run --workspace: pass
  • plain cargo test --test <binary> for the 4 harness binaries: pass
  • the 4 test files hold exactly the 10 passing tests, no #[ignore], no backlog reference: pass
  • Linux arm64 in Docker (rust 1.93.1), on the full harness at the end of the stack: the same 10 tests pass.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-29T00:51:23.972846Z 399b90b New commits
🔒 Security Review ✅ Completed 2026-10-29T00:46:32.607208Z 399b90b New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f0e4939146

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread bottlecap/tests/fault_injection_test.rs Outdated
);

s.intake.set_mode(FaultMode::Status(503));
let (released, took) = end_invocation_and_wait(&s, "req-1", 2, secs(10)).await;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Measure /next against the invocation deadline

When retries consume nearly the full invocation window, this test can still pass after Lambda's deadline: the event gets a 10-second deadline in Scenario::invoke, but end_invocation_and_wait starts its timer only after both telemetry requests complete and then grants another 10 seconds here. Measure elapsed time from when the INVOKE is queued, or compute the remaining absolute deadline, so a late /next is detected.

Useful? React with 👍 / 👎.

Comment thread bottlecap/tests/common/harness.rs Outdated
Comment on lines +118 to +119
self.api.push_shutdown("spindown", secs(2));
bc.wait_for_exit(bottlecap_process::EXIT_BUDGET).await

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Enforce the SHUTDOWN event deadline

If the extension takes between 2 and 8 seconds to terminate, every shutdown_cleanly assertion still passes even though the generated SHUTDOWN event advertises only a 2-second deadline and Lambda may terminate the environment at that point. Derive the wait budget from the event deadline, or separately assert that exit occurred within two seconds, so these lifecycle tests catch shutdown overruns.

Useful? React with 👍 / 👎.

@datadog-datadog-prod-us1-2

This comment has been minimized.

@duncanista duncanista changed the title [SVLS-9978] test: process-level fault-injection harness and the contract tests that pass today test: process-level fault-injection harness and the contract tests that pass today Oct 28, 2026
- fault_injection_test: measure the /next budget from when the INVOKE was queued
- harness: shutdown_cleanly asserts the exit came within the SHUTDOWN deadline

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 51f541581a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread bottlecap/tests/fault_injection_test.rs Outdated
.await
.unwrap();
eprintln!("trace agent answered {status} to a 4 MB span");
let (released, _) = end_invocation_and_wait(&s, "req-1", 2, invoked, secs(15)).await;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Match trace waits to the invocation deadline

In oversized_trace_is_dropped_and_later_traces_flow, both INVOKE events advertise a 10-second deadline, but the test allows /next up to 15 seconds after invocation. If rejected-trace processing completes between 10 and 15 seconds, released is true even though Lambda would already have terminated the invocation, so this contract test misses the regression; use the 10-second event deadline for these waits.

Useful? React with 👍 / 👎.

)
.await;

s.intake.set_reject_over(None);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Assert the rejected trace is never retried

The test removes the size rejection immediately after observing the first 413 and never checks whether that large request is sent again. If a regression starts retrying 413 responses, the queued 4 MB trace can be accepted after this line and the test still passes once the separate small trace arrives, despite its stated dropped-not-redriven contract; record the rejected request and assert that no subsequent large trace request occurs.

Useful? React with 👍 / 👎.

bc.log_tail(40)
);

s.shutdown_cleanly(&mut bc).await;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Verify the invocation flush before shutdown

shutdown_cleanly triggers the extension's final blocking shutdown flush before these intake assertions are evaluated. If the end-of-invocation flush regresses and /next is called without sending the buffered logs or metrics, the shutdown flush can deliver them and this test still passes, so inspect or snapshot the intake before sending SHUTDOWN.

Useful? React with 👍 / 👎.

- oversized trace: wait for /next against the 10 s INVOKE deadline, and assert
  the rejected trace is never sent again after the rejection is lifted
- lifecycle: read the intake before SHUTDOWN so the final flush cannot hide a
  missing end-of-invocation flush

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 399b90b8e6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +181 to +182
s.intake.set_mode(FaultMode::Ok);
let exit = s.shutdown(&mut bc).await;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Verify recovery after the truncated response

When the intake returns a truncated 200 response, this test only observes that the fault served a request; after clearing the fault here, it shuts down without checking that the request was classified as failed. In src/logs/flusher.rs:117-132, the body-read result is discarded and a 2xx is still accepted, so the current data-loss behavior passes despite the test's stated contract that truncation is a failed request; assert a retry or another customer-visible recovery signal before shutdown.

Useful? React with 👍 / 👎.

Comment on lines +52 to +54
s.api.push_invoke("req-1", secs(5));
if deliver_runtime_done(&s.api, "req-1", true).await.is_ok() {
flushed = s.api.wait_for_next_calls(2, secs(10)).await;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Enforce the invocation deadline in port-conflict tests

When a held listener makes end-of-invocation processing take more than five seconds, this helper can still pass: the INVOKE advertises a five-second deadline, but the test waits ten seconds and stores the result in flushed only for diagnostic output. Even if that wait expires, the subsequently queued SHUTDOWN may be consumed by a still-late /next, so the final exit assertion succeeds without enforcing the invocation contract; wait only for the remaining five-second budget and assert flushed before shutdown.

Useful? React with 👍 / 👎.

Comment on lines +156 to +159
// A connection accepted in blackhole mode is never read.
if *accept_shared.mode.lock().expect("mode lock") == FaultMode::Blackhole {
accept_shared.held.lock().expect("held lock").push(stream);
continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Honor path overrides during global blackholes

When the global mode is Blackhole and a specific path has an Ok or status override, the accept loop holds the socket before reading the HTTP request, so effective_mode never sees the path and the documented override is ignored. This prevents scenarios such as keeping a credentials endpoint available while blackholing intake traffic; only apply the accept-level blackhole when no path override needs routing, or read enough of the request to select the effective mode.

Useful? React with 👍 / 👎.

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