From 6f8d266f4af37ff9290f4d336f2b5eee8864d1cb Mon Sep 17 00:00:00 2001 From: Mac Chaffee Date: Sun, 20 Oct 2024 20:34:42 -0400 Subject: [PATCH 1/4] Implement caching using Souin --- .env.example | 4 ++- caddy/Caddyfile.pgs | 21 +++++++++++ caddy/Dockerfile | 4 ++- pgs/config.go | 4 +++ pgs/uploader.go | 78 +++++++++++++++++++++++++++++++++++----- pgs/web_asset_handler.go | 3 ++ shared/config.go | 2 ++ 7 files changed, 106 insertions(+), 10 deletions(-) diff --git a/.env.example b/.env.example index cd56239e..1de8bede 100644 --- a/.env.example +++ b/.env.example @@ -101,7 +101,7 @@ FEEDS_DOMAIN=feeds.dev.pico.sh:3004 FEEDS_PROTOCOL=http FEEDS_DEBUG=1 -PGS_CADDYFILE=./caddy/Caddyfile +PGS_CADDYFILE=./caddy/Caddyfile.pgs PGS_V4= PGS_V6= PGS_HTTP_V4=$PGS_V4:80 @@ -118,6 +118,8 @@ PGS_DOMAIN=pgs.dev.pico.sh:3005 PGS_PROTOCOL=http PGS_STORAGE_DIR=.storage PGS_DEBUG=1 +PGS_CACHE_USER=testuser +PGS_CACHE_PASSWORD=password PICO_CADDYFILE=./caddy/Caddyfile.pico PICO_V4= diff --git a/caddy/Caddyfile.pgs b/caddy/Caddyfile.pgs index e1841ff1..35fd79bb 100644 --- a/caddy/Caddyfile.pgs +++ b/caddy/Caddyfile.pgs @@ -8,6 +8,14 @@ metrics trusted_proxies static 0.0.0.0/0 } + cache { + ttl 300s + max_cacheable_body_bytes 1000000 + otter + api { + souin + } + } } *.{$APP_DOMAIN}, {$APP_DOMAIN} { @@ -21,6 +29,19 @@ dns cloudflare {$CF_API_TOKEN} resolvers 1.1.1.1 } + route { + @souinApi path /souin-api/* + basic_auth @souinApi { + testuser $2a$14$i1G0lil5qti7qahb4.Kte.wP/3O8uaStduzhBBtuDUZhMJeSjxbqm + } + cache { + regex { + exclude /check + } + } + reverse_proxy web:3000 + } + encode zstd gzip header { diff --git a/caddy/Dockerfile b/caddy/Dockerfile index 58fb629d..3922a4eb 100644 --- a/caddy/Dockerfile +++ b/caddy/Dockerfile @@ -8,7 +8,9 @@ ARG TARGETARCH ENV GOOS=${TARGETOS} GOARCH=${TARGETARCH} RUN xcaddy build \ - --with github.com/caddy-dns/cloudflare + --with github.com/caddy-dns/cloudflare \ + --with github.com/darkweak/souin/plugins/caddy@v1.7.2 \ + --with github.com/darkweak/storages/otter/caddy FROM caddy:alpine diff --git a/pgs/config.go b/pgs/config.go index f0cdaa0f..04ff48c4 100644 --- a/pgs/config.go +++ b/pgs/config.go @@ -16,6 +16,8 @@ func NewConfigSite() *shared.ConfigSite { port := utils.GetEnv("PGS_WEB_PORT", "3000") protocol := utils.GetEnv("PGS_PROTOCOL", "https") storageDir := utils.GetEnv("PGS_STORAGE_DIR", ".storage") + pgsCacheUser := utils.GetEnv("PGS_CACHE_USER", "") + pgsCachePass := utils.GetEnv("PGS_CACHE_PASSWORD", "") minioURL := utils.GetEnv("MINIO_URL", "") minioUser := utils.GetEnv("MINIO_ROOT_USER", "") minioPass := utils.GetEnv("MINIO_ROOT_PASSWORD", "") @@ -27,6 +29,8 @@ func NewConfigSite() *shared.ConfigSite { Protocol: protocol, DbURL: dbURL, StorageDir: storageDir, + CacheUser: pgsCacheUser, + CachePassword: pgsCachePass, MinioURL: minioURL, MinioUser: minioUser, MinioPass: minioPass, diff --git a/pgs/uploader.go b/pgs/uploader.go index a2d79cb9..bd1f6bc8 100644 --- a/pgs/uploader.go +++ b/pgs/uploader.go @@ -6,11 +6,13 @@ import ( "io" "io/fs" "log/slog" + "net/http" "os" "path" "path/filepath" "slices" "strings" + "sync" "time" "github.com/charmbracelet/ssh" @@ -97,16 +99,21 @@ type FileData struct { } type UploadAssetHandler struct { - DBPool db.DB - Cfg *shared.ConfigSite - Storage sst.ObjectStorage + DBPool db.DB + Cfg *shared.ConfigSite + Storage sst.ObjectStorage + CacheClearingQueue chan string } func NewUploadAssetHandler(dbpool db.DB, cfg *shared.ConfigSite, storage sst.ObjectStorage) *UploadAssetHandler { + // Enable buffering so we don't slow down uploads. + ch := make(chan string, 100) + go runCacheQueue(ch, cfg) return &UploadAssetHandler{ - DBPool: dbpool, - Cfg: cfg, - Storage: storage, + DBPool: dbpool, + Cfg: cfg, + Storage: storage, + CacheClearingQueue: ch, } } @@ -405,6 +412,7 @@ func (h *UploadAssetHandler) Write(s ssh.Session, entry *sendutils.FileEntry) (s utils.BytesToGB(maxSize), (float32(nextStorageSize)/float32(maxSize))*100, ) + h.CacheClearingQueue <- fmt.Sprintf("%s-%s", user.Name, projectName) return str, nil } @@ -471,8 +479,9 @@ func (h *UploadAssetHandler) Delete(s ssh.Session, entry *sendutils.FileEntry) e return err } } - - return h.Storage.DeleteObject(bucket, assetFilepath) + err = h.Storage.DeleteObject(bucket, assetFilepath) + h.CacheClearingQueue <- fmt.Sprintf("%s-%s", user.Name, projectName) + return err } func (h *UploadAssetHandler) validateAsset(data *FileData) (bool, error) { @@ -518,3 +527,56 @@ func (h *UploadAssetHandler) writeAsset(reader io.Reader, data *FileData) (int64 ) return fsize, err } + +// runCacheQueue processes requests to purge the cache for a single site. +// One message arrives per file that is written/deleted during uploads. +// Repeated messages for the same site are grouped so that we only flush once +// per site per 5 seconds. +func runCacheQueue(ch chan string, cfg *shared.ConfigSite) { + cacheApiUrl := fmt.Sprintf("https://%s/souin-api/souin/", cfg.Domain) + var pendingFlushes sync.Map + tick := time.Tick(5 * time.Second) + for { + select { + case host := <-ch: + pendingFlushes.Store(host, host) + case <-tick: + go func() { + pendingFlushes.Range(func(key, value any) bool { + pendingFlushes.Delete(key) + err := purgeCache(key.(string), cacheApiUrl, cfg.CacheUser, cfg.CachePassword) + if err != nil { + cfg.Logger.Error("failed to clear cache", "err", err.Error()) + } + return true + }) + }() + } + } +} + +// purgeCache send an HTTP request to the pgs Caddy instance which purges +// cached entries for a given subdomain (like "fakeuser-www-proj"). We set a +// "surrogate-key: " header on every pgs response which ensures all +// cached assets for a given subdomain are grouped under a single key (which is +// separate from the "GET-https-example.com-/path" key used for serving files +// from the cache). +func purgeCache(subdomain string, cacheApiUrl string, username string, password string) error { + client := &http.Client{ + Timeout: time.Second * 5, + } + req, err := http.NewRequest("PURGE", cacheApiUrl, nil) + if err != nil { + return err + } + req.Header.Add("Surrogate-Key", subdomain) + req.SetBasicAuth(username, password) + resp, err := client.Do(req) + if err != nil { + return err + } + if resp.StatusCode != 204 { + return fmt.Errorf("received unexpected response code %d", resp.StatusCode) + } + return nil +} diff --git a/pgs/web_asset_handler.go b/pgs/web_asset_handler.go index fac47df3..0f32dad0 100644 --- a/pgs/web_asset_handler.go +++ b/pgs/web_asset_handler.go @@ -216,6 +216,9 @@ func (h *ApiAssetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Set("content-type", contentType) } + // Allows us to invalidate the cache when files are modified + w.Header().Set("surrogate-key", h.Subdomain) + finContentType := w.Header().Get("content-type") logger.Info( diff --git a/shared/config.go b/shared/config.go index 2ebc85ec..f3421999 100644 --- a/shared/config.go +++ b/shared/config.go @@ -38,6 +38,8 @@ type ConfigSite struct { Protocol string DbURL string StorageDir string + CacheUser string + CachePassword string MinioURL string MinioUser string MinioPass string From 9c903d2cec7c9ac2c176a8c11375babce07dc35e Mon Sep 17 00:00:00 2001 From: Mac Chaffee Date: Tue, 29 Oct 2024 22:31:59 -0400 Subject: [PATCH 2/4] Update to Souin v1.7.4 to fix surrogate-key bug --- caddy/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/caddy/Dockerfile b/caddy/Dockerfile index 3922a4eb..f59c1f1c 100644 --- a/caddy/Dockerfile +++ b/caddy/Dockerfile @@ -9,7 +9,7 @@ ENV GOOS=${TARGETOS} GOARCH=${TARGETARCH} RUN xcaddy build \ --with github.com/caddy-dns/cloudflare \ - --with github.com/darkweak/souin/plugins/caddy@v1.7.2 \ + --with github.com/darkweak/souin/plugins/caddy@v1.7.4 \ --with github.com/darkweak/storages/otter/caddy FROM caddy:alpine From 7755d63d6f1f6514ad9e9bfb0626a440baae1dcc Mon Sep 17 00:00:00 2001 From: Mac Chaffee Date: Tue, 29 Oct 2024 22:43:40 -0400 Subject: [PATCH 3/4] Disable caching from proxied responses --- pgs/web_asset_handler.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pgs/web_asset_handler.go b/pgs/web_asset_handler.go index 0f32dad0..c29e9e94 100644 --- a/pgs/web_asset_handler.go +++ b/pgs/web_asset_handler.go @@ -121,6 +121,11 @@ func (h *ApiAssetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { r.Host = destUrl.Host r.URL = destUrl } + // Disable caching + proxy.ModifyResponse = func(r *http.Response) error { + r.Header.Set("cache-control", "no-cache") + return nil + } proxy.ServeHTTP(w, r) return } From 063df7415e0748e5059c5b0bd9dbd149b7ee8dff Mon Sep 17 00:00:00 2001 From: Mac Chaffee Date: Tue, 3 Dec 2024 18:44:07 -0500 Subject: [PATCH 4/4] Use Souin 1.7.5 --- caddy/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/caddy/Dockerfile b/caddy/Dockerfile index f59c1f1c..1e56eba8 100644 --- a/caddy/Dockerfile +++ b/caddy/Dockerfile @@ -9,7 +9,7 @@ ENV GOOS=${TARGETOS} GOARCH=${TARGETARCH} RUN xcaddy build \ --with github.com/caddy-dns/cloudflare \ - --with github.com/darkweak/souin/plugins/caddy@v1.7.4 \ + --with github.com/darkweak/souin/plugins/caddy@v1.7.5 \ --with github.com/darkweak/storages/otter/caddy FROM caddy:alpine