Skip to content

Cancel sibling logs targets promptly when shared --count limit is reached - #60323

Merged
pelikhan merged 2 commits into
mainfrom
copilot/create-integration-test-logs-command
Sep 11, 2026
Merged

pelikhan merged 2 commits into
mainfrom
copilot/create-integration-test-logs-command

Conversation

Copilot AI commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

gh aw logs --count N with multiple workflow targets shares one run-count budget across all targets, but a target already mid-batch had no way to be interrupted once a sibling exhausted that budget — it would finish downloading its full (potentially over-fetched) chunk before ever re-checking the limit.

Root cause

logsCountLimit.isReached() was only consulted at three checkpoints: before a target starts, right after it acquires a semaphore slot, and at the top of each new iteration. Nothing interrupted a target already in flight.

Fix: cancel on limit reached

  • logsCountLimit now carries a cancel context.CancelFunc (+ sync.Once), invoked the instant the atomic counter reaches max.
  • collectLogsTargets derives a cancellable child context (targetsCtx) shared by all targets and wires it to countLimit.cancel. Cancellation propagates through the existing activeCtx plumbing (semaphore waits, per-run download tasks) with no changes needed downstream.

Avoid misclassifying the cancellation as an error

Several call sites treated any ctx.Done()/context.Canceled as a real failure or printed a user-facing "Operation cancelled" warning. These now check countLimit.isReached() first and treat it as an expected, silent, successful stop instead:

  • collectSingleLogsTarget's semaphore-wait and post-wait ctx.Err() checks
  • shouldStopLogsIteration
  • handleLogsBatchError
  • the waitForLogsRateLimit error branch in collectProcessedWorkflowRuns, which previously discarded all already-accumulated processedRuns on cancellation

Tests

  • TestLogsMultiTargetCountIsSharedMaxAcrossTargets: regression test proving --count is a shared max, not per-target, across 3 concurrent targets.
  • TestLogsMultiTargetCancelsSiblingsWhenSharedCountLimitReached: proves a sibling target blocked mid-operation is interrupted well before it would naturally finish, once another target exhausts the shared budget.

Both are wired into CI as a dedicated matrix job to isolate them from the broader logs test suite.

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
@pelikhan
pelikhan marked this pull request as ready for review September 11, 2026 20:07
Copilot AI balanced review requested due to automatic review settings September 11, 2026 20:07
@github-actions

github-actions Bot commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer

@github-actions

github-actions Bot commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

✅ Test Quality Sentinel completed test quality analysis.

Warning

Threat Detection Engine Failure — The analysis engine could not complete. This is a tooling failure, not a security finding.

What happened

The threat detection engine failed to produce results.

Review the workflow run logs for details.

🧪 Test quality analysis by Test Quality Sentinel

@github-actions

github-actions Bot commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

⚠️ Security scanning failed for Design Decision Gate 🏗️. Review the logs for details.

🏗️ ADR gate enforced by Design Decision Gate 🏗️

@github-actions

github-actions Bot commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

✅ Ponytail Reviewer completed successfully!

Lean already. Ship.

Warning

Firewall blocked 1 domain

The following domain was blocked by the firewall during workflow execution:

  • ab.chatgpt.com

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "ab.chatgpt.com"

See Network Configuration for more information.

Generated by Ponytail Reviewer for #60323

@github-actions

github-actions Bot commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

⚠️ PR Code Quality Reviewer failed during code quality review.

Warning

Threat Detection Engine Failure — The analysis engine could not complete. This is a tooling failure, not a security finding.

What happened

The threat detection engine failed to produce results.

Review the workflow run logs for details.

🔎 Code quality review by PR Code Quality Reviewer

@github-actions

Copy link
Copy Markdown
Contributor
🏗️ ADR Required — draft added for PR #60323

This PR triggers ADR enforcement because it adds more than 100 new lines in business-logic directories (pkg/ additions: 257 lines).

Evidence reviewed

  • PR title: Cancel sibling logs targets promptly when shared --count limit is reached
  • PR body explains a behavioral change in multi-target log collection: shared --count exhaustion now cancels sibling targets and reclassifies that cancellation as an expected success path.
  • Diff updates concurrency/orchestration code in pkg/cli/logs_multi.go and pkg/cli/logs_orchestrator_download.go, and adds dedicated regression coverage in pkg/cli/logs_multi_target_count_integration_test.go.

Outcome

I did not find an existing ADR in the PR body or on the branch that fully covered this specific decision, so I generated a draft ADR:

  • docs/adr/60323-cancel-sibling-log-targets-on-shared-count-limit.md

Inferred decision

The PR makes this architectural decision:

  • multi-target gh aw logs --count N should cancel the shared target context as soon as the shared budget is exhausted, instead of letting already-running sibling targets continue until their next local limit check.

Next action

Please review and refine the draft ADR so the decision, trade-offs, and consequences reflect maintainer intent before merging.

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · pi · gpt54 · 18.9 AIC · ⊞ 10.1K · ◷
Comment /review to run again

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Cancellation can skip resumable runs, surface expected-cancellation errors, and produce a scheduler-dependent test failure.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Adds prompt sibling cancellation when multi-target log collection reaches its shared count limit.

Changes:

  • Cancels active targets when the shared budget is exhausted.
  • Treats count-limit cancellation as successful termination.
  • Adds isolated multi-target integration coverage.
File summaries
File Description
pkg/cli/logs_orchestrator_download.go Handles expected cancellation during collection.
pkg/cli/logs_multi.go Adds shared target cancellation.
pkg/cli/logs_multi_target_count_integration_test.go Tests shared limits and cancellation latency.
Makefile Adds a dedicated integration-test target.
.github/workflows/ci.yml Isolates the new tests in CI.
Review details
  • Files reviewed: 6/6 changed files
  • Comments generated: 4
  • Review effort level: Balanced

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Comment thread pkg/cli/logs_multi.go
targetsCtx := ctx
if countLimit != nil {
var cancel context.CancelFunc
targetsCtx, cancel = context.WithCancel(ctx)
Comment thread pkg/cli/logs_multi.go
for _, target := range targets {
wg.Go(func() {
resultChannel <- collectSingleLogsTarget(ctx, opts, target, shared)
resultChannel <- collectSingleLogsTarget(targetsCtx, opts, target, shared)
Comment thread pkg/cli/logs_multi.go
var cancel context.CancelFunc
targetsCtx, cancel = context.WithCancel(ctx)
defer cancel()
countLimit.cancel = cancel
Comment on lines +131 to +148
limitReached := make(chan struct{})
elapsed := make(chan time.Duration, 2)

collectWorkflowLogsForTarget = func(ctx context.Context, opts LogsDownloadOptions) (workflowLogsResult, error) {
require.NoError(t, os.MkdirAll(opts.OutputDir, 0o755))
if opts.WorkflowName == "first" {
// Immediately consume the only slot in the shared budget, then let
// the other targets know the limit has now been hit.
require.True(t, opts.countLimit.tryAdd())
close(limitReached)
return workflowLogsResult{}, nil
}

// Simulate a target that is already mid-operation (e.g. downloading an
// over-fetched batch) at the moment a sibling exhausts the shared
// budget. It should be interrupted well before simulatedInFlightDuration
// elapses instead of running to completion.
<-limitReached

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Skills-Based Review 🧠

Applied /diagnosing-bugs and /tdd. The root-cause fix (a cancel context.CancelFunc on logsCountLimit, wired to a derived targetsCtx in collectLogsTargets, plus careful reclassification of context.Canceled at every call site that could see it) is well-targeted and I verified both new integration tests pass locally (TestLogsMultiTargetCountIsSharedMaxAcrossTargets, TestLogsMultiTargetCancelsSiblingsWhenSharedCountLimitReached). No correctness issues found in the production code paths.

📋 Key Themes & Highlights

Key Themes

  • Test coverage gap: the new cancellation-classification branches (handleLogsBatchError, shouldStopLogsIteration) are only proven end-to-end via the new integration tests; a few fast unit tests alongside the existing effectiveLogsBatchCount unit tests would pin the exact logic independent of timing.
  • Test hygiene: require.* calls inside goroutines spawned via wg.Go (in the fakes for collectWorkflowLogsForTarget) don't fail safely off the main test goroutine — worth switching to assert.* there.
  • Minor flakiness risk: the 1s cancellation-latency threshold in the new sibling-cancellation test could be tight under loaded CI (-parallel=8); a wider or relative margin would reduce false failures.

Positive Highlights

  • ✅ Root cause correctly identified and fixed at the source (tryAdd triggers cancellation exactly once via sync.Once) rather than patched at symptom sites only.
  • ✅ Thorough, well-commented reclassification of context.Canceled across all affected call sites so a shared-budget exhaustion is never misreported as a real error.
  • ✅ Strong regression tests that reproduce the original bug (shared vs. per-target --count) and prove the fix's latency bound, isolated into a dedicated CI job.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · copilot · sonnet50 · 150.2 AIC · ⌖ 15.2 AIC · ⊞ 10.4K
Comment /matt to run again


const simulatedInFlightDuration = 5 * time.Second
const maxAcceptableCancelLatency = 1 * time.Second

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/tdd] Hard-coded latency thresholds (maxAcceptableCancelLatency = 1s) in a concurrency test risk flakiness on loaded CI runners (this job already runs with -parallel=8 alongside other suites per ci.yml).

💡 Suggestion

Consider widening the margin (e.g. 2–3s) or asserting a relative bound (elapsed well under simulatedInFlightDuration, e.g. < simulatedInFlightDuration/2) instead of a fixed absolute value, so scheduling jitter under CI load doesn't intermittently fail a correctness-proving regression test.

@copilot please address this.


const availableRunsForTarget = 5
collectWorkflowLogsForTarget = func(_ context.Context, opts LogsDownloadOptions) (workflowLogsResult, error) {
require.NotNil(t, opts.countLimit, "multi-target downloads must share a countLimit across targets")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/tdd] require.NoError/require.True are called from goroutines spawned by wg.Go in collectLogsTargets (via the faked collectWorkflowLogsForTarget), not the test's own goroutine.

💡 Why this matters

testify's require calls t.FailNow() on failure, which only works correctly from the test's own goroutine — from another goroutine it can panic the process or silently fail to stop the test (see testify docs and the well-known Go testing pitfall). If any of these assertions ever fail under CI, the failure could manifest as a confusing panic/hang instead of a clean, attributable test failure.

Prefer assert.* inside these goroutines (which only marks the test failed without calling FailNow), reserving require.* for the main test goroutine after joining.

@copilot please address this.

}

func handleLogsBatchError(state *logsCollectionState, ctx context.Context, err error) (bool, error) {
func handleLogsBatchError(state *logsCollectionState, fetchAllInRange bool, countLimit *logsCountLimit, err error) (bool, error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/tdd] The new cancellation-classification branches (handleLogsBatchError, shouldStopLogsIteration, markSharedLogsCountReached treating context.Canceled as expected-success when countLimit.isReached()) are only exercised indirectly through the two new (go/redacted):build integration tests, with no direct unit tests.

💡 Suggestion

These are small, pure functions (handleLogsBatchError(state, fetchAllInRange, countLimit, err), shouldStopLogsIteration(runtime, opts)) that could be unit-tested in logs_orchestrator_unit_test.go (which already covers sibling helpers like effectiveLogsBatchCount) without spinning up the full multi-target integration harness — e.g. asserting that a context.Canceled error with an exhausted countLimit returns (true, nil) from handleLogsBatchError but (true, err) when the limit is not reached. This would pin the exact classification logic independently of timing-sensitive integration behavior and run in the fast unit suite.

@copilot please address this.

@pelikhan
pelikhan merged commit f7e35d5 into main Sep 11, 2026
42 checks passed
@pelikhan
pelikhan deleted the copilot/create-integration-test-logs-command branch September 11, 2026 21:21
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This pull request is included in a new release.

Release: v0.89.7

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants