Skip to content

test(e2e): End-to-end tests for the DB hot-row contention fixes - #720

Merged
bupd merged 1 commit into
perf/quota-async-refreshfrom
test/e2e-db-contention
Aug 26, 2026
Merged

test(e2e): End-to-end tests for the DB hot-row contention fixes#720
bupd merged 1 commit into
perf/quota-async-refreshfrom
test/e2e-db-contention

Conversation

@Vad1mo

@Vad1mo Vad1mo commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

What this PR does

Adds an opt-in end-to-end suite (go test -tags e2e ./tests/e2e/) that runs against a live Harbor — by default the slot-0 dev environment — and verifies the four fixes in this stack behave correctly together.

Test Verifies
TestConcurrentSharedLayerPushBurst the incident shape (concurrent pushes into one unlimited project sharing base layers) completes with zero failures
TestQuotaEnforcementDeniesOverLimit projects with a real storage limit still deny over-limit pushes synchronously — guards the unlimited-skip change
TestUsageConvergesOnUnlimitedProject with the reservation skipped, the usage figure still converges
TestTouchDebounce repeated HEAD probes of a fresh blob leave its row untouched — asserted directly against the blob table (version and update_time unchanged)
TestAsyncRefreshConvergence opt-in (E2E_ASYNC_REFRESH=1) tighter convergence bound with QUOTA_ASYNC_REFRESH_DURATION set

Why it earns its place

This suite caught a real design bug before it shipped. The first version of the unlimited-reservation skip assumed the usage figure would keep flowing through RefreshMiddleware. It does not: RefreshForProjectMiddleware is mounted only on artifact/repository DELETE routes (server/registry/route.go), never on manifest PUT — so on the push path the reservation is the only writer of quota_usage. Skipping it alone would have silently frozen every unlimited project's storage figure, including the exporter metric. TestUsageConvergesOnUnlimitedProject failed with used=0, which is why that PR now ships the deferred coalesced refresh with it.

Results against slot-0 running this stack

burst: 64 pushes across 16 workers in 18.8s   PASS
over-quota push denied as expected: 403 DENIED  PASS
usage converged: 787080 bytes (expected >= 786432)  PASS
debounce ok: 25 HEAD probes, blob row untouched (version=0)  PASS

Database-level confirmation after a full push burst plus a HEAD storm: 0 of 963 blob rows had version > 0.

Notes

  • Build-tagged e2e, so it never runs in the normal unit-test sweep.
  • Configurable via E2E_CORE_URL, E2E_ADMIN_USER, E2E_ADMIN_PASSWORD, E2E_DB_DSN (slot-0 defaults).
  • Uses a dependency-free minimal OCI client (monolithic blob upload + manifest PUT) and mirrors real client behaviour by HEAD-probing layers before upload.

@coderabbitai

coderabbitai Bot commented Aug 25, 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 26190149-5f30-4200-a201-82dbc2374557

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

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.

@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.

All reported issues were addressed across 3 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/tests/e2e/README.md Outdated
Comment thread src/tests/e2e/client_test.go
@Vad1mo
Vad1mo force-pushed the test/e2e-db-contention branch from 1ff7f54 to 0381db9 Compare August 25, 2026 08:28
Copilot AI lite review requested due to automatic review settings August 25, 2026 09:00
@Vad1mo
Vad1mo force-pushed the test/e2e-db-contention branch from 0381db9 to f863639 Compare August 25, 2026 09:00

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.

Pull request overview

Adds an opt-in Go e2e test suite under src/tests/e2e/ intended to validate (against a live Harbor instance) the recent database hot-row contention mitigation stack: concurrent push burst behavior, quota enforcement correctness, usage convergence for unlimited projects, and blob Touch debouncing (plus an optional async-refresh convergence check).

Changes:

  • Introduces a new -tags e2e test suite that drives real registry/Harbor API interactions and asserts key outcomes for the contention fixes.
  • Implements a small, dependency-free OCI push client (blob upload + manifest PUT) used by the e2e tests.
  • Documents how to run/configure the suite against the default slot-0 dev environment.

Reviewed changes

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

File Description
src/tests/e2e/README.md Documents running and configuring the new opt-in e2e test suite.
src/tests/e2e/contention_test.go Adds end-to-end regression tests covering burst pushes, quota enforcement, usage convergence, blob Touch debounce, and async refresh convergence.
src/tests/e2e/client_test.go Adds a minimal HTTP-based Harbor/OCI client used by the e2e tests (project creation, blob/manifest push, summary reads).

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

Comment on lines +117 to +134
for time.Now().Before(deadline) {
used, err = e.projectUsedStorage(project)
if err == nil && used > 0 {
break
}
time.Sleep(3 * time.Second)
}
if err != nil {
t.Fatalf("summary: %v", err)
}
// used must cover at least the layer bytes (config+manifest add a bit)
var minExpected int64
for _, l := range layers {
minExpected += int64(len(l.data))
}
if used < minExpected {
t.Fatalf("usage did not converge: used=%d < expected>=%d", used, minExpected)
}

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.

Fixed in bfffa01 - the loop now polls until used >= minExpected (the summed layer bytes), so a partially refreshed intermediate value cannot end the wait early.

t.Fatalf("open db: %v", err)
}
defer db.Close()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)

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.

Fixed in bfffa01 - the outer 10s context now only guards the initial ping; each row read gets its own 10s per-query context.


func randomBlob(size int) blob {
data := make([]byte, size)
_, _ = rand.Read(data)

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.

crypto/rand.Read is documented (since Go 1.24) to never return an error - it always fills the buffer and crashes the program on entropy failure - so there is no error path to handle here.

Comment on lines +49 to +53
coreURL: get("E2E_CORE_URL", "http://localhost:8080"),
user: get("E2E_ADMIN_USER", "admin"),
password: get("E2E_ADMIN_PASSWORD", "Harbor12345"),
dbDSN: get("E2E_DB_DSN", "postgres://postgres:root123@localhost:5432/registry?sslmode=disable"),
http: &http.Client{Timeout: 60 * time.Second},

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.

These are Harbor's canonical dev-environment defaults, not real credentials: admin/Harbor12345 is hardcoded throughout the upstream apitests and every make/compose dev setup, and the DSN matches the throwaway dev database. Requiring the env vars would only add friction for the single environment this suite targets; anything non-default is already configurable via the documented env vars.

Comment thread src/tests/e2e/README.md
Comment on lines +14 to +18
E2E_CORE_URL http://localhost:8080
E2E_ADMIN_USER admin
E2E_ADMIN_PASSWORD Harbor12345
E2E_DB_DSN postgres://postgres:root123@localhost:5432/registry?sslmode=disable
E2E_ASYNC_REFRESH (unset)

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.

Same as the client thread: these are the well-known Harbor dev defaults documented upstream, listed here precisely so a reader knows what the suite assumes and which env vars override them. Nothing secret is disclosed.

@github-actions

Copy link
Copy Markdown
Contributor

Dependency Review

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

Scanned Files

None

@github-actions github-actions Bot added the tests label Aug 25, 2026
@Vad1mo
Vad1mo force-pushed the test/e2e-db-contention branch from f863639 to bfffa01 Compare August 25, 2026 09:35
Vad1mo added a commit that referenced this pull request Aug 25, 2026
goheader expects `http://www.apache.org/licenses/LICENSE-2.0` in the
file header. #711 introduced `src/migration/authoritative.go` with the
`https://` form, so `Go Lint` now fails on every PR targeting `main`
(e.g. #716, #720). One-character fix.

Signed-off-by: Prasanth Baskar <prasanth@8gears.com>
Co-authored-by: Prasanth Baskar <prasanth@8gears.com>
@github-actions

Copy link
Copy Markdown
Contributor

Preview images for this PR are available in 8gears.container-registry.com/8gcr-pr with tag pr-720.

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

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-720

Verify 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-720

@Vad1mo
Vad1mo force-pushed the test/e2e-db-contention branch from bfffa01 to 34e6ca2 Compare August 25, 2026 11:52
@Vad1mo
Vad1mo force-pushed the test/e2e-db-contention branch from 34e6ca2 to 78add7a Compare August 25, 2026 12:04
@Vad1mo
Vad1mo force-pushed the test/e2e-db-contention branch from 78add7a to 41c6722 Compare August 25, 2026 12:04
@bupd
bupd force-pushed the test/e2e-db-contention branch from 41c6722 to 66b3872 Compare August 26, 2026 07:56
@bupd bupd changed the title test(e2e): end-to-end tests for the DB hot-row contention fixes test(e2e): End-to-end tests for the DB hot-row contention fixes Aug 26, 2026
@bupd
bupd force-pushed the test/e2e-db-contention branch from 66b3872 to db029f9 Compare August 26, 2026 11:40

@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.

2 issues found across 3 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/tests/e2e/client_test.go">

<violation number="1" location="src/tests/e2e/client_test.go:41">
P3: When E2E_CORE_URL is set with a trailing slash (http://host:8080/), concatenating it with the leading-slash paths builds double-slash URLs that Harbor's router rejects with 404, failing the whole suite. Normalize coreURL by trimming a trailing '/' in newEnv so configuration with or without the slash behaves identically.</violation>
</file>

<file name="src/tests/e2e/contention_test.go">

<violation number="1" location="src/tests/e2e/contention_test.go:216">
P2: When `QUOTA_ASYNC_REFRESH_DURATION` exceeds 10 seconds, this test can fail before three configured intervals elapse. Derive the deadline from the configured duration or enforce and document a maximum.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

}

// assume interval <= 10s; allow 3 intervals + slack
deadline := time.Now().Add(35 * time.Second)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: When QUOTA_ASYNC_REFRESH_DURATION exceeds 10 seconds, this test can fail before three configured intervals elapse. Derive the deadline from the configured duration or enforce and document a maximum.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/tests/e2e/contention_test.go, line 216:

<comment>When `QUOTA_ASYNC_REFRESH_DURATION` exceeds 10 seconds, this test can fail before three configured intervals elapse. Derive the deadline from the configured duration or enforce and document a maximum.</comment>

<file context>
@@ -0,0 +1,226 @@
+	}
+
+	// assume interval <= 10s; allow 3 intervals + slack
+	deadline := time.Now().Add(35 * time.Second)
+	for time.Now().Before(deadline) {
+		used, err := e.projectUsedStorage(project)
</file context>

http *http.Client
}

func newEnv() *env {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: When E2E_CORE_URL is set with a trailing slash (http://host:8080/), concatenating it with the leading-slash paths builds double-slash URLs that Harbor's router rejects with 404, failing the whole suite. Normalize coreURL by trimming a trailing '/' in newEnv so configuration with or without the slash behaves identically.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/tests/e2e/client_test.go, line 41:

<comment>When E2E_CORE_URL is set with a trailing slash (http://host:8080/), concatenating it with the leading-slash paths builds double-slash URLs that Harbor's router rejects with 404, failing the whole suite. Normalize coreURL by trimming a trailing '/' in newEnv so configuration with or without the slash behaves identically.</comment>

<file context>
@@ -0,0 +1,242 @@
+	http     *http.Client
+}
+
+func newEnv() *env {
+	get := func(k, d string) string {
+		if v := os.Getenv(k); v != "" {
</file context>

Adds an opt-in e2e suite (go test -tags e2e ./tests/e2e/) that runs
against a live Harbor - by default the slot-0 dev environment - and
verifies the four fixes from this branch stack end to end:

- TestConcurrentSharedLayerPushBurst: the incident shape - concurrent
  pushes into one unlimited project sharing base layers - completes with
  zero failures
- TestQuotaEnforcementDeniesOverLimit: projects with a real storage
  limit still deny over-limit pushes synchronously (guards the
  unlimited-skip change)
- TestUsageConvergesOnUnlimitedProject: with the reservation skipped,
  the usage figure still converges via the refresh path
- TestTouchDebounce: repeated HEAD probes of a fresh blob leave its row
  untouched, verified directly against the blob table (version and
  update_time unchanged)
- TestAsyncRefreshConvergence: opt-in (E2E_ASYNC_REFRESH=1) tighter
  convergence bound when core runs with QUOTA_ASYNC_REFRESH_DURATION

The suite uses a dependency-free minimal OCI client (monolithic blob
upload + manifest PUT) and mirrors real client behavior by HEAD-probing
layers before upload.

Signed-off-by: Vadim Bauer <vb@container-registry.com>
@bupd
bupd force-pushed the test/e2e-db-contention branch from db029f9 to 383a263 Compare August 26, 2026 12:06
@bupd
bupd merged commit 85511a1 into main Aug 26, 2026
36 of 44 checks passed
@bupd
bupd deleted the test/e2e-db-contention branch August 26, 2026 12:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants