9b18bbf471
Add a per-remote mirror_strategy selector for the mirrorlist, supporting
round_robin (default, unchanged) and least_conn.
- models.Remote gains MirrorStrategy string + MirrorStrategy{RoundRobin,LeastConn}
constants and ValidateMirrorStrategy (enum check; least_conn requires a
non-empty mirrorlist). Empty behaves as round_robin for back-compat.
- DB: additive mirror_strategy TEXT NOT NULL DEFAULT 'round_robin' column
(CREATE TABLE + ADD COLUMN IF NOT EXISTS), wired through remoteCols/scanRemote/
CreateRemote/UpdateRemote; empty normalized to round_robin on write.
- Engine: least_conn starts each attempt with the pool URL holding the fewest
in-flight requests via a per-remote/per-URL atomic gauge (incremented around
each upstream call in head/fetch/checkUpstream), ties broken by the existing
round-robin rotation. Round-robin path and failover order unchanged;
single-URL pools are a no-op.
- Tests: unit tests for least-loaded selection, round-robin default, gauge
inc/dec, single-URL no-op, and strategy validation; DB round-trip covers the
new column; docker e2e adds a least_conn distribution test and a real dnf
install through a least_conn remote.
106 lines
3.7 KiB
Go
106 lines
3.7 KiB
Go
//go:build dockere2e
|
|
|
|
package e2edocker
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestLeastConnMultiBaseURL configures an rpm remote with mirror_strategy =
|
|
// least_conn over a two-mirror pool and drives distinct cache-miss paths through
|
|
// it, asserting every request succeeds and both mirrors serve traffic. This
|
|
// exercises the least-connections selection path (leastConnOrder + the in-flight
|
|
// gauge inc/dec around each upstream call) end-to-end through a real HTTP client.
|
|
// A precise least-loaded assertion is timing-sensitive over HTTP and is covered
|
|
// deterministically by the proxy unit test (TestLeastConnPicksLeastLoaded);
|
|
// here, with requests issued serially, in-flight counts return to zero between
|
|
// them so equal-load mirrors are spread by the round-robin tie-break.
|
|
func TestLeastConnMultiBaseURL(t *testing.T) {
|
|
name := "e2e-leastconn"
|
|
createRepo(t, fmt.Sprintf(`{
|
|
"name": %q,
|
|
"package_type": "rpm",
|
|
"repo_type": "remote",
|
|
"base_url": %q,
|
|
"mirrorlist": [%q],
|
|
"mirror_strategy": "least_conn",
|
|
"stale_on_error": false
|
|
}`, name, mockUpstreamA(), mockUpstreamB()))
|
|
defer deleteRepo(t, name)
|
|
|
|
seenA, seenB := false, false
|
|
const n = 12
|
|
for i := 0; i < n; i++ {
|
|
url := api(fmt.Sprintf("/api/v1/remote/%s/lc/%d", name, i))
|
|
resp, body := doRequest(t, http.MethodGet, url, nil, "")
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("request %d: status %d: %s", i, resp.StatusCode, body)
|
|
}
|
|
switch strings.TrimSpace(string(body)) {
|
|
case "UPSTREAM-A":
|
|
seenA = true
|
|
case "UPSTREAM-B":
|
|
seenB = true
|
|
default:
|
|
t.Fatalf("request %d: unexpected body %q", i, body)
|
|
}
|
|
}
|
|
if !seenA || !seenB {
|
|
t.Fatalf("least_conn remote did not reach both upstreams: A=%v B=%v", seenA, seenB)
|
|
}
|
|
}
|
|
|
|
// TestLeastConnDnfInstall drives a real dnf (stock rockylinux container) at a
|
|
// two-mirror rpm remote configured with mirror_strategy = least_conn whose
|
|
// primary base_url is dead: makecache + install must succeed via the live mirror.
|
|
// This proves a real package-manager client installs correctly through a
|
|
// least_conn remote and that failover semantics are unchanged under the new
|
|
// strategy. Requires the compose network exported by scripts/docker-e2e.sh.
|
|
func TestLeastConnDnfInstall(t *testing.T) {
|
|
network := os.Getenv("COMPOSE_NETWORK")
|
|
internal := os.Getenv("ARTIFACTAPI_INTERNAL")
|
|
if network == "" || internal == "" {
|
|
t.Skip("COMPOSE_NETWORK/ARTIFACTAPI_INTERNAL not set; run via scripts/docker-e2e.sh")
|
|
}
|
|
if _, err := exec.LookPath("docker"); err != nil {
|
|
t.Skip("docker not available on the test host")
|
|
}
|
|
|
|
name := "e2e-leastconn-dnf"
|
|
createRepo(t, fmt.Sprintf(`{
|
|
"name": %q,
|
|
"package_type": "rpm",
|
|
"repo_type": "remote",
|
|
"base_url": "http://mockupstream-dead:80",
|
|
"mirrorlist": [%q],
|
|
"mirror_strategy": "least_conn",
|
|
"stale_on_error": false
|
|
}`, name, mockUpstream()))
|
|
defer deleteRepo(t, name)
|
|
|
|
repoURL := strings.TrimRight(internal, "/") + "/api/v1/remote/" + name + "/rpm-mirror"
|
|
repoConf := fmt.Sprintf("[dnflc]\nname=dnflc\nbaseurl=%s\nenabled=1\ngpgcheck=0\nsslverify=0\nmetadata_expire=0\n", repoURL)
|
|
script := "set -euo pipefail; " +
|
|
"printf '%s' \"$REPO\" > /etc/yum.repos.d/dnflc.repo; " +
|
|
"dnf -y --disablerepo='*' --enablerepo=dnflc makecache; " +
|
|
"dnf -y --disablerepo='*' --enablerepo=dnflc install e2e-testpkg; " +
|
|
"rpm -q e2e-testpkg"
|
|
|
|
cmd := exec.Command("docker", "run", "--rm",
|
|
"--network", network,
|
|
"-e", "REPO="+repoConf,
|
|
"rockylinux:9", "bash", "-c", script)
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
t.Fatalf("real dnf install through a least_conn remote failed: %v\n%s", err, out)
|
|
}
|
|
if !strings.Contains(string(out), "e2e-testpkg-1.0-1") {
|
|
t.Fatalf("dnf did not install the expected package via least_conn remote; output:\n%s", out)
|
|
}
|
|
}
|