mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-11-09 23:59:16 -05:00
* 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.
98 lines
2.7 KiB
Go
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())
|
|
}
|