Skip to content

fix: unify SBOM scan-retry budgets to bound total attempts - #857

Merged
matthyx merged 3 commits into
mainfrom
fix/856-unify-sbom-scan-retry-budget
Jul 21, 2026
Merged

fix: unify SBOM scan-retry budgets to bound total attempts#857
matthyx merged 3 commits into
mainfrom
fix/856-unify-sbom-scan-retry-budget

Conversation

@matthyx

@matthyx matthyx commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Overview

SbomManager tracked SBOM-generation retry attempts via two independent counters, both compared against the same maxScanRetries (3):

  • scanRetries (plain map, no TTL) — incremented only by handleScannerCrash for consecutive sidecar OOM crashes.
  • failureRetries (TTL'd LRU) — incremented only by handleGenericFailure for consecutive generic SBOM-generation failures.

Because the counters were independent, an image alternating between failure categories could take more than maxScanRetries total attempts (e.g. 2 generic failures + 3 crashes = 5) before ever reaching a terminal status.

This PR unifies both onto the existing TTL'd failureRetries LRU via a shared incrementFailureCount helper, so the retry budget now bounds total attempts per sbomName regardless of failure category.

Two intentional behavior consequences are documented inline rather than left silent:

  • Scanner-crash counting now inherits the 30-min TTL-reset that generic failures already had (a crash loop with gaps >30min between crashes won't accumulate to a pin).
  • Terminal-status classification (Incomplete vs TooLarge) is now attempt-attributed — whichever handler's call crosses the combined threshold decides, based on that attempt. Safety is preserved by the existing hadContent gate, which is untouched: content-bearing SBOMs are always pinned Incomplete, never TooLarge.

How to Test

go build ./...
go test ./pkg/sbommanager/v1/... -run . -v

The new Test_processContainerWithMetadata_MixedFailureCategoriesShareBudget reproduces the bug scenario from the issue: alternating generic-failure/scanner-crash calls now pin a terminal status at exactly the 3rd combined attempt instead of up to 5-6.

Related issues/PRs

Checklist before requesting a review

  • My code follows the style guidelines of this project
  • I have commented on my code, particularly in hard-to-understand areas
  • I have performed a self-review of my code
  • New and existing unit tests pass locally with my changes

🤖 Generated with Claude Code

AI-skills: oh-my-claudecode:ralph,oh-my-claudecode:ai-slop-cleaner,oh-my-claudecode:cancel | cmds: /clear,/oh-my-claudecode:ralplan

Summary by CodeRabbit

  • Bug Fixes
    • Improved SBOM retry handling by consolidating generic failure and scanner-crash retries into shared, TTL-bounded counters, with clearer crash-loop vs. general failure classification.
    • Refined terminal status selection for contentless images: TooLarge is set only on a pure crash-loop backstop; otherwise Incomplete is used.
    • Preserved existing SBOM artifacts and updated how terminal status is written (annotation patching).
  • Tests
    • Added regression tests covering mixed failure-category sharing, pure crash-loop TooLarge behavior (including scanner memory limit capture), and TTL-expiry survival across sparse restarts.

scanRetries and failureRetries tracked crash and generic-failure
counts independently, both compared against maxScanRetries, so an
image alternating between failure categories could take more than
maxScanRetries total attempts before ever reaching a terminal status.

Unify both onto the existing TTL'd failureRetries LRU via a shared
incrementFailureCount helper so the retry budget bounds total
attempts per sbomName regardless of failure category.

Docs-exempt: internal retry-counter bookkeeping, not previously documented behavior; no API/config/user-facing change
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
@matthyx matthyx added the ai-assisted Created through Armosec AI tooling (armosec-shared-rules plugin) label Jul 20, 2026
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 21df63cc-97db-4c41-a409-e89c4d549b42

📥 Commits

Reviewing files that changed from the base of the PR and between 328e3e8 and 332442e.

📒 Files selected for processing (1)
  • pkg/sbommanager/v1/sbom_manager_reprocessing_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/sbommanager/v1/sbom_manager_reprocessing_test.go

📝 Walkthrough

Walkthrough

SbomManager replaces independent scanner-crash bookkeeping with a shared TTL-bounded failure budget and a separate crash-loop backstop. Reprocessing tests cover mixed failures, terminal status classification, annotation-only persistence, artifact preservation, and TTL expiry.

Changes

SBOM retry budget

Layer / File(s) Summary
Unify retry tracking and terminal classification
pkg/sbommanager/v1/sbom_manager.go
failureRetries is shared by generic failures and scanner crashes, while crashLoopRetries identifies contentless crash loops for TooLarge; successful processing clears crash-loop state.
Validate retry budgets and persistence
pkg/sbommanager/v1/sbom_manager_reprocessing_test.go
Tests cover mixed failure thresholds, annotation-only Incomplete marking with artifact preservation, pure crash-loop TooLarge marking, and sparse-restart TTL behavior.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant processContainerWithMetadata
  participant handleScannerCrash
  participant failureRetries
  participant crashLoopRetries
  participant PatchSBOMAnnotations
  processContainerWithMetadata->>handleScannerCrash: Handle scanner crash
  handleScannerCrash->>failureRetries: Increment shared failure count
  handleScannerCrash->>crashLoopRetries: Increment crash-only count
  handleScannerCrash->>PatchSBOMAnnotations: Mark Incomplete or contentless TooLarge
Loading

Possibly related PRs

🚥 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 clearly summarizes the main change: unifying SBOM scan-retry budgets to cap total attempts.
Linked Issues check ✅ Passed The changes satisfy #856 by sharing the retry budget across generic and crash failures while preserving TooLarge and sparse-crash behavior.
Out of Scope Changes check ✅ Passed The patch stays focused on SBOM retry logic and regression tests, with no obvious unrelated code changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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/856-unify-sbom-scan-retry-budget

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@matthyx matthyx left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Reviewed the unification itself and it's correct — the shared budget does bound total attempts, and the regression test genuinely fails on main. One blocker before merge, plus one design question and a test gap.

Blocker: sharing the counter also silently shares the classification, so a single sidecar OOM can now pin a contentless SBOM TooLarge — a strictly less recoverable state than the Incomplete it would have gotten before. Details inline; I verified this with a PoC test that passes on this branch and fails on main.

The PR body calls that edge "harmless -- a contentless TooLarge freezes no real content". That framing is about content preservation, but the actual cost of TooLarge here is recoverability, and by that measure it isn't harmless.

func (s *SbomManager) handleScannerCrash(sbomName string, notif containercollection.PubSubEvent, scanErr error, imageTag, imageID string, hadContent bool) {
s.scanRetries[sbomName]++
retryCount := s.scanRetries[sbomName]
retryCount := s.incrementFailureCount(sbomName)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Blocker — a single OOM can now pin a contentless SBOM TooLarge.

retryCount is now the combined budget, but the classification below it (retryCount >= maxScanRetriesTooLarge when !hadContent) still assumes the count was consumed by scanner crashes. It no longer is.

Concrete sequence on a brand-new image (never succeeded, so wipSbomHadContent == false):

  1. sidecar scan fails generically (transient) → count 1
  2. sidecar scan fails generically (transient) → count 2
  3. sidecar OOMs onceincrementFailureCount returns 3 → !hadContentmarkSBOMStatus(TooLarge, {scannerMemoryLimit: ...})

PoC (drop into pkg/sbommanager/v1, scanner returns generic/generic/ErrScannerCrashed):

type genericThenCrashClient struct{ calls int }

func (a *genericThenCrashClient) CreateSBOM(_ context.Context, _ sbomscanner.ScanRequest) (*sbomscanner.ScanResult, error) {
	a.calls++
	if a.calls < 3 {
		return nil, errors.New("scan failed")
	}
	return nil, sbomscanner.ErrScannerCrashed
}
func (a *genericThenCrashClient) Ready() bool  { return true }
func (a *genericThenCrashClient) Close() error { return nil }

func Test_POC_SingleOOMAfterGenericFailuresPinsTooLarge(t *testing.T) {
	fake := newFakeSbomClient()
	mgr := &SbomManager{
		cfg:             config.Config{NodeName: "node-1"},
		ctx:             context.Background(),
		processing:      mapset.NewSet[string](),
		storageClient:   fake,
		scannerClient:   &genericThenCrashClient{},
		metrics:         metricsmanager.NewMetricsNoop(),
		version:         "v2.0.0",
		scannerMemLimit: 1024,
		failureRetries:  newFailureRetries(),
	}
	notif, imageStatus, imageTag, imageID := testNotifAndImageStatus()
	sbomName, _ := names.ImageInfoToSlug(imageTag, imageID)
	for range 3 {
		mgr.processContainerWithMetadata(notif, nil, imageStatus, imageTag, imageID)
	}
	raw := fake.get(sbomName)
	assert.Equal(t, helpersv1.TooLarge, raw.Annotations[helpersv1.StatusMetadataKey])
}

Result:

  • this branch → too-large, scannerMemoryLimit: "1024"passes
  • main (same test, plus scanRetries: make(map[string]int)) → initializingfails, no terminal status pinned at all

Why this isn't the harmless edge the PR body describes — TooLarge is not merely "a status that freezes no content", it's the least recoverable terminal state in this file:

  • the TooLarge case in processContainerWithMetadata returns early and, unlike Incomplete, has no tool-version escape hatchshouldRetryAtCurrentVersion is never consulted. A node-agent/syft upgrade re-attempts an Incomplete SBOM; it will never re-attempt a TooLarge one.
  • the only way out is ScannerMemoryLimitAnnotation != scannerMemLimit, i.e. an operator changing the sidecar memory limit — for an image whose real problem was two transient scan errors.
  • and per the wipSbomHadContent comment above, GuaranteedUpdate silently drops every future write once status=too-large, so even that escape hatch can't rewrite the object.

Net effect: two transient sidecar errors plus one unlucky OOM permanently costs that image its SBOM (and therefore its vulnerability coverage), where before it would have landed on retryable Incomplete. That's a behavior change the issue didn't ask for, and it lands on first-time images — the common case, since hadContent is only true on the Learning reprocess path.

Suggested fix — keep the unified budget (it's the right call), but make TooLarge require crash attribution rather than "the crossing attempt happened to be a crash". Cheapest version: have incrementFailureCount also return whether every counted failure in the current window was a crash (a parallel bool/count in the same LRU value, e.g. a small struct), and only take the TooLarge branch when that holds. Simplest version if you'd rather not carry state: pin Incomplete unconditionally from the shared path and treat TooLarge as reachable only via syftutil.ErrImageTooLarge / the MaxSBOMSize check, which are the two places it's actually true by measurement.

// handleGenericFailure and handleScannerCrash) and returns the new count. Once the count
// reaches maxScanRetries the LRU entry is removed, so a later successful attempt or a tool
// version bump starts with a fresh budget.
func (s *SbomManager) incrementFailureCount(sbomName string) int {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Design question — crash counting silently lost its only unbounded backstop.

The struct comment is upfront that scanner-crash counting now inherits the 30-min TTL reset, and calls it an intentional consistency choice. I'd push back on the choice itself, not the disclosure.

The untimed scanRetries map wasn't an oversight — the failure it bounds is qualitatively different. A generic failure is cheap and image-local. A sidecar OOM kills the shared scanner process (ReportSBOMScannerRestart, SetSBOMScannerReady(false)), which stalls scanning for every other image on the node. The untimed counter guaranteed such an image was eventually pinned and stopped costing the node.

With the 30-min TTL, any image whose container starts are spaced >30 min apart never accumulates to a pin: a CronJob on an hourly schedule, a slow rollout, node churn. Each start OOMs the sidecar again, indefinitely, and the counter is back to zero every time. That's an availability regression for the whole node, hidden behind a per-image consistency argument.

Options, cheapest first: bump failureRetryTTL (30 min is tuned for transient scan errors, not for "is this image fundamentally too big for the sidecar"); or don't refresh the TTL on the crash path; or keep the crash count on a separate longer-lived TTL and only unify the budget check.

Minor, same function: the doc comment says removal at threshold is what lets "a later successful attempt or a tool version bump start with a fresh budget." Neither is accurate — a successful attempt resets via s.failureRetries.Remove(sbomName) on the success path, and a tool-version bump doesn't touch the LRU at all. Worth correcting so the next reader doesn't rely on a mechanism that isn't there.

scannerClient: &alternatingScannerClient{},
metrics: metricsmanager.NewMetricsNoop(),
version: "v2.0.0",
failureRetries: newFailureRetries(),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Test gap. The new test seeds a content-bearing SBOM, so hadContent is true and the crossing attempt can only ever produce Incomplete. It proves the budget is shared but can't see the classification change the sharing introduces — the contentless path is exactly where the blocker lives, and it's currently uncovered.

Worth adding the mirror case: no seeded SBOM (hadContent == false), generic → generic → crash, asserting the status the fix decides on (Incomplete under either suggested fix), plus that ScannerMemoryLimitAnnotation is absent. That test fails on this branch today, which is the point.

Nit while you're here: the struct-field comment is 12 lines and the handleScannerCrash comment is 16, and both spend most of that pre-arguing with a reviewer ("That is an intentional consistency choice, not an oversight", "The one residual edge is honest to disclose"). Rationale like that belongs in the PR description or the commit message; the surviving code comment should just say what the invariant is. Roughly: "Shared by handleGenericFailure and handleScannerCrash; TTL-reset, so it bounds tight failure cadences rather than every possible one." The rest is recoverable from git blame.

@github-actions

Copy link
Copy Markdown

Performance Benchmark Results

Node-Agent Resource Usage
Metric BEFORE AFTER Delta
Avg CPU (cores) 0.217 0.208 -3.8%
Peak CPU (cores) 0.230 0.218 -5.5%
Avg Memory (MiB) 349.384 270.712 -22.5%
Peak Memory (MiB) 355.805 272.305 -23.5%
Dedup Effectiveness

No data available.

Address review feedback on PR #857:

- Blocker: sharing one counter between generic failures and scanner
  crashes also silently shared the terminal-status classification, so
  a contentless image could be pinned TooLarge after just one crash
  mixed with two unrelated generic failures. TooLarge is now reachable
  only through a new crashLoopRetries counter that exclusively tracks
  scanner crashes, so a mixed threshold crossing always resolves to
  the safely-retryable Incomplete instead.

- Design gap: unifying onto failureRetries' 30-min TTL meant
  scanner-crash counting lost the unbounded-time backstop it had
  before #857, letting a sparse-cadence crash loop (restarts >30min
  apart) stall the shared scanner sidecar indefinitely without ever
  pinning. crashLoopRetries uses a much longer TTL (24h) specifically
  so it survives the gaps a slow crash loop needs.

Corrected an inaccurate doc comment on incrementFailureCount and
trimmed the failureRetries struct comment per review feedback. Added
regression tests for the mixed-category contentless path, the
preserved pure-crash-loop TooLarge path, and the sparse-cadence
backstop.

Docs-exempt: internal retry-counter bookkeeping, not previously documented behavior; no API/config/user-facing change
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
@matthyx

matthyx commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

Thanks — the blocker's PoC and the availability point are both fair, and I was wrong to frame the contentless-crash edge as harmless.

Pushed a fix in 328e3e8:

  • Added a second, dedicated crashLoopRetries counter that only handleScannerCrash increments, on a much longer TTL (24h vs the existing 30min). TooLarge is now reachable only when this crash-only counter itself crosses maxScanRetries — a threshold crossing driven by the shared failureRetries budget (which may include generic failures) always falls back to Incomplete, regardless of mix. Your PoC sequence (2 generic + 1 crash, contentless) now pins Incomplete.
  • The long TTL also restores the unbounded-time protection scanner-crash counting had before fix: persist terminal status on generic SBOM-generation failures to stop reprocessing loop #855/fix: unify SBOM scan-retry budgets to bound total attempts #857 for the shared-sidecar-availability concern you raised — a sparse-cadence crash loop (restarts spaced >30min apart) still eventually accumulates to a pin via crashLoopRetries, even though the shared budget keeps resetting.
  • Corrected the inaccurate incrementFailureCount doc comment (a successful scan resets the budget explicitly on the success path; a tool-version bump doesn't touch the LRU at all) and trimmed the failureRetries struct comment per your comment-verbosity note.
  • Added three tests: the mixed-category contentless-Incomplete regression (your exact PoC scenario, Test_processContainerWithMetadata_MixedFailureCategoriesPinIncomplete), a guard for the still-supported pure-crash-loop TooLarge path (previously untested — Test_processContainerWithMetadata_MarksContentlessImageTooLargeOnPureCrashLoop), and a test proving the sparse-cadence backstop actually works, using short direct-constructed TTLs with real sleeps so the shared budget provably expires between crashes while the backstop survives (Test_processContainerWithMetadata_CrashLoopBackstopSurvivesSparseCadence).

go build ./..., go vet, gofmt -l, and the full pkg/sbommanager/v1/... suite are all green.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
pkg/sbommanager/v1/sbom_manager_reprocessing_test.go (1)

471-552: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding a hadContent == true pure-crash-loop test to lock the content-protection branch.

The three new tests exercise the contentless paths well, but the !hadContent && crashLoopTriggered guard in handleScannerCrash — the invariant that a content-bearing SBOM must stay Incomplete (never the TooLarge one-way door) even under a pure crash loop — has no direct coverage. Test_..._MixedFailureCategoriesShareBudget seeds content but uses the alternating client, so the shared failureRetries budget pins Incomplete before crashLoopTriggered is ever reached, leaving the crash-loop-with-content branch untested. A mirror of Test_..._MarksContentlessImageTooLargeOnPureCrashLoop seeding a prior successful (Learning) SBOM and asserting Incomplete (and absence of ScannerMemoryLimitAnnotation) would guard this safety-critical branch against regressions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/sbommanager/v1/sbom_manager_reprocessing_test.go` around lines 471 - 552,
Add a pure-crash-loop regression test mirroring
Test_processContainerWithMetadata_MarksContentlessImageTooLargeOnPureCrashLoop,
but seed a prior successful Learning SBOM so hadContent is true. Invoke
processContainerWithMetadata three times with sbomscanner.ErrScannerCrashed,
then assert the result remains Incomplete and ScannerMemoryLimitAnnotation is
absent, covering the !hadContent && crashLoopTriggered guard in
handleScannerCrash.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@pkg/sbommanager/v1/sbom_manager_reprocessing_test.go`:
- Around line 471-552: Add a pure-crash-loop regression test mirroring
Test_processContainerWithMetadata_MarksContentlessImageTooLargeOnPureCrashLoop,
but seed a prior successful Learning SBOM so hadContent is true. Invoke
processContainerWithMetadata three times with sbomscanner.ErrScannerCrashed,
then assert the result remains Incomplete and ScannerMemoryLimitAnnotation is
absent, covering the !hadContent && crashLoopTriggered guard in
handleScannerCrash.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: feb39621-4dcf-4736-8219-b17afb8cfb23

📥 Commits

Reviewing files that changed from the base of the PR and between 375d725 and 328e3e8.

📒 Files selected for processing (2)
  • pkg/sbommanager/v1/sbom_manager.go
  • pkg/sbommanager/v1/sbom_manager_reprocessing_test.go

@github-actions

Copy link
Copy Markdown

Performance Benchmark Results

Node-Agent Resource Usage
Metric BEFORE AFTER Delta
Avg CPU (cores) 0.140 0.135 -2.9%
Peak CPU (cores) 0.145 0.139 -4.1%
Avg Memory (MiB) 326.602 274.481 -16.0%
Peak Memory (MiB) 333.828 280.727 -15.9%
Dedup Effectiveness

No data available.

@matthyx matthyx left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Re-reviewed at 328e3e8. Both findings are resolved — the fix is the right shape, and I verified it independently rather than just reading the diff.

Splitting crashLoopRetries out as a crash-only counter on its own longer TTL addresses both findings with one mechanism, which is neater than either option I suggested: the shared budget keeps bounding total attempts (the original #856 fix), while TooLarge now requires crash-attributed evidence, and the 24h TTL restores the unbounded-time backstop that the 30-min TTL had removed.

Verification — I re-ran my original PoC verbatim against this commit and wrote three more for paths your tests don't cover:

scenario result
2 generic + crash, contentless (the original blocker) incomplete, no memory-limit annotation, exactly 1 patch ✅
crash → generic → crash, contentless (3 combined, 2 crashes) incomplete
content-bearing + sparse-cadence pure crash loop incomplete, 2 artifacts intact, no memory-limit annotation ✅
go vet + full package, and 15× -race on the sleep-based test clean, no flakes observed ✅

That third row is the combination your three new tests don't hit — the long-TTL backstop crossing its threshold on an SBOM that does have content. The hadContent gate holds there, so the backstop can't route a content-bearing SBOM into TooLarge's one-way door. Good.

One interaction I chased and satisfied myself is a non-issue, noting it so it doesn't have to be re-derived later: when handleGenericFailure pins Incomplete, crashLoopRetries keeps its stale count for up to 24h, so in principle a later crash could cross the crash threshold on evidence that straddles a pin. It isn't reachable — an Incomplete pin blocks reprocessing at the same tool version, and the only escapes (version bump, memory-limit change) both imply a process restart, which starts with empty LRUs. No action needed.

Also confirmed every SbomManager construction site (1 production + 6 in tests) initializes the new LRU, so there's no nil-crashLoopRetries panic path into handleScannerCrash.

Only a nit left, inline. Nothing blocking — LGTM.

version: "v2.0.0",
scannerMemLimit: 4096,
failureRetries: expirable.NewLRU[string, int](maxFailureRetryEntries, nil, 5*time.Millisecond),
crashLoopRetries: expirable.NewLRU[string, int](maxFailureRetryEntries, nil, 500*time.Millisecond),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Nit (non-blocking): the 500ms crashLoopRetries TTL is a fairly tight margin for a wall-clock-dependent test. The run needs 3 iterations plus 2×20ms sleeps to finish inside 500ms; a GC pause or a loaded CI runner stalling past that budget expires the backstop entry and flips the assertion to Incomplete — a confusing failure, since it looks like the feature regressed rather than the runner hiccuped.

I ran it 15× under -race without a flake, so this is speculative rather than observed. Still, widening to 5*time.Second costs nothing in runtime (the test doesn't wait on this TTL, it only needs it not to expire) and removes the failure mode entirely. The 5ms failureRetries TTL is fine as-is — that one you do wait on, and the 20ms sleep gives it 4× headroom.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Resolved in 332442e — 5s, and the inline comment now explains why the value is generous (that the test never waits on this TTL) so it doesn't get "optimized" back down later. Stressed the retry/classification tests 20× under -race on the new head: clean, no flakes. Thanks.

…ke risk

500ms was a tight margin: the test needs 3 iterations plus 2x20ms
sleeps to finish inside that window, and a GC pause or a loaded CI
runner stalling past it would expire the backstop entry and flip the
assertion to Incomplete -- a confusing failure that looks like a
regression rather than a runner hiccup. The test never waits on this
TTL (only failureRetries' 5ms TTL needs to actually expire), so
widening it to 5s costs nothing and removes the failure mode.

Docs-exempt: test-only change, no behavior or documented API change
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
@github-actions

Copy link
Copy Markdown

Performance Benchmark Results

Node-Agent Resource Usage
Metric BEFORE AFTER Delta
Avg CPU (cores) 0.215 0.214 -0.8%
Peak CPU (cores) 0.223 0.226 +1.1%
Avg Memory (MiB) 331.784 270.180 -18.6%
Peak Memory (MiB) 333.621 274.520 -17.7%
Dedup Effectiveness

No data available.

@matthyx matthyx moved this to WIP in KS PRs tracking Jul 21, 2026
@matthyx matthyx added the release Create release label Jul 21, 2026
@matthyx
matthyx merged commit 57618e7 into main Jul 21, 2026
30 checks passed
@matthyx
matthyx deleted the fix/856-unify-sbom-scan-retry-budget branch July 21, 2026 15:58
@matthyx matthyx moved this from WIP to To Archive in KS PRs tracking Jul 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-assisted Created through Armosec AI tooling (armosec-shared-rules plugin) release Create release

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

SBOM manager: scanRetries and failureRetries are independent budgets, allowing more than maxScanRetries total attempts before a terminal status

1 participant