mirror of
https://codeberg.org/git-pages/git-pages.git
synced 2026-05-21 22:51:36 +00:00
Cache manifests in memory when using the S3 backend.
This commit is contained in:
@@ -71,6 +71,7 @@ An object store (filesystem, S3, ...) is used as the sole mechanism for state st
|
||||
- The `site/` prefix contains site manifests organized by domain and project name (e.g. `site/example.org/myproject` or `site/example.org/.index`).
|
||||
- The manifest is a Protobuf object containing a flat mapping of paths to entries. An entry is comprised of type (file, directory, symlink, etc) and data, which may be stored inline or refer to a blob.
|
||||
- A small amount of internal metadata within a manifest allows attributing deployments to their source and computing quotas.
|
||||
- The S3 backend caches manifests in memory. Since a manifest is necessary and sufficient to return `304 Not Modified` responses for a matching `ETag`, this drastically reduces navigation latency.
|
||||
- Additionally, the object store contains *staged manifests*, representing an in-progress update operation.
|
||||
- An update first creates a staged manifest, then uploads blobs, then replaces the deployed manifest with the staged one. This avoids TOCTTOU race conditions during garbage collection.
|
||||
- Stable marshalling allows addressing staged manifests by the hash of their contents.
|
||||
|
||||
1
go.mod
1
go.mod
@@ -4,6 +4,7 @@ go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/go-git/go-git/v6 v6.0.0-20250910120214-3a68d0404116
|
||||
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
|
||||
google.golang.org/protobuf v1.36.9
|
||||
|
||||
2
go.sum
2
go.sum
@@ -51,6 +51,8 @@ github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/maypok86/otter/v2 v2.2.1 h1:hnGssisMFkdisYcvQ8L019zpYQcdtPse+g0ps2i7cfI=
|
||||
github.com/maypok86/otter/v2 v2.2.1/go.mod h1:1NKY9bY+kB5jwCXBJfE59u+zAwOt6C7ni1FTlFFMqVs=
|
||||
github.com/minio/crc64nvme v1.0.2 h1:6uO1UxGAD+kwqWWp7mBFsi5gAse66C4NXO8cmcVculg=
|
||||
github.com/minio/crc64nvme v1.0.2/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
|
||||
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||
|
||||
@@ -18,6 +18,8 @@ import (
|
||||
|
||||
"github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
|
||||
"github.com/maypok86/otter/v2"
|
||||
)
|
||||
|
||||
type Backend interface {
|
||||
@@ -205,10 +207,16 @@ func (fs *FSBackend) DeleteManifest(name string) error {
|
||||
return fs.siteRoot.Remove(name)
|
||||
}
|
||||
|
||||
type CachedManifest struct {
|
||||
manifest *Manifest
|
||||
weight uint32
|
||||
}
|
||||
|
||||
type S3Backend struct {
|
||||
ctx context.Context
|
||||
client *minio.Client
|
||||
bucket string
|
||||
cache *otter.Cache[string, *CachedManifest]
|
||||
}
|
||||
|
||||
func NewS3Backend(
|
||||
@@ -244,7 +252,27 @@ func NewS3Backend(
|
||||
}
|
||||
}
|
||||
|
||||
return &S3Backend{ctx, client, bucket}, nil
|
||||
cacheConfig := config.Backend.S3.Cache
|
||||
var maxWeight uint64 = 134217728
|
||||
if cacheConfig.MaxSize != 0 {
|
||||
maxWeight = cacheConfig.MaxSize
|
||||
}
|
||||
var maxAge time.Duration = 5 * time.Second
|
||||
if cacheConfig.MaxAge != "" {
|
||||
maxAge, err = time.ParseDuration(cacheConfig.MaxAge)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("max-age: %s", err)
|
||||
}
|
||||
}
|
||||
cache := otter.Must[string, *CachedManifest](&otter.Options[string, *CachedManifest]{
|
||||
MaximumWeight: maxWeight,
|
||||
Weigher: func(key string, value *CachedManifest) uint32 {
|
||||
return value.weight
|
||||
},
|
||||
ExpiryCalculator: otter.ExpiryCreating[string, *CachedManifest](maxAge),
|
||||
})
|
||||
|
||||
return &S3Backend{ctx, client, bucket, cache}, nil
|
||||
}
|
||||
|
||||
func (s3 *S3Backend) Backend() Backend {
|
||||
@@ -308,20 +336,34 @@ func stagedManifestObjectName(manifestData []byte) string {
|
||||
}
|
||||
|
||||
func (s3 *S3Backend) GetManifest(name string) (*Manifest, error) {
|
||||
log.Printf("s3: get manifest %s\n", name)
|
||||
loader := func(ctx context.Context, name string) (*CachedManifest, error) {
|
||||
log.Printf("s3: get manifest %s\n", name)
|
||||
|
||||
object, err := s3.client.GetObject(s3.ctx, s3.bucket, manifestObjectName(name),
|
||||
minio.GetObjectOptions{})
|
||||
object, err := s3.client.GetObject(s3.ctx, s3.bucket, manifestObjectName(name),
|
||||
minio.GetObjectOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(object)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
manifest, err := DecodeManifest(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &CachedManifest{manifest, uint32(len(data))}, nil
|
||||
}
|
||||
|
||||
cached, err := s3.cache.Get(s3.ctx, name, otter.LoaderFunc[string, *CachedManifest](loader))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(object)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return DecodeManifest(data)
|
||||
return cached.manifest, err
|
||||
}
|
||||
|
||||
func (s3 *S3Backend) StageManifest(manifest *Manifest) error {
|
||||
|
||||
@@ -6,14 +6,14 @@ import (
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
)
|
||||
|
||||
type Listen struct {
|
||||
type ListenConfig struct {
|
||||
Protocol string `toml:"protocol"`
|
||||
Address string `toml:"address"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Pages Listen `toml:"pages"`
|
||||
Caddy Listen `toml:"caddy"`
|
||||
Pages ListenConfig `toml:"pages"`
|
||||
Caddy ListenConfig `toml:"caddy"`
|
||||
Wildcard struct {
|
||||
Domain string `toml:"domain"`
|
||||
CloneURL string `toml:"clone-url"`
|
||||
@@ -31,11 +31,15 @@ type Config struct {
|
||||
SecretAccessKey string `toml:"secret-access-key"`
|
||||
Region string `toml:"region"`
|
||||
Bucket string `toml:"bucket"`
|
||||
Cache struct {
|
||||
MaxSize uint64 `toml:"max-size"` // in bytes
|
||||
MaxAge string `toml:"max-age"`
|
||||
} `toml:"cache"`
|
||||
}
|
||||
} `toml:"backend"`
|
||||
}
|
||||
|
||||
func readConfig(path string, config *Config) error {
|
||||
func ReadConfig(path string, config *Config) error {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
var config Config
|
||||
var backend Backend
|
||||
|
||||
func serveHandler(name string, listen Listen, serve func(http.ResponseWriter, *http.Request)) {
|
||||
func serveHandler(name string, listen ListenConfig, serve func(http.ResponseWriter, *http.Request)) {
|
||||
listener, err := net.Listen(listen.Protocol, listen.Address)
|
||||
if err != nil {
|
||||
log.Fatalf("%s: %s\n", name, err)
|
||||
@@ -29,7 +29,7 @@ func main() {
|
||||
configPath := flag.String("config", "config.toml", "path to configuration file")
|
||||
flag.Parse()
|
||||
|
||||
if err = readConfig(*configPath, &config); err != nil {
|
||||
if err = ReadConfig(*configPath, &config); err != nil {
|
||||
log.Fatalln("configuration:", err)
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ func main() {
|
||||
|
||||
log.Println("ready")
|
||||
|
||||
if config.Caddy != (Listen{}) {
|
||||
if config.Caddy != (ListenConfig{}) {
|
||||
go serveHandler("caddy", config.Caddy, ServeCaddy)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user