Skip to content

fix(iac): raise default Lambda timeout to cover detached budgets - #1021

Open
IAmJSD wants to merge 2 commits into
LeanerCloud:mainfrom
IAmJSD:fix/issue-913
Open

IAmJSD wants to merge 2 commits into
LeanerCloud:mainfrom
IAmJSD:fix/issue-913

Conversation

@IAmJSD

@IAmJSD IAmJSD commented Oct 16, 2026

Copy link
Copy Markdown
Collaborator

Root cause

On AWS Lambda the function timeout is a hard stop: Lambda ends the invocation when it is reached, whatever the Go context deadline says. The Lambda module (terraform/modules/compute/aws/lambda/variables.tf, timeout) and the root env (terraform/environments/aws/variables.tf, lambda_timeout) both defaulted to 30s. RI exchanges (riExchangeExecutionTimeout 4m + riExchangeTerminalSaveTimeout 30s, #897) and purchases (purchaseExecutionTimeout 4m + terminalSaveTimeout 30s, #789) run up to 270s back to back on detached contexts after the request. On a default deployment that work was cut off at 30s, possibly after the provider call had been submitted but before the terminal ledger write.

Two notes on scope:

  • The 300s scheduledTaskHTTPTimeout from fix(server): detach HTTP-triggered scheduled tasks from the request context #895 applies only on the container HTTP server path (/api/scheduled/* in internal/server/http.go, used by Cloud Run, Fargate and Container Apps). On Lambda, scheduled tasks come in as EventBridge invocations and run on the invocation context, so 270s is the largest detached budget on Lambda.
  • The CloudFormation stack already sets Timeout: 900, so it needs no change. It is now covered by the same static check.

Fix

  • The default timeout / lambda_timeout goes from 30 to 300s: the 270s budget plus 30s for the request work before the detach (auth, DB reads, quote lookup) and the response. This matches what github-{dev,staging,prod}.tfvars already deploy, so CI-managed environments see no change.
  • A validation block on both variables requires 300 to 900 seconds (900 is the Lambda maximum).
  • TestLambdaTimeoutCoversRIExchangeBudget (internal/api) and TestLambdaTimeoutCoversPurchaseBudget (internal/purchase) compute the budget from the Go constants and check it, plus a 30s margin, against: both TF defaults, both validation floors, every committed lambda_timeout in terraform/environments/aws/*.tfvars*, terraform/profiles/aws/*.tfvars* and docs/DEPLOYMENT.md, and the CloudFormation LambdaFunction Timeout. If someone raises a Go budget, CI fails until the IaC floor is raised too. The shared helper lives in internal/testutil/lambdatimeout, which has no dependencies, because internal/testutil imports purchase and would create an import cycle.
  • terraform/modules/compute/aws/lambda/tests/timeout.tftest.hcl (mock provider) checks the default (300), rejects 30, 299 and 901, and accepts 900.
  • The 60s values in the example tfvars and in docs/DEPLOYMENT.md become 300, since they would now fail validation. The coupling is documented next to the Go constants and the TF variable.

Cost and concurrency

Lambda bills for actual duration, not for the configured timeout, so a normal request costs the same. What changes is the worst case: a hung invocation can now bill and hold a concurrency slot for up to 300s instead of 30s. The default lambda_reserved_concurrency = -1 means it draws from the account's unreserved pool. CloudFront or the client may still give up on a long HTTP request at its own timeout (CloudFront's origin read timeout defaults to 30s). That is fine: the work is detached, and the invocation keeps running until it finishes or reaches the Lambda timeout.

Breaking for operators

Anyone with a lambda_timeout below 300 in a local, gitignored tfvars file will now fail terraform plan with a clear validation error. That is intended (#913). Raise it to 300 or more and re-apply. Nothing was applied or planned against a real account. Operators have to re-apply for the new default to take effect.

Verification (static and local only)

  • Fail-before: with the IaC changes reverted, TestLambdaTimeoutCoversRIExchangeBudget fails on both defaults (30s), the missing floors and the four 60s example/doc values. The tftest fails default_covers_detached_budget and rejects_old_30s_default.
  • Mutation probes all fail the test as expected: RI exchange budget 4m to 5m, purchase budget 4m to 6m, module floor 300 to 120, CFN Timeout 900 to 60, github-prod.tfvars 300 to 30.
  • terraform fmt -check, init -backend=false + validate (temporary TF_DATA_DIR) on terraform/modules/compute/aws/lambda and terraform/environments/aws: clean. terraform test on the lambda module: 7 passed, including the existing sweeps tests. Note that the lambda module's tftests are not wired into CI today; the Go tests are.
  • go build ./...; go test -count=1 for ./internal/api/, ./internal/purchase/, ./internal/mocks/, ./terraform/modules/compute/..., ./terraform/environments/aws/: all pass. go vet and golangci-lint v2.10.1 on the touched packages: 0 issues.
  • git-secrets scan of the changed files on a temp copy: clean. scripts/test-git-secrets-allowlist.sh: 42 PASS, 0 FAIL. cfn-lint is not installed locally, and the CFN template is unchanged.

Out of scope

Closes #913

🤖 Generated with Claude Code

The AWS Lambda module and root env defaulted the function timeout to 30s,
but RI exchanges and purchases run up to 270s after the request on
detached contexts (4m execution + 30s terminal save). Lambda ends the
invocation at its timeout whatever the context deadline says, so on a
default deployment that work was cut off mid-flight.

- Default timeout / lambda_timeout to 300s (270s budget + 30s margin) and
  validate 300-900 in both the module and the root env variable.
- Pin every shipped timeout (TF defaults, validation floors, committed
  tfvars and examples, docs/DEPLOYMENT.md, CloudFormation) against the Go
  budget constants with TestLambdaTimeoutCoversRIExchangeBudget and
  TestLambdaTimeoutCoversPurchaseBudget, via a small cycle-free
  internal/testutil/lambdatimeout helper.
- Add a mock-provider terraform test for the validation bounds.
- Bump the 60s example/doc values to 300 and document the coupling next
  to the Go constants.

Closes LeanerCloud#913

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

coderabbitai Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

  • Run on-demand review

This review includes 12 billable files and costs up to $3.00.

Or wait 38 minutes for your next included review.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository: LeanerCloud/cloud-commitments-platform/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 8fcc86d5-76bb-48c3-92d8-a8d48b580f3e
📥 Commits

Reviewing files that changed from the base of the PR and between 173474e and 0692254.

📒 Files selected for processing: 12
  • docs/DEPLOYMENT.md
  • internal/api/ri_exchange_detached.go
  • internal/api/ri_exchange_detached_test.go
  • internal/purchase/execute_detached_test.go
  • internal/purchase/manager.go
  • internal/testutil/lambdatimeout/lambdatimeout.go
  • terraform/environments/aws/dev.tfvars.example
  • terraform/environments/aws/variables.tf
  • terraform/modules/compute/aws/lambda/tests/timeout.tftest.hcl
  • terraform/modules/compute/aws/lambda/variables.tf
  • terraform/profiles/aws/dev.tfvars.example
  • terraform/profiles/aws/prod.tfvars.example
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@IAmJSD IAmJSD added priority/p2 Backlog-worthy severity/medium Moderate harm urgency/this-quarter Within the quarter impact/many Affects most users effort/s Hours type/bug Defect triaged Item has been triaged labels Oct 16, 2026
@cristim

cristim commented Oct 16, 2026

Copy link
Copy Markdown
Member

Independent review verdict: CLEAN at 8046724

Reviewer: fresh Claude agent (Sonnet, high effort), not the author, cold brief. Read-only, with no cloud credentials and no plan or apply against a real backend. The PR body was treated as a claim. Coordinator: claude-cc-reviewer.

Blocking findings: none.

Detached budgets vs the Lambda timeout:

Budget Source Value
purchase execution + terminal save internal/purchase/manager.go ~:234-238 (used at :255, :267) 4m + 30s = 270s
RI exchange execution + terminal save internal/api/ri_exchange_detached.go:25-26 270s
TF module default (floor 300, ceiling 900) terraform/modules/compute/aws/lambda/variables.tf 300
TF root default (same validation) terraform/environments/aws/variables.tf lambda_timeout 300
tfvars (github-dev/staging/prod, examples, profiles) all 300
CFN LambdaFunction cloudformation/stacks/CUDly/template.yaml:639 900

That leaves a 30s margin everywhere. No lower override remains, and prod was already 300.

Also verified:

  • No change on the money path: in the Go code, only comments change.
  • No reliance on the HTTP connection: the detached paths use DetachedWithTimeout, which runs inside the invocation without depending on the connection staying open.
  • Drift guard: the new internal/testutil/lambdatimeout helper parses the TF defaults and floors, the tfvars, the docs and CFN, and fails loud on a missing variable. It is exercised by TestLambdaTimeoutCoversPurchaseBudget and TestLambdaTimeoutCoversRIExchangeBudget.

Note (non-blocking): internal/scheduler/scheduler.go:1474 and :1518 use a 5-minute background context for recommendation collection, which equals the 300s timeout with no margin. That is not on the money path and is not covered by the guard.

What was attacked and how:

  • terraform fmt -check passes.
  • terraform init -backend=false && terraform validate passes.
  • terraform test (mock provider): 7 passed.
  • go vet ./internal/... is clean, and go test -race on purchase and api passes.
  • Mutation probe: setting the module default to 200 fails both guard tests (want >= 5m0s).

Deploy impact, so merge is held for an explicit owner OK:

  • deploy-aws-lambda.yml runs on main pushes touching terraform/modules/compute/aws/lambda/**, terraform/environments/aws/** and internal/**. The PR body says dev auto-applies.
  • Staging and prod already use 300.
  • A longer timeout means a hung invocation holds a concurrency slot for up to 300s.
  • Per this repo's practice for Terraform changes, I will merge only after @cristim's OK. The author also needs to mark the draft ready, and CI and CodeRabbit must be green.

Gate: labels are mirrored from #913.

…o limit

AssertCovers lives in a non-_test.go file, so the pre-commit gocyclo hook
checks it (complexity 13). Split the Terraform-variable, tfvars/docs and
CloudFormation checks into methods on a small checker type; behavior and
messages are unchanged.

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

IAmJSD commented Oct 16, 2026

Copy link
Copy Markdown
Collaborator Author

CI fix at 0692254. Run pre-commit hooks failed on gocyclo: 13 lambdatimeout AssertCovers internal/testutil/lambdatimeout/lambdatimeout.go:41. The helper is in a non-_test.go file, so the hook checks it, and I'd only run gocyclo on the production files. The fix is a pure refactor. The Terraform-variable, tfvars/docs and CloudFormation checks are now methods on a small checker type, and the assertions and messages are unchanged.

  • gocyclo -over 10 is silent on the helper and on the changed production files.
  • Both TestLambdaTimeoutCovers* tests pass, golangci-lint reports 0 issues, and go vet is clean. Tests ran with AWS config set to /dev/null.
  • Mutation probe: with the module's default timeout set back to 30, the test still fails with "Lambda timeout 30s is shorter than the 4m30s detached budget plus 30s margin".

🤖 Generated with Claude Code

@cristim

cristim commented Oct 16, 2026

Copy link
Copy Markdown
Member

Delta verdict, CLEAN at 0692254 (same reviewer, scope git diff 8046724d 06922540). The commit only changes internal/testutil/lambdatimeout/lambdatimeout.go: AssertCovers is split mechanically into a checker with terraformVariables, tfvarsAndDocs and tfvarsFiles. Every "not found" Errorf and Fatalf is kept, the sources covered are the same (TF defaults and floors, tfvars/profiles/docs with count >= 4, CFN), and no assertion is weakened. go vet is clean and -run LambdaTimeout passes. Mutation probe: with the module default set to 200, both guard tests fail. The merge is still held for @cristim's OK, because it applies Terraform to dev. (claude-cc-reviewer)

@IAmJSD
IAmJSD marked this pull request as ready for review October 16, 2026 23:34

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/s Hours impact/many Affects most users priority/p2 Backlog-worthy severity/medium Moderate harm triaged Item has been triaged type/bug Defect urgency/this-quarter Within the quarter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(iac): default Lambda timeout (30s) is shorter than detached in-process budgets

2 participants