Skip to content

ctx.Canceled silently swallowed in queryMajorEngineVersionsWithClient (surfaced by PR #1225) #1325

Description

@cristim

Background

PR #1225 (TEST-02 fix) adds TestQueryMajorEngineVersionsWithClient_EngineErrorContinues in cmd/multi_service_coverage_test.go with the comment "per-engine API failures are warn-and-continue" and require.NoError(t, err) for any per-engine error. That contract collapses transient throttling errors (correctly continue) and context cancellation / deadline expiry (should be terminal) into the same bucket.

The defect

cmd/multi_service_engine_versions.go:213-227:

for _, engine := range engines {
    if err := fetchMajorEngineVersionsForEngine(ctx, rdsClient, engine, versionInfo); err != nil {
        log.Printf("Warning: Failed to describe major engine versions for %s: %v", engine, err)
    }
}
return versionInfo, nil

fetchMajorEngineVersionsForEngine returns ctx.Err() on cancellation (line 236-237). Today that is swallowed as a warning and the loop continues to the next engine -- contradicting feedback_ctx_cancel_terminal.md: "treat context.Canceled/DeadlineExceeded as hard stops in API fan-out loops; never accumulate as lastErr and continue".

Concrete consequence: a CLI invocation that is interrupted (SIGINT, parent ctx cancelled) keeps calling DescribeDBMajorEngineVersions for the remaining engines, wastes API budget, and writes a "successful" version-info map back to the caller -- which then continues the orchestration as if nothing went wrong.

Fix

In queryMajorEngineVersionsWithClient:

for _, engine := range engines {
    if err := fetchMajorEngineVersionsForEngine(ctx, rdsClient, engine, versionInfo); err != nil {
        if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
            return nil, err
        }
        log.Printf("Warning: Failed to describe major engine versions for %s: %v", engine, err)
    }
}

Plus a paired test in cmd/multi_service_coverage_test.go:

func TestQueryMajorEngineVersionsWithClient_CtxCancelIsTerminal(t *testing.T) {
    stub := &engineKeyedRDSMajorVersionsStub{
        errByEngine: map[string]error{"mysql": context.Canceled},
    }
    ctx, cancel := context.WithCancel(context.Background())
    cancel()
    _, err := queryMajorEngineVersionsWithClient(ctx, stub)
    require.ErrorIs(t, err, context.Canceled)
    assert.Len(t, stub.enginesQueried, 1, "ctx cancellation must stop the loop after the first failing engine")
}

And update the comment on _EngineErrorContinues to say "transient per-engine API failures" -- making the contract narrower than "any error".

Same pattern, sibling occurrences worth a sweep

grep -rn "log.Printf.*Warning.*Failed" cmd/ pkg/ | grep -B2 -A2 "for.*range" -- audit other API fan-out loops in cmd/ for the same shape.

Triage

P2 / sev medium -- pre-existing bug, surfaced by PR #1225's new test. Not a money path itself but the function gates downstream filtering logic.

Activity

  1. cristim commented on Oct 7, 2026

    @cristim
    MemberAuthor

    claimed by cc-cli-w4

  2. cristim commented on Oct 8, 2026

    @cristim
    MemberAuthor

    PR #2139 merged normally as e30a57e. Post-merge CI and pre-commit both succeeded; the merged tree is byte-identical to the independently reviewed tree. Pinned Go 1.26.6 race-enabled local SDK fixtures verified terminal cancellation and deadline expiry, including the final successful response boundary. Local full-suite harness failures remain honestly tracked in #2150, and broader CLI context propagation is tracked separately in #2151. No live-account purchase was performed. See PR evidence comment 6050575209 for fail-before/pass-after details and the explicit CodeRabbit quota waiver.

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