Skip to content

fix(aws/recs): RateLimiter.Wait skips ctx.Err() check on first attempt (retryCount=0) #16

Closed

Description

@cristim

Affected file

  • providers/aws/recommendations/ratelimiter.go:62-65

Evidence

// Wait implements exponential backoff delay
func (r *RateLimiter) Wait(ctx context.Context) error {
    if r.retryCount == 0 {
        // No delay for first attempt
        return nil
    }
    // ... select { case <-ctx.Done(): return ctx.Err() } only on retries
}

When retryCount == 0 (the common case: first call into fetchRIPageWithRetry / fetchSPPageWithRetry / fetchCoveragePage / fetchUtilizationPage), Wait returns nil immediately without consulting ctx.Err(). A context already cancelled before the call is invisible to the rate-limiter layer.

Impact

Two surfaces:

  1. Hidden test scaffolding dependency. The PERF-02 regression tests in concurrency_cap_test.go (PR fix(aws/recs): cap coverage/utilization CE calls with shared semaphore cloud-commitments-cli#1218) rely on the cancelled-ctx surviving Wait so that concurrency.Acquire catches it. If anyone reorders the retry loop (Wait → Acquire → SDK call) so the Acquire moves below the SDK call or is swapped to a non-blocking variant, a cancelled-from-the-start context would NOT be caught at the rate-limiter layer and the SDK call would fire on a dead ctx. The test still asserts on mock.coverageCalls == 0, so it would catch the regression — but only because of an architectural accident, not by explicit contract.

  2. Wasted work on early cancellation. If the scheduler cancels the parent ctx between batch dispatch and the first per-tuple call (e.g. global shutdown signal, request timeout), the rate-limiter passes the cancelled ctx through to Acquire and the SDK without an early bail. Today this works only because Acquire (when a semaphore is attached) catches it. Without an attached semaphore (CLI ambient calls, unit tests), the SDK call would run on a cancelled ctx.

Recommendation

Short-circuit on ctx.Err() even when retryCount == 0:

func (r *RateLimiter) Wait(ctx context.Context) error {
    if err := ctx.Err(); err != nil {
        return err
    }
    if r.retryCount == 0 {
        return nil
    }
    // ... existing backoff logic
}

Aligns with feedback_ctx_cancel_terminal.md ("context cancellation is terminal in loops") and makes the rate-limiter respect cancellation as a first-class concern at every entry point, not only when there's a delay to interrupt.

Verifier verdict

Confirmed pre-existing in ratelimiter.go; pre-dates PR LeanerCloud/cloud-commitments-cli#1218 (LeanerCloud/cloud-commitments-cli#1144 / PERF-02). PR LeanerCloud/cloud-commitments-cli#1218 does not regress it. Filed as an independent improvement so the rate-limiter no longer leaks early-cancelled contexts past its boundary.

Activity

  1. IAmJSD commented on Oct 16, 2026

    @IAmJSD
    Collaborator

    Claiming: working on this

  2. added a commit that references this issue on Oct 17, 2026
    8afb655
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions