Skip to content

fix: Stop Scan All holding a pool connection per artifact - #889

Open
bupd wants to merge 1 commit into
mainfrom
fix-scanall-pool-connections
Open

bupd wants to merge 1 commit into
mainfrom
fix-scanall-pool-connections

Conversation

@bupd

@bupd bupd commented Sep 14, 2026

Copy link
Copy Markdown
Member

Closes #856.

The bug

A Scan All run drained the core DB connection pool and wedged core completely, including routes that touch no artifact table. Three things in the per-artifact scan dispatch combined to do it:

  1. startScanAll wrapped the whole scan of an artifact in a transaction, so one pool connection was held across the read phase, which includes the artifact_blob join in HasUnscannableLayer, and across the job submission to jobservice.
  2. MakePlaceHolder, which runs inside that transaction, read the previous reports on an ORM of its own (h.cloneCtx(ctx)). That second acquire is where the goroutines were parked in the production dump. It also explains the pg_stat_activity evidence in the issue: every session idle in transaction, all on the artifact_blob join with no lock waits, because the join is the last statement to run on the transaction's connection before the goroutine blocks asking for a second one.
  3. GetReport spawned a goroutine per artifact in an index, each with an ORM of its own, so a single artifact-list request took a pool connection per child artifact. The same shape was in both listScanTasks implementations.

The fix

The scan is split into a read-only planning half (planScan: resolve the scanner, walk the artifacts) and a dispatch half (dispatchScan: placeholders, job submission), and only the dispatch half runs in a transaction. Scan still runs both, so the API path is unchanged.

The report lookups run on the caller's connection: GetReport asks for every artifact's reports in one query grouped by digest, and the task lookups run in sequence instead of one goroutine and one connection each. MakePlaceHolder and the SBOM delete read on the caller's context, which removes cloneCtx from both handlers.

Removing the extra acquire from the dispatch path also removes the deadlock itself, not only the time a connection is held. A pool where every connection is held by a caller waiting for another connection cannot recover on its own, which is why the incident needed all three core pods restarted.

Reproduction

Two regression tests, both failing on main:

$ go test ./controller/scan/ -count=1
--- FAIL: TestController/TestScanAllReadsOutsideTheTransaction
    Error: Should be false
    Test:  the read phase must not run inside the Scan All transaction
--- FAIL: TestController/TestGetReportQueriesReportsOnce
    assert: mock: I don't know what to return because the method call was unexpected

$ go test ./pkg/scan/vulnerability/ -run TestMakePlaceHolderReads -count=1
--- FAIL: TestMakePlaceHolderReadsOnTheCallerConnection
panic: <Ormer.Using> unknown db alias name `default`

The last one panics rather than asserting, which is itself the evidence: with orm.Clone the handler builds a new ORM, and a new ORM needs a registered database.

What this does not do

Test

$ task test:unit:pure                       # no failures
$ go test -tags db -p 1 ./pkg/scan/... ./controller/scan/... ./pkg/task/...
ok  	github.com/goharbor/harbor/src/pkg/scan/...      (all)
ok  	github.com/goharbor/harbor/src/controller/scan
ok  	github.com/goharbor/harbor/src/pkg/task
ok  	github.com/goharbor/harbor/src/pkg/task/dao

Release Notes

Fixed a deadlock where a Scan All run could exhaust harbor-core's database connection pool and stop it serving any request, including /v2/ pulls and pushes, until every core pod was restarted. Scanning an artifact no longer holds a database connection while reading the artifact's layers or while submitting the scan job, and listing artifacts with scan overviews now reads every report in one query instead of one connection per artifact.

A Scan All run drained the core DB connection pool and wedged core completely,
including routes that touch no artifact table. Three things combined to do it,
all of them in the per-artifact scan dispatch:

  * startScanAll wrapped the whole scan of an artifact in a transaction, so one
    pool connection was held across the read phase, which includes the
    artifact_blob join in HasUnscannableLayer, and across the job submission to
    jobservice.
  * MakePlaceHolder, which runs inside that transaction, read the previous
    reports on an ORM of its own. That second acquire is where the goroutines
    were parked in the production dump, and it is why every session showed
    "idle in transaction" on the artifact_blob join with no lock waits.
  * GetReport spawned a goroutine per artifact in an index, each with an ORM of
    its own, so one artifact-list request took a pool connection per child
    artifact. The same shape appeared in both listScanTasks implementations.

The scan is now split into a read-only planning half and a dispatch half, and
only the dispatch half runs in a transaction. The report lookups run on the
caller's connection: GetReport asks for every artifact's reports in one query,
and the task lookups run in sequence rather than one goroutine and one
connection each.

Removing the extra acquire from the dispatch path also removes the deadlock
itself, not only the time a connection is held: a pool where every connection is
held by a caller waiting for another connection cannot recover on its own, which
is why the incident needed all three core pods restarted.

Closes #856

Signed-off-by: Prasanth Baskar <prasanth@8gears.com>
@coderabbitai

coderabbitai Bot commented Sep 14, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 34 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: ba1901dc-b5fa-47e2-80c7-65ac4b895869

📥 Commits

Reviewing files that changed from the base of the PR and between 58adb87 and 016aa2e.

📒 Files selected for processing (8)
  • src/controller/scan/base_controller.go
  • src/controller/scan/base_controller_test.go
  • src/pkg/scan/sbom/sbom.go
  • src/pkg/scan/sbom/sbom_test.go
  • src/pkg/scan/vulnerability/placeholder_test.go
  • src/pkg/scan/vulnerability/vul.go
  • src/pkg/scan/vulnerability/vul_test.go
  • src/testing/mock/mock.go

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.

@github-actions

Copy link
Copy Markdown
Contributor

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@cubic-dev-ai cubic-dev-ai 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.

No issues found across 8 files

Re-trigger cubic

@github-actions

Copy link
Copy Markdown
Contributor

This change may need patch-release backports. Comment with one of these commands to open a cherry-pick PR:

/backport v2.15

@github-actions github-actions Bot added the tests label Sep 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Preview images for this PR are available in 8gears.container-registry.com/8gcr-pr with tag pr-889, built from 016aa2e:

  • 8gears.container-registry.com/8gcr-pr/harbor-core:pr-889
  • 8gears.container-registry.com/8gcr-pr/harbor-jobservice:pr-889
  • 8gears.container-registry.com/8gcr-pr/harbor-registryctl:pr-889
  • 8gears.container-registry.com/8gcr-pr/harbor-exporter:pr-889
  • 8gears.container-registry.com/8gcr-pr/harbor-portal:pr-889
  • 8gears.container-registry.com/8gcr-pr/harbor-registry:pr-889
  • 8gears.container-registry.com/8gcr-pr/trivy-adapter:pr-889

Verify a preview image:

cosign verify \
  --certificate-identity-regexp="https://github.com/container-registry/harbor-next/.github/workflows/pr-ci.yml@.*" \
  --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
  8gears.container-registry.com/8gcr-pr/harbor-core:pr-889

Verify the SBOM attestation:

cosign verify-attestation \
  --certificate-identity-regexp="https://github.com/container-registry/harbor-next/.github/workflows/pr-ci.yml@.*" \
  --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
  --type spdxjson \
  8gears.container-registry.com/8gcr-pr/harbor-core:pr-889

@bupd bupd added prio/P1 Wanted this cycle; schedule it into a merge wave workstream/dbpool DB connection pool and request-transaction work state/ci-failing CI red on its own changes; needs a fix, not a rebase labels Sep 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

prio/P1 Wanted this cycle; schedule it into a merge wave state/ci-failing CI red on its own changes; needs a fix, not a rebase tests workstream/dbpool DB connection pool and request-transaction work

Projects

None yet

2 participants