Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement caching using Souin (take 2) #159

Merged
merged 4 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading