mirror of
https://codeberg.org/git-pages/git-pages.git
synced 2026-05-14 03:01:48 +00:00
Add stale-while-revalidate support to the cache.
This commit is contained in:
@@ -34,6 +34,7 @@ max-size = "256MB"
|
||||
[storage.s3.site-cache]
|
||||
max-size = "16MB"
|
||||
max-age = "60s"
|
||||
max-stale = "1h"
|
||||
|
||||
[limits]
|
||||
max-site-size = "128M"
|
||||
|
||||
1
go.mod
1
go.mod
@@ -14,6 +14,7 @@ require (
|
||||
github.com/maypok86/otter/v2 v2.2.1
|
||||
github.com/minio/minio-go/v7 v7.0.95
|
||||
github.com/pelletier/go-toml/v2 v2.2.4
|
||||
github.com/pquerna/cachecontrol v0.2.0
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/samber/slog-multi v1.5.0
|
||||
github.com/tj/go-redirects v0.0.0-20200911105812-fd1ba1020b37
|
||||
|
||||
2
go.sum
2
go.sum
@@ -97,6 +97,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pquerna/cachecontrol v0.2.0 h1:vBXSNuE5MYP9IJ5kjsdo8uq+w41jSPgvba2DEnkRx9k=
|
||||
github.com/pquerna/cachecontrol v0.2.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI=
|
||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
|
||||
@@ -34,7 +34,7 @@ type Backend interface {
|
||||
DeleteBlob(ctx context.Context, name string) error
|
||||
|
||||
// Retrieve a manifest.
|
||||
GetManifest(ctx context.Context, name string) (*Manifest, error)
|
||||
GetManifest(ctx context.Context, name string, opts GetManifestOptions) (*Manifest, error)
|
||||
|
||||
// Stage a manifest. This operation stores a new version of a manifest, locking any blobs
|
||||
// referenced from it in place (for garbage collection purposes) but without any other side
|
||||
@@ -52,6 +52,10 @@ type Backend interface {
|
||||
CheckDomain(ctx context.Context, domain string) (found bool, err error)
|
||||
}
|
||||
|
||||
type GetManifestOptions struct {
|
||||
BypassCache bool
|
||||
}
|
||||
|
||||
var backend Backend
|
||||
|
||||
func ConfigureBackend(config *StorageConfig) (err error) {
|
||||
|
||||
@@ -133,7 +133,7 @@ func (fs *FSBackend) DeleteBlob(ctx context.Context, name string) error {
|
||||
return fs.blobRoot.Remove(blobPath)
|
||||
}
|
||||
|
||||
func (fs *FSBackend) GetManifest(ctx context.Context, name string) (*Manifest, error) {
|
||||
func (fs *FSBackend) GetManifest(ctx context.Context, name string, opts GetManifestOptions) (*Manifest, error) {
|
||||
data, err := fs.siteRoot.ReadFile(name)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil, fmt.Errorf("%w: %s", errNotFound, err.(*os.PathError).Path)
|
||||
|
||||
@@ -121,8 +121,11 @@ func makeCacheOptions[K comparable, V any](
|
||||
options.MaximumWeight = config.MaxSize.Bytes()
|
||||
options.Weigher = weigher
|
||||
}
|
||||
if config.MaxAge != 0 {
|
||||
options.ExpiryCalculator = otter.ExpiryWriting[K, V](time.Duration(config.MaxAge))
|
||||
if config.MaxStale != 0 {
|
||||
options.RefreshCalculator = otter.RefreshWriting[K, V](time.Duration(config.MaxAge))
|
||||
}
|
||||
if config.MaxAge != 0 || config.MaxStale != 0 {
|
||||
options.ExpiryCalculator = otter.ExpiryWriting[K, V](time.Duration(config.MaxAge + config.MaxStale))
|
||||
}
|
||||
return options
|
||||
}
|
||||
@@ -284,7 +287,7 @@ func stagedManifestObjectName(manifestData []byte) string {
|
||||
return fmt.Sprintf("dirty/%x", sha256.Sum256(manifestData))
|
||||
}
|
||||
|
||||
func (s3 *S3Backend) GetManifest(ctx context.Context, name string) (*Manifest, error) {
|
||||
func (s3 *S3Backend) GetManifest(ctx context.Context, name string, opts GetManifestOptions) (*Manifest, error) {
|
||||
loader := func(ctx context.Context, name string) (*CachedManifest, error) {
|
||||
manifest, size, err := func() (*Manifest, uint32, error) {
|
||||
log.Printf("s3: get manifest %s\n", name)
|
||||
@@ -322,6 +325,13 @@ func (s3 *S3Backend) GetManifest(ctx context.Context, name string) (*Manifest, e
|
||||
}
|
||||
}
|
||||
|
||||
if opts.BypassCache {
|
||||
entry, found := s3.siteCache.Cache.GetEntry(name)
|
||||
if found && entry.RefreshableAt().Before(time.Now()) {
|
||||
s3.siteCache.Cache.Invalidate(name)
|
||||
}
|
||||
}
|
||||
|
||||
cached, err := s3.siteCache.Get(ctx, name, otter.LoaderFunc[string, *CachedManifest](loader))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
11
src/cache.go
11
src/cache.go
@@ -13,19 +13,20 @@ type weightedCacheEntry interface {
|
||||
}
|
||||
|
||||
type trackedLoader[K comparable, V any] struct {
|
||||
loader otter.Loader[K, V]
|
||||
invoked bool
|
||||
loader otter.Loader[K, V]
|
||||
loaded bool
|
||||
reloaded bool
|
||||
}
|
||||
|
||||
func (l *trackedLoader[K, V]) Load(ctx context.Context, key K) (V, error) {
|
||||
val, err := l.loader.Load(ctx, key)
|
||||
l.invoked = true
|
||||
l.loaded = true
|
||||
return val, err
|
||||
}
|
||||
|
||||
func (l *trackedLoader[K, V]) Reload(ctx context.Context, key K, oldValue V) (V, error) {
|
||||
val, err := l.loader.Reload(ctx, key, oldValue)
|
||||
l.invoked = true
|
||||
l.reloaded = true
|
||||
return val, err
|
||||
}
|
||||
|
||||
@@ -67,7 +68,7 @@ func (c *observedCache[K, V]) Get(ctx context.Context, key K, loader otter.Loade
|
||||
observedLoader := trackedLoader[K, V]{loader: loader}
|
||||
val, err := c.Cache.Get(ctx, key, &observedLoader)
|
||||
if err == nil {
|
||||
if observedLoader.invoked {
|
||||
if observedLoader.loaded {
|
||||
if c.metrics.MissNumberCounter != nil {
|
||||
c.metrics.MissNumberCounter.Inc()
|
||||
}
|
||||
|
||||
@@ -59,8 +59,9 @@ type WildcardConfig struct {
|
||||
}
|
||||
|
||||
type CacheConfig struct {
|
||||
MaxSize datasize.ByteSize `toml:"max-size"`
|
||||
MaxAge Duration `toml:"max-age"`
|
||||
MaxSize datasize.ByteSize `toml:"max-size"`
|
||||
MaxAge Duration `toml:"max-age"`
|
||||
MaxStale Duration `toml:"max-stale"`
|
||||
}
|
||||
|
||||
type StorageConfig struct {
|
||||
@@ -81,7 +82,7 @@ type S3Config struct {
|
||||
Region string `toml:"region"`
|
||||
Bucket string `toml:"bucket"`
|
||||
BlobCache CacheConfig `toml:"blob-cache" default:"{\"MaxSize\":\"256MB\"}"`
|
||||
SiteCache CacheConfig `toml:"site-cache" default:"{\"MaxAge\":\"60s\",\"MaxSize\":\"16MB\"}"`
|
||||
SiteCache CacheConfig `toml:"site-cache" default:"{\"MaxAge\":\"60s\",\"MaxStale\":\"1h\",\"MaxSize\":\"16MB\"}"`
|
||||
}
|
||||
|
||||
type LimitsConfig struct {
|
||||
|
||||
@@ -149,7 +149,7 @@ func main() {
|
||||
webRoot += "/.index"
|
||||
}
|
||||
|
||||
manifest, err := backend.GetManifest(context.Background(), webRoot)
|
||||
manifest, err := backend.GetManifest(context.Background(), webRoot, GetManifestOptions{})
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
@@ -232,9 +232,16 @@ func (backend *observedBackend) DeleteBlob(ctx context.Context, name string) (er
|
||||
return
|
||||
}
|
||||
|
||||
func (backend *observedBackend) GetManifest(ctx context.Context, name string) (manifest *Manifest, err error) {
|
||||
func (backend *observedBackend) GetManifest(
|
||||
ctx context.Context,
|
||||
name string,
|
||||
opts GetManifestOptions,
|
||||
) (
|
||||
manifest *Manifest,
|
||||
err error,
|
||||
) {
|
||||
span, ctx := ObserveFunction(ctx, "GetManifest", "manifest.name", name)
|
||||
if manifest, err = backend.inner.GetManifest(ctx, name); err == nil {
|
||||
if manifest, err = backend.inner.GetManifest(ctx, name, opts); err == nil {
|
||||
manifestsRetrievedCount.Inc()
|
||||
}
|
||||
span.Finish()
|
||||
|
||||
18
src/pages.go
18
src/pages.go
@@ -17,6 +17,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/klauspost/compress/zstd"
|
||||
"github.com/pquerna/cachecontrol/cacheobject"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
)
|
||||
@@ -70,6 +71,17 @@ func getPage(w http.ResponseWriter, r *http.Request) error {
|
||||
var sitePath string
|
||||
var manifest *Manifest
|
||||
|
||||
cacheControl, err := cacheobject.ParseRequestCacheControl(r.Header.Get("Cache-Control"))
|
||||
if err != nil {
|
||||
cacheControl = &cacheobject.RequestCacheDirectives{
|
||||
MaxAge: -1,
|
||||
MaxStale: -1,
|
||||
MinFresh: -1,
|
||||
}
|
||||
}
|
||||
|
||||
bypassCache := cacheControl.NoCache || cacheControl.MaxAge == 0
|
||||
|
||||
host, err := GetHost(r)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -77,13 +89,15 @@ func getPage(w http.ResponseWriter, r *http.Request) error {
|
||||
|
||||
sitePath = strings.TrimPrefix(r.URL.Path, "/")
|
||||
if projectName, projectPath, found := strings.Cut(sitePath, "/"); found {
|
||||
projectManifest, err := backend.GetManifest(r.Context(), makeWebRoot(host, projectName))
|
||||
projectManifest, err := backend.GetManifest(r.Context(), makeWebRoot(host, projectName),
|
||||
GetManifestOptions{BypassCache: bypassCache})
|
||||
if err == nil {
|
||||
sitePath, manifest = projectPath, projectManifest
|
||||
}
|
||||
}
|
||||
if manifest == nil {
|
||||
manifest, err = backend.GetManifest(r.Context(), makeWebRoot(host, ".index"))
|
||||
manifest, err = backend.GetManifest(r.Context(), makeWebRoot(host, ".index"),
|
||||
GetManifestOptions{BypassCache: bypassCache})
|
||||
if manifest == nil {
|
||||
if found, fallbackErr := HandleWildcardFallback(w, r); found {
|
||||
return fallbackErr
|
||||
|
||||
@@ -30,7 +30,7 @@ func Update(ctx context.Context, webRoot string, manifest *Manifest) UpdateResul
|
||||
var err error
|
||||
|
||||
outcome := UpdateError
|
||||
oldManifest, _ = backend.GetManifest(ctx, webRoot)
|
||||
oldManifest, _ = backend.GetManifest(ctx, webRoot, GetManifestOptions{})
|
||||
if IsManifestEmpty(manifest) {
|
||||
newManifest, err = manifest, backend.DeleteManifest(ctx, webRoot)
|
||||
if err == nil {
|
||||
|
||||
Reference in New Issue
Block a user