Skip to content

fix(storage): fail fast on connection-pool exhaustion, not just lock contention#345

Open
matthyx wants to merge 2 commits into
fix/consolidator-connection-scopingfrom
fix/pool-contention-fail-fast
Open

fix(storage): fail fast on connection-pool exhaustion, not just lock contention#345
matthyx wants to merge 2 commits into
fix/consolidator-connection-scopingfrom
fix/pool-contention-fail-fast

Conversation

@matthyx

@matthyx matthyx commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Context

Stacked on #344 (consolidator connection scoping). Final PR in the stack for the ContainerProfile 504 regression; see #342 for the shared root-cause context.

Test plan

  • go build ./..., go vet, gofmt clean
  • go test -race ./pkg/registry/file/... passes
  • Live Helm e2e re-validation after this fix: network_policy_known_servers now passes cleanly (2 consecutive clean runs, ~5min each, no retries) — was failing with 504s before this PR
  • relevantCVEs unaffected, still passing
  • Ruled out an unrelated pre-existing failure (relevant_data_is_appended — a stale CVE fixture, identical failure present in the pre-fix baseline run too, unrelated to locking/pooling)

Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com

@matthyx matthyx added the ai-assisted Created through Armosec AI tooling (armosec-shared-rules plugin) label Jul 3, 2026
@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ca9ef9a7-fb18-411e-93b7-9b5491666349

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/pool-contention-fail-fast

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.

Copilot AI 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.

Pull request overview

This PR extends the storage layer’s “fail fast on internal contention” behavior from per-key lock acquisition to SQLite connection-pool acquisition, aiming to return a consistent apierrors.ServerTimeout + Retry-After signal under pool exhaustion (instead of stalling until upstream timeouts). It also adds regression and diagnostic tests to cover both lock contention and pool contention scenarios.

Changes:

  • Shrinks connection-pool acquisition timeout to 5s (mirroring lockTimeout) and generalizes the timeout error helper (newLockTimeoutErrornewContentionTimeoutError).
  • Updates storage operations to return ServerTimeout on contention and adds a deterministic regression test for pool exhaustion.
  • Adds a gated load/stress benchmark and a deterministic lock fail-fast regression test for ContainerProfile contention patterns.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 10 comments.

File Description
pkg/registry/file/storage.go Introduces poolTimeout and routes pool/lock contention to a generalized ServerTimeout constructor.
pkg/registry/file/storage_test.go Renames the timeout-error unit test and adds a pool-contention regression test.
pkg/registry/file/containerprofile_load_test.go Adds a gated mixed-load benchmark plus a deterministic lock fail-fast regression test.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

// retry logic honors. It is used for both lock acquisition (s.locks.Lock/RLock) and
// connection-pool acquisition (s.pool.Take) timeouts: both are internal contention points
// that should fail fast with the same backoff-friendly signal rather than hang to the outer
// request deadline. op is "create"/"get"/"update"/"delete"/"list".
Comment on lines 311 to +315
poolCtx, cancel := poolContext()
defer cancel()
conn, err := s.pool.Take(poolCtx)
if err != nil {
return fmt.Errorf("take connection: %w", err)
return newContentionTimeoutError("create", key, err)
Comment on lines 390 to +394
poolCtx, cancel := poolContext()
defer cancel()
conn, err := s.pool.Take(poolCtx)
if err != nil {
return fmt.Errorf("take connection: %w", err)
return newContentionTimeoutError("delete", key, err)
Comment on lines 468 to +472
poolCtx, cancel := poolContext()
defer cancel()
conn, err := s.pool.Take(poolCtx)
if err != nil {
return fmt.Errorf("take connection: %w", err)
return newContentionTimeoutError("get", key, err)
Comment on lines 713 to +717
poolCtx, cancel := poolContext()
defer cancel()
conn, err := s.pool.Take(poolCtx)
if err != nil {
return fmt.Errorf("take connection: %w", err)
return newContentionTimeoutError("list", key, err)
Comment on lines 898 to +902
poolCtx, cancel := poolContext()
defer cancel()
conn, err := s.pool.Take(poolCtx)
if err != nil {
return fmt.Errorf("take connection: %w", err)
return newContentionTimeoutError("update", key, err)
Comment on lines 1078 to +1082
poolCtx, cancel := poolContext()
defer cancel()
conn, err := s.pool.Take(poolCtx)
if err != nil {
return 0, fmt.Errorf("take connection: %w", err)
return 0, newContentionTimeoutError("count", key, err)
Comment on lines +443 to +445
// Shrink the backstop so the fail-fast path resolves quickly; this exercises
// the real child-context timeout -> newLockTimeoutError code path, just with
// a smaller bound (same technique as TestStorageImpl_LockContentionReturnsServerTimeout).
// Each REST-facing call gets a request context whose deadline models the
// apiserver's request timeout. Pre-fix, a contended lock blocks up to this
// deadline; post-fix the 5s lockTimeout fails fast well under it. We keep it
// generous (30s) so we measure the *actual* wait, not an artificial cap.
Comment on lines +285 to +291
seedCtx, seedCancel := context.WithTimeout(context.Background(), 15*time.Second)
for _, tpl := range templates {
p := tpl.profile.DeepCopy()
key := "/spdx.softwarecomposition.kubescape.io/containerprofile/" + p.Namespace + "/" + p.Name
_ = s.Create(seedCtx, key, p, nil, 0)
}
seedCancel()
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown

Summary:

  • License scan: failure
  • Credentials scan: failure
  • Vulnerabilities scan: failure
  • Unit test: success
  • Go linting: failure

@matthyx matthyx force-pushed the fix/consolidator-connection-scoping branch from 8983b7f to 9546fdf Compare July 3, 2026 15:52
matthyx and others added 2 commits July 3, 2026 17:52
…lock backstop

Adds TestContainerProfileLockFailFast: 20 concurrent GETs contend for a key
whose write lock is held elsewhere; asserts every request resolves within a
2s bound (well above the shrunk 500ms lockTimeout used for determinism, far
below the 10s request deadline) as apierrors.IsServerTimeout, not a plain
timeout. Deterministic (~0.5s, no flakiness) and runs in the normal suite --
this is the closest thing to an AC4 latency-bound regression test that's
practical to assert on reliably in CI.

Also adds TestContainerProfileLoad, a mixed-load diagnostic (background
consolidation + concurrent REST create/get/update contending for a small
connection pool) gated behind LOAD_TEST=1 and skipped by default. Before/after
measurement against the pre-fix baseline showed the fail-fast test's signal
cleanly (all 20 gets go from hanging to the ~10s deadline pre-fix to
resolving in ~1s post-fix); the mixed-load numbers were directionally
consistent (higher read throughput, shorter consolidation passes) but too
noisy under its deliberately adversarial pool/concurrency ratio to assert on,
hence it stays a manual diagnostic rather than a CI gate.

Two items surfaced during this measurement remain open, not addressed here:
- The mixed-load harness's writes are time-series profiles, which early-
  return in PreSave before reaching CollapseSettings(), so it does not
  confirm PR2's in-lock CollapseConfig Get removal is a genuine hot path
  under load -- that AC4 sub-claim is still unverified.
- The SQLite connection pool has no WAL mode or busy_timeout configured
  (NewPool sets neither Flags nor PrepareConn), which under concurrent
  writes to different keys can surface "database is locked" errors
  independent of anything these commits touch (they operate above SQLite).
  Worth its own follow-up.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
…contention

Live Helm e2e validation of the prior 3 commits showed relevantCVEs now passing
cleanly, but network_policy_known_servers still failing with a 504 after a
~34.5s stall and a generic apiserver Reason:Timeout -- not the ServerTimeout
this package's own lock-contention fix produces. That test runs node-agent
with a 10s updatePeriod (vs 5m for the CVE test) and polls the same CRDs every
5-10s, sustaining far more consolidator/REST contention than the CVE test ever
exercises. The timing and error shape point at s.pool.Take(poolCtx), which
still used the original 60s poolTimeout untouched by the prior commits: long
enough that a pool-exhaustion stall can blow past the k8s apiserver's own
outer non-long-running-request deadline (~34s) before this package's error
ever fires, surfacing as an opaque client-side 504 instead of our fail-fast
signal.

Bound s.pool.Take the same way lock acquisition is already bounded: shrink
poolTimeout from a 1-minute const to a 5s var (mirroring lockTimeout), and
return the same ServerTimeout+Retry-After:1 error instead of an opaque "take
connection" error on expiry. Generalized the error constructor (renamed
newLockTimeoutError -> newContentionTimeoutError) since it now covers both
lock and connection-pool contention -- both are internal contention points
that should fail fast with the same backoff-friendly signal rather than hang
to the outer request deadline.

Adds TestStorageImpl_PoolContentionReturnsServerTimeout, the connection-pool
analogue of the existing lock-contention regression test: exhausts a size-1
pool, asserts a subsequent Get fails fast as ServerTimeout+Retry-After rather
than hanging to the old ~60s bound.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
@matthyx matthyx force-pushed the fix/pool-contention-fail-fast branch from 97014cb to 9c515a2 Compare July 3, 2026 15:54
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown

Summary:

  • License scan: failure
  • Credentials scan: failure
  • Vulnerabilities scan: failure
  • Unit test: success
  • Go linting: failure

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)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants