Watch
1
0
Fork
You've already forked pkg-proxy
1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-11-09 23:59:16 -05:00
pkg-proxy/internal/server/mirror_schedule_test.go
Christian Heim 8d96791f1a
Re-mirror container images on a schedule (#508)
* Add mirror.schedules, a list of image tags the server re-mirrors at a fixed interval. Digests are rejected since they never move.

* Run each schedule through the normal mirror, one run after the other. The first run starts at a random point within ten minutes and later runs get ±10 % jitter, so several instances don't hit the registry at once.

* Start the schedules with the server and let shutdown wait for a running refresh before storage and database are closed. Docs included.

* Check cached layers during a mirror without opening them, so a refresh no longer counts as a cache hit and doesn't keep scheduled images at the top of LRU eviction.
2026-10-18 18:33:13 +01:00

98 lines
2.7 KiB
Go

package server
import (
"context"
"io"
"log/slog"
"sync/atomic"
"testing"
"time"
"github.com/git-pkgs/proxy/internal/handler"
"github.com/git-pkgs/proxy/internal/mirror"
)
// blockingImages blocks every mirror until release is closed, and with
// ignoreCancel even past the end of its context.
type blockingImages struct {
started atomic.Bool
release chan struct{}
ignoreCancel bool
}
func (b *blockingImages) MirrorImage(ctx context.Context, _ handler.ImageRef, _ []handler.Platform) (handler.ImageResult, error) {
b.started.Store(true)
if b.ignoreCancel {
<-b.release
return handler.ImageResult{}, nil
}
select {
case <-b.release:
case <-ctx.Done():
}
return handler.ImageResult{}, ctx.Err()
}
func startBlockingSchedule(t *testing.T, images *blockingImages) (*Server, context.CancelFunc) {
t.Helper()
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
m := mirror.New(nil, nil, nil, logger, 1)
m.Images = images
// A 1ns interval starts the first run right away.
s := &Server{logger: logger, scheduler: mirror.NewScheduler(m, []mirror.Schedule{
{PURLs: []string{"pkg:docker/alpine@3"}, Interval: time.Nanosecond},
}, logger)}
ctx, cancel := context.WithCancel(context.Background())
s.scheduler.Start(ctx)
deadline := time.Now().Add(5 * time.Second)
for !images.started.Load() {
if time.Now().After(deadline) {
t.Fatal("scheduled run did not start")
}
time.Sleep(time.Millisecond)
}
return s, cancel
}
func TestWaitForMirrorSchedulesWaitsForRunningRun(t *testing.T) {
images := &blockingImages{release: make(chan struct{}), ignoreCancel: true}
s, cancel := startBlockingSchedule(t, images)
cancel()
done := make(chan struct{})
go func() {
s.waitForMirrorSchedules(context.Background())
close(done)
}()
select {
case <-done:
t.Fatal("waitForMirrorSchedules returned while a run was still going")
case <-time.After(50 * time.Millisecond):
}
close(images.release)
select {
case <-done:
case <-time.After(5 * time.Second):
t.Fatal("waitForMirrorSchedules did not return after the run ended")
}
}
func TestWaitForMirrorSchedulesGivesUpAtDeadline(t *testing.T) {
images := &blockingImages{release: make(chan struct{}), ignoreCancel: true}
s, cancel := startBlockingSchedule(t, images)
defer close(images.release)
cancel()
ctx, stop := context.WithTimeout(context.Background(), 20*time.Millisecond)
defer stop()
started := time.Now()
s.waitForMirrorSchedules(ctx)
if elapsed := time.Since(started); elapsed > 5*time.Second {
t.Errorf("waitForMirrorSchedules took %v past its deadline", elapsed)
}
}
func TestWaitForMirrorSchedulesWithoutSchedules(t *testing.T) {
s := &Server{}
s.waitForMirrorSchedules(context.Background())
}