Skip to content

fix(api): correct async self-invoke payload shape so collection actually runs#265

Merged
cristim merged 1 commit into
feat/multicloud-web-frontendfrom
fix/async-self-invoke-payload
May 4, 2026
Merged

fix(api): correct async self-invoke payload shape so collection actually runs#265
cristim merged 1 commit into
feat/multicloud-web-frontendfrom
fix/async-self-invoke-payload

Conversation

@cristim
Copy link
Copy Markdown
Member

@cristim cristim commented May 4, 2026

Summary

The async refresh path landed in #260 but the Lambda self-invoke payload doesn't match what the receiving Lambda's ParseScheduledEvent accepts. Every async invoke is being rejected, so the user clicks Refresh, the frontend polls indefinitely, and collection never actually runs — exact same UX as the original 60s timeout.

Evidence (deployed log group /aws/lambda/cudly-dev-426fc8af-api)

16:49:40 Received unknown event (size: 37 bytes)
16:49:40 Unknown event type, treating as scheduled event
16:49:40 {"errorMessage":"failed to parse scheduled event:
              unknown scheduled task action: \"\""}

37 bytes = the {"event":"scheduled_recommendations"} string exactly. The handler's ScheduledEvent struct has no event field — it has Action, Source, DetailType, Detail — so Action unmarshals as "" and the dispatch switch hits default → unknown scheduled task action.

Fix

Send the payload the dispatcher accepts:

{"source": "aws.events", "action": "collect_recommendations"}

action="collect_recommendations" maps to TaskCollectRecommendations in internal/server/handler.go::ParseScheduledEvent. source="aws.events" makes detectLambdaEventType in internal/server/lambda.go (which checks Source == "aws.events" || Action != "") classify it consistently with EventBridge cron deliveries.

Test plan

  • go build ./... succeeds
  • go test ./internal/api/... — 1,055 tests pass
  • Pre-commit hooks all pass
  • CI green on push
  • After merge + redeploy, click Refresh on /recommendations and confirm:
    • Lambda log shows collect_recommendations task running (not the rejection error)
    • Frontend banner clears once last_collection_started_at clears
    • Recommendations list shows monthly_cost values populated for newly-collected rows

Why this didn't surface during PR #260's CI

CI runs Go unit tests with mocked Lambda invokers, so the payload shape was never end-to-end exercised against the receiving Lambda. The mismatch only manifests in the deployed environment when the real lambda.Invoke delivers the event back to the Lambda runtime.

Closes the user-visible symptom that re-opened the parent investigation under #257.

Summary by CodeRabbit

  • Chores
    • Updated internal scheduler event handling to improve system reliability.

The async refresh path landed in PR #260 but the Lambda self-invoke
payload didn't match what the receiving Lambda expects. The shipped
payload was:

  {"event": "scheduled_recommendations"}

But internal/server/handler.go::ScheduledEvent has no "event" field —
it has Action, Source, DetailType, Detail. Unmarshalling that JSON
gives Action="", and ParseScheduledEvent rejects every event with:

  unknown scheduled task action: ""

Verified live in /aws/lambda/cudly-dev-426fc8af-api request
3befb380-6d46-47e1-954b-69a2b59ea90a:

  16:49:40 Received unknown event (size: 37 bytes)
  16:49:40 Unknown event type, treating as scheduled event
  16:49:40 {"errorMessage":"failed to parse scheduled event:
              unknown scheduled task action: \"\""}

The 37-byte event = the {"event":"scheduled_recommendations"} string
exactly. The async self-invoke is firing successfully but every
invocation is rejected, so the "Refreshing..." banner never clears
and collection never runs — same user-visible symptom (refresh times
out + no fresh data) as if the env var or IAM grant was missing.

Fix: send the payload shape the dispatcher actually accepts:

  {"source": "aws.events", "action": "collect_recommendations"}

action="collect_recommendations" maps to TaskCollectRecommendations
in the ParseScheduledEvent switch. source="aws.events" makes
detectLambdaEventType in internal/server/lambda.go classify the
invoke consistently with EventBridge cron deliveries that already
exercise this code path.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 4, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a3d15d62-6a78-4490-a737-ede65c422d98

📥 Commits

Reviewing files that changed from the base of the PR and between 7eb77d1 and f5ff0c0.

📒 Files selected for processing (1)
  • internal/api/handler_recommendations_refresh.go

📝 Walkthrough

Walkthrough

The scheduler Lambda's self-invocation payload is reformatted from { "event": "scheduled_recommendations" } to { "source": "aws.events", "action": "collect_recommendations" } to align with receiving-side dispatch logic. Inline comments are updated to document the expected payload structure.

Changes

Scheduler Payload Update

Layer / File(s) Summary
Payload Definition & Comments
internal/api/handler_recommendations_refresh.go (lines 142–162)
asyncInvokeSelf builds the invoke payload with source: "aws.events" and action: "collect_recommendations". Comments updated to describe how the receiving side classifies and dispatches scheduled tasks based on source/action fields, noting that the prior event field payload is now rejected.
Invocation Wiring
internal/api/handler_recommendations_refresh.go (lines 164–170)
Lambda invoke call remains unchanged in mechanism (InvocationTypeEvent, Payload field); only the payload contents passed to this call were updated.

Estimated code review effort

🎯 1 (Trivial) | ⏱️ ~3 minutes


🐰 A payload once old, now takes new shape,
With source and action in their place,
The scheduler hops along with glee,
Recommendations flow more gracefully!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: correcting the async self-invoke payload shape to fix recommendation collection execution.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/async-self-invoke-payload

Review rate limit: 4/5 reviews remaining, refill in 12 minutes.

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

@cristim cristim added type/bug Defect priority/p1 Next up; this sprint severity/high Significant harm urgency/now Drop other things impact/many Affects most users effort/xs Trivial / one-liner triaged Item has been triaged labels May 4, 2026
@cristim
Copy link
Copy Markdown
Member Author

cristim commented May 4, 2026

@coderabbitai full review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 4, 2026

✅ Actions performed

Full review triggered.

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

Labels

effort/xs Trivial / one-liner impact/many Affects most users priority/p1 Next up; this sprint severity/high Significant harm triaged Item has been triaged type/bug Defect urgency/now Drop other things

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant