341 lines
9.1 KiB
Go
341 lines
9.1 KiB
Go
package virtual
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.unkin.net/unkin/artifactapi/pkg/models"
|
|
)
|
|
|
|
func rpmPkgMember(name, ver string) *RPMMember {
|
|
return &RPMMember{RemoteName: name, Data: map[string][]byte{"primary": primaryXML(primaryPkgXML("foo", ver, "h"+ver, "foo-"+ver+".rpm"))}}
|
|
}
|
|
|
|
// staleEngine re-merges on every request; member "a" is fetched through fetch
|
|
// and has serve_stale set to stale.
|
|
func staleEngine(stale bool, fetch func() *RPMMember) (*Engine, *atomic.Int32) {
|
|
e, calls, _ := staleEngineErr(stale, func() (*RPMMember, error) { return fetch(), nil })
|
|
return e, calls
|
|
}
|
|
|
|
func staleEngineErr(stale bool, fetch func() (*RPMMember, error)) (*Engine, *atomic.Int32, *atomic.Bool) {
|
|
var calls atomic.Int32
|
|
var flag atomic.Bool
|
|
flag.Store(stale)
|
|
e := fakeEngine(nil)
|
|
e.getRemote = func(_ context.Context, name string) (*models.Remote, error) {
|
|
return &models.Remote{Name: name, RepoType: models.RepoTypeRemote, ServeStale: flag.Load()}, nil
|
|
}
|
|
e.rpmMember = func(_ context.Context, name string, _ bool) (*RPMMember, error) {
|
|
calls.Add(1)
|
|
return fetch()
|
|
}
|
|
return e, &calls, &flag
|
|
}
|
|
|
|
// waitRefresh blocks until the background refresh of sk, if any, has finished.
|
|
func waitRefresh(e *Engine, sk string) {
|
|
_, _, _ = e.sf.Do("stale\x00"+sk, func() (any, error) { return nil, nil })
|
|
}
|
|
|
|
const rpmStaleKey = "rpm\x00v\x00a"
|
|
|
|
func fetchRepomd(t *testing.T, e *Engine) string {
|
|
t.Helper()
|
|
b, _, err := e.Fetch(context.Background(), rpmVirt("a"), "repodata/repomd.xml", "")
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
return string(b)
|
|
}
|
|
|
|
func TestServeStaleMemberDoesNotBlockMerge(t *testing.T) {
|
|
release := make(chan struct{})
|
|
refreshing := make(chan struct{}, 1)
|
|
var slow atomic.Bool
|
|
e, calls := staleEngine(true, func() *RPMMember {
|
|
if slow.Load() {
|
|
select {
|
|
case refreshing <- struct{}{}:
|
|
default:
|
|
}
|
|
<-release
|
|
return rpmPkgMember("a", "2")
|
|
}
|
|
return rpmPkgMember("a", "1")
|
|
})
|
|
v1 := fetchRepomd(t, e)
|
|
slow.Store(true)
|
|
|
|
for range 3 {
|
|
done := make(chan string)
|
|
go func() { done <- fetchRepomd(t, e) }()
|
|
select {
|
|
case got := <-done:
|
|
if got != v1 {
|
|
t.Fatal("stale merge should serve the previous member copy")
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("merge blocked on a serve_stale member refresh")
|
|
}
|
|
}
|
|
select {
|
|
case <-refreshing:
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("background refresh never started")
|
|
}
|
|
if n := calls.Load(); n != 2 {
|
|
t.Fatalf("member fetched %d times, want 2 (cold + one single-flighted refresh)", n)
|
|
}
|
|
|
|
close(release)
|
|
deadline := time.Now().Add(5 * time.Second)
|
|
for fetchRepomd(t, e) == v1 {
|
|
if time.Now().After(deadline) {
|
|
t.Fatal("background refresh never reached a merge")
|
|
}
|
|
time.Sleep(5 * time.Millisecond)
|
|
}
|
|
}
|
|
|
|
func assertMergeBlocks(t *testing.T, e *Engine, release chan struct{}) {
|
|
t.Helper()
|
|
done := make(chan struct{})
|
|
go func() { fetchRepomd(t, e); close(done) }()
|
|
select {
|
|
case <-done:
|
|
t.Fatal("merge returned before the member fetch finished")
|
|
case <-time.After(50 * time.Millisecond):
|
|
}
|
|
close(release)
|
|
<-done
|
|
}
|
|
|
|
func TestNonServeStaleMemberBlocksMerge(t *testing.T) {
|
|
release := make(chan struct{})
|
|
var slow atomic.Bool
|
|
e, _ := staleEngine(false, func() *RPMMember {
|
|
if slow.Load() {
|
|
<-release
|
|
}
|
|
return rpmPkgMember("a", "1")
|
|
})
|
|
fetchRepomd(t, e)
|
|
slow.Store(true)
|
|
assertMergeBlocks(t, e, release)
|
|
}
|
|
|
|
func TestServeStaleColdStartBlocksMerge(t *testing.T) {
|
|
release := make(chan struct{})
|
|
e, _ := staleEngine(true, func() *RPMMember {
|
|
<-release
|
|
return rpmPkgMember("a", "1")
|
|
})
|
|
assertMergeBlocks(t, e, release)
|
|
}
|
|
|
|
func TestServeStaleRefreshErrorKeepsCopy(t *testing.T) {
|
|
var down atomic.Bool
|
|
e, _, _ := staleEngineErr(true, func() (*RPMMember, error) {
|
|
if down.Load() {
|
|
return nil, errors.New("upstream down")
|
|
}
|
|
return rpmPkgMember("a", "1"), nil
|
|
})
|
|
v1 := fetchRepomd(t, e)
|
|
down.Store(true)
|
|
for range 3 {
|
|
if got := fetchRepomd(t, e); got != v1 {
|
|
t.Fatal("a failed refresh should keep serving the previous copy")
|
|
}
|
|
waitRefresh(e, rpmStaleKey)
|
|
}
|
|
}
|
|
|
|
func TestServeStaleMaxAgeFallsBackToFetch(t *testing.T) {
|
|
var down atomic.Bool
|
|
e, _, _ := staleEngineErr(true, func() (*RPMMember, error) {
|
|
if down.Load() {
|
|
return nil, errors.New("upstream down")
|
|
}
|
|
return rpmPkgMember("a", "1"), nil
|
|
})
|
|
fetchRepomd(t, e)
|
|
down.Store(true)
|
|
e.mu.Lock()
|
|
e.stale[rpmStaleKey].at = time.Now().Add(-maxStaleAge)
|
|
e.mu.Unlock()
|
|
if _, _, err := e.Fetch(context.Background(), rpmVirt("a"), "repodata/repomd.xml", ""); err == nil {
|
|
t.Fatal("a copy past maxStaleAge should not be served")
|
|
}
|
|
if len(e.stale) != 0 {
|
|
t.Fatal("an expired copy should be dropped")
|
|
}
|
|
}
|
|
|
|
func TestServeStaleToggleOffClearsCopy(t *testing.T) {
|
|
var down atomic.Bool
|
|
e, _, flag := staleEngineErr(true, func() (*RPMMember, error) {
|
|
if down.Load() {
|
|
return nil, errors.New("upstream down")
|
|
}
|
|
return rpmPkgMember("a", "1"), nil
|
|
})
|
|
fetchRepomd(t, e)
|
|
flag.Store(false)
|
|
fetchRepomd(t, e)
|
|
if len(e.stale) != 0 {
|
|
t.Fatal("turning serve_stale off should drop the copy")
|
|
}
|
|
flag.Store(true)
|
|
down.Store(true)
|
|
if _, _, err := e.Fetch(context.Background(), rpmVirt("a"), "repodata/repomd.xml", ""); err == nil {
|
|
t.Fatal("re-enabling serve_stale must not resurrect the old copy")
|
|
}
|
|
}
|
|
|
|
func staleRemote(_ context.Context, name string) (*models.Remote, error) {
|
|
return &models.Remote{Name: name, RepoType: models.RepoTypeRemote, ServeStale: true}, nil
|
|
}
|
|
|
|
func TestServeStaleDebAndAlpine(t *testing.T) {
|
|
var down atomic.Bool
|
|
e := fakeEngine(nil)
|
|
e.getRemote = staleRemote
|
|
e.debMember = func(_ context.Context, name, _ string, _ bool) (*DebMember, error) {
|
|
if down.Load() {
|
|
return nil, errors.New("upstream down")
|
|
}
|
|
return suiteDebMember(name, "1"), nil
|
|
}
|
|
e.apkMember = func(_ context.Context, name, _ string) (*AlpineMember, error) {
|
|
if down.Load() {
|
|
return nil, errors.New("upstream down")
|
|
}
|
|
return &AlpineMember{RemoteName: name, Index: []byte(apkRec("foo", "1-r0", "x86_64"))}, nil
|
|
}
|
|
for _, tc := range []struct {
|
|
virt models.Virtual
|
|
path string
|
|
}{
|
|
{debVirt("a"), "dists/s/Release"},
|
|
{alpineVirt("a"), "x86_64/APKINDEX.tar.gz"},
|
|
} {
|
|
down.Store(false)
|
|
first, _, err := e.Fetch(context.Background(), tc.virt, tc.path, "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
down.Store(true)
|
|
got, _, err := e.Fetch(context.Background(), tc.virt, tc.path, "")
|
|
if err != nil || string(got) != string(first) {
|
|
t.Fatalf("%s: want the previous copy while the member is down, got err %v", tc.virt.PackageType, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestServeStaleEvictionDropsLateRefresh(t *testing.T) {
|
|
release := make(chan struct{})
|
|
var slow atomic.Bool
|
|
e := fakeEngine(nil)
|
|
e.mergeTTL = time.Minute
|
|
e.getRemote = staleRemote
|
|
e.debMember = func(_ context.Context, name, base string, _ bool) (*DebMember, error) {
|
|
if base == "dists/s0/" && slow.Load() {
|
|
<-release
|
|
}
|
|
return suiteDebMember(name, "1"), nil
|
|
}
|
|
fetch := func(suite string) {
|
|
t.Helper()
|
|
if _, _, err := e.Fetch(context.Background(), debVirt("a"), "dists/"+suite+"/Release", ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
fetch("s0")
|
|
slow.Store(true)
|
|
e.mu.Lock()
|
|
e.gens["deb\x00v\x00dists/s0/"].at = time.Time{}
|
|
e.mu.Unlock()
|
|
fetch("s0")
|
|
for i := range maxDebSuites {
|
|
time.Sleep(time.Microsecond)
|
|
fetch(fmt.Sprintf("s%d", i+1))
|
|
}
|
|
close(release)
|
|
sk := "deb\x00v\x00dists/s0/\x00a"
|
|
waitRefresh(e, sk)
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
if e.gens["deb\x00v\x00dists/s0/"] != nil {
|
|
t.Fatal("suite s0 should be evicted")
|
|
}
|
|
if e.stale[sk] != nil {
|
|
t.Fatal("a refresh finishing after eviction must not re-insert its copy")
|
|
}
|
|
if len(e.stale) != maxDebSuites {
|
|
t.Fatalf("stale holds %d copies, want %d", len(e.stale), maxDebSuites)
|
|
}
|
|
}
|
|
|
|
func TestServeStaleEvictionDropsLateColdFetch(t *testing.T) {
|
|
release := make(chan struct{})
|
|
var slow atomic.Bool
|
|
e := fakeEngine(nil)
|
|
e.mergeTTL = time.Minute
|
|
e.getRemote = staleRemote
|
|
e.debMember = func(_ context.Context, name, base string, _ bool) (*DebMember, error) {
|
|
if base == "dists/s0/" && slow.Load() {
|
|
<-release
|
|
}
|
|
return suiteDebMember(name, "1"), nil
|
|
}
|
|
fetch := func(suite string) {
|
|
if _, _, err := e.Fetch(context.Background(), debVirt("a"), "dists/"+suite+"/Release", ""); err != nil {
|
|
t.Error(err)
|
|
}
|
|
}
|
|
sk := "deb\x00v\x00dists/s0/\x00a"
|
|
fetch("s0")
|
|
slow.Store(true)
|
|
e.mu.Lock()
|
|
e.gens["deb\x00v\x00dists/s0/"].at = time.Time{}
|
|
delete(e.stale, sk)
|
|
e.mu.Unlock()
|
|
done := make(chan struct{})
|
|
go func() { defer close(done); fetch("s0") }()
|
|
for {
|
|
e.mu.Lock()
|
|
cold := e.stale[sk] != nil
|
|
e.mu.Unlock()
|
|
if cold {
|
|
break
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
for i := range maxDebSuites {
|
|
time.Sleep(time.Microsecond)
|
|
fetch(fmt.Sprintf("s%d", i+1))
|
|
}
|
|
close(release)
|
|
<-done
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
if e.stale[sk] != nil {
|
|
t.Fatal("a cold fetch finishing after eviction must not re-insert its copy")
|
|
}
|
|
}
|
|
|
|
func TestMergePrunesExpiredCopiesOfOtherMembers(t *testing.T) {
|
|
e, _ := staleEngine(false, func() *RPMMember { return rpmPkgMember("a", "1") })
|
|
e.stale = map[string]*staleEntry{"rpm\x00gone\x00x": {m: rpmPkgMember("x", "1"), at: time.Now().Add(-maxStaleAge)}}
|
|
fetchRepomd(t, e)
|
|
if len(e.stale) != 0 {
|
|
t.Fatal("any merge should prune expired copies of removed members")
|
|
}
|
|
}
|