Skip to content

Commit

Permalink
feat(pgs): http caching with souin (#159)
Browse files Browse the repository at this point in the history
  • Loading branch information
mac-chaffee authored Dec 4, 2024
1 parent 4429411 commit e01669e
Show file tree
Hide file tree
Showing 7 changed files with 111 additions and 10 deletions.
4 changes: 3 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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=
Expand Down
21 changes: 21 additions & 0 deletions caddy/Caddyfile.pgs
Original file line number Diff line number Diff line change
Expand Up @@ -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} {
Expand All @@ -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 {
Expand Down
4 changes: 3 additions & 1 deletion caddy/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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/[email protected] \
--with github.com/darkweak/storages/otter/caddy

FROM caddy:alpine

Expand Down
4 changes: 4 additions & 0 deletions pgs/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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", "")
Expand All @@ -27,6 +29,8 @@ func NewConfigSite() *shared.ConfigSite {
Protocol: protocol,
DbURL: dbURL,
StorageDir: storageDir,
CacheUser: pgsCacheUser,
CachePassword: pgsCachePass,
MinioURL: minioURL,
MinioUser: minioUser,
MinioPass: minioPass,
Expand Down
78 changes: 70 additions & 8 deletions pgs/uploader.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import (
"io"
"io/fs"
"log/slog"
"net/http"
"os"
"path"
"path/filepath"
"slices"
"strings"
"sync"
"time"

"github.com/charmbracelet/ssh"
Expand Down Expand Up @@ -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,
}
}

Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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: <subdomain>" 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
}
8 changes: 8 additions & 0 deletions pgs/web_asset_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -216,6 +221,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(
Expand Down
2 changes: 2 additions & 0 deletions shared/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ type ConfigSite struct {
Protocol string
DbURL string
StorageDir string
CacheUser string
CachePassword string
MinioURL string
MinioUser string
MinioPass string
Expand Down

0 comments on commit e01669e

Please sign in to comment.