dashboard / erock/pico / feat(pgs): lru cache for object info and special files #59 rss

accepted · opened on 2025-04-06T03:01:44Z by erock
Help
checkout latest patchset:
ssh pr.pico.sh print pr-59 | git am -3
checkout any patchset in a patch request:
ssh pr.pico.sh print ps-X | git am -3
add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh pr add 59
add review to patch request:
git format-patch main --stdout | ssh pr.pico.sh pr add --review 59
accept PR:
ssh pr.pico.sh pr accept 59
close PR:
ssh pr.pico.sh pr close 59

Logs

erock created pr with ps-119 on 2025-04-06T03:01:44Z
erock added ps-120 on 2025-04-06T03:03:49Z
erock added ps-122 on 2025-04-06T19:08:31Z
erock added ps-123 on 2025-04-06T19:41:38Z
erock changed status on 2025-04-06T22:13:51Z {"status":"accepted"}

Patchsets

ps-119 by erock on 2025-04-06T03:01:44Z
Range Diff ↕ rd-120
2: caace51 ! 1: 26daea4 feat(pgs): lru cache for object info and special files
1: 2cf56f0 ! 2: b004b64 chore(pgs): use http cache clear event to rm lru cache for special files
ps-120 by erock on 2025-04-06T03:03:49Z
Range Diff ↕ rd-122
1: 26daea4 = 1: 26daea4 feat(pgs): lru cache for object info and special files
2: b004b64 = 2: b004b64 chore(pgs): use http cache clear event to rm lru cache for special files
-: ------- > 3: 59f5618 refactor(pgs): store lru cache on web router
ps-122 by erock on 2025-04-06T19:08:31Z
Range Diff ↕ rd-123
1: 26daea4 = 1: 26daea4 feat(pgs): lru cache for object info and special files
2: b004b64 = 2: b004b64 chore(pgs): use http cache clear event to rm lru cache for special files
3: 59f5618 = 3: 59f5618 refactor(pgs): store lru cache on web router
-: ------- > 4: ee12290 refactor(pgs): update minio lru and remove object info cache
ps-123 by erock on 2025-04-06T19:41:38Z

Range-diff rd-120

title
feat(pgs): lru cache for object info and special files
description
Patch changed
old #2
caace51
new #1
26daea4
title
chore(pgs): use http cache clear event to rm lru cache for special files
description
Patch changed
old #1
2cf56f0
new #2
b004b64
Back to top
2: caace51 ! 1: 26daea4 feat(pgs): lru cache for object info and special files
pkg/apps/pgs/web.go pkg/apps/pgs/web.go
 		routes:  routes,
 	}
 
-	go routes.cacheMgmt(ctx, httpCache)
+	go routes.cacheMgmt(ctx, httpCache, cfg.CacheClearingQueue)
 
 	portStr := fmt.Sprintf(":%s", cfg.WebPort)
 	cfg.Logger.Info(
 	w.WriteHeader(http.StatusNotFound)
 }
 
-func (web *WebRouter) cacheMgmt(ctx context.Context, httpCache *middleware.SouinBaseHandler) {
+func (web *WebRouter) cacheMgmt(ctx context.Context, httpCache *middleware.SouinBaseHandler, notify chan string) {
 	storer := httpCache.Storers[0]
 	drain := createSubCacheDrain(ctx, web.Cfg.Logger)
 
 		for scanner.Scan() {
 			surrogateKey := strings.TrimSpace(scanner.Text())
 			web.Cfg.Logger.Info("received cache-drain item", "surrogateKey", surrogateKey)
+			notify <- surrogateKey
 
 			if surrogateKey == "*" {
 				storer.DeleteMany(".+")
 		}
 	}
 
+	go func() {
+		for key := range web.Cfg.CacheClearingQueue {
+			rKey := filepath.Join(key, "_redirects")
+			redirectsCache.Remove(rKey)
+			hKey := filepath.Join(key, "_headers")
+			headersCache.Remove(hKey)
+		}
+	}()
+
 	asset := &ApiAssetHandler{
 		WebRouter: web,
 		Logger:    logger,
pkg/apps/pgs/web.go pkg/apps/pgs/web.go
 	"net/http"
 	"net/url"
 	"os"
+	"path/filepath"
 	"regexp"
 	"strings"
 	"time"
 		"host", r.Host,
 	)
 
-	if fname == "_headers" || fname == "_redirects" || fname == "_pgs_ignore" {
+	if isSpecialFile(fname) {
 		logger.Info("special file names are not allowed to be served over http")
 		http.Error(w, "404 not found", http.StatusNotFound)
 		return
pkg/apps/pgs/web_asset_handler.go pkg/apps/pgs/web_asset_handler.go
 	logger := h.Logger
 	var redirects []*RedirectRule
 
-	redirectsCacheKey := filepath.Join(h.Bucket.Name, h.ProjectDir, "_redirects")
+	redirectsCacheKey := filepath.Join(getSurrogateKey(h.UserID, h.ProjectDir), "_redirects")
 	if cachedRedirects, found := redirectsCache.Get(redirectsCacheKey); found {
 		redirects = cachedRedirects
 	} else {
 
 	var headers []*HeaderRule
 
-	headersCacheKey := filepath.Join(h.Bucket.Name, h.ProjectDir, "_headers")
+	headersCacheKey := filepath.Join(getSurrogateKey(h.UserID, h.ProjectDir), "_headers")
 	if cachedHeaders, found := headersCache.Get(headersCacheKey); found {
 		headers = cachedHeaders
 	} else {
pkg/apps/pgs/web_asset_handler.go pkg/apps/pgs/web_asset_handler.go
 	"net/http/httputil"
 	_ "net/http/pprof"
 
+	"github.com/hashicorp/golang-lru/v2/expirable"
+	"github.com/picosh/pico/pkg/cache"
 	sst "github.com/picosh/pico/pkg/pobj/storage"
 	"github.com/picosh/pico/pkg/shared/storage"
 )
 
+var (
+	redirectsCache = expirable.NewLRU[string, []*RedirectRule](2048, nil, cache.CacheTimeout)
+	headersCache   = expirable.NewLRU[string, []*HeaderRule](2048, nil, cache.CacheTimeout)
+)
+
 type ApiAssetHandler struct {
 	*WebRouter
 	Logger *slog.Logger
 func (h *ApiAssetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	logger := h.Logger
 	var redirects []*RedirectRule
-	redirectFp, redirectInfo, err := h.Cfg.Storage.GetObject(h.Bucket, filepath.Join(h.ProjectDir, "_redirects"))
-	if err == nil {
-		defer redirectFp.Close()
-		if redirectInfo != nil && redirectInfo.Size > h.Cfg.MaxSpecialFileSize {
-			errMsg := fmt.Sprintf("_redirects file is too large (%d > %d)", redirectInfo.Size, h.Cfg.MaxSpecialFileSize)
-			logger.Error(errMsg)
-			http.Error(w, errMsg, http.StatusInternalServerError)
-			return
-		}
-		buf := new(strings.Builder)
-		lr := io.LimitReader(redirectFp, h.Cfg.MaxSpecialFileSize)
-		_, err := io.Copy(buf, lr)
-		if err != nil {
-			logger.Error("io copy", "err", err.Error())
-			http.Error(w, "cannot read _redirects file", http.StatusInternalServerError)
-			return
-		}
 
-		redirects, err = parseRedirectText(buf.String())
-		if err != nil {
-			logger.Error("could not parse redirect text", "err", err.Error())
+	redirectsCacheKey := filepath.Join(h.Bucket.Name, h.ProjectDir, "_redirects")
+	if cachedRedirects, found := redirectsCache.Get(redirectsCacheKey); found {
+		redirects = cachedRedirects
+	} else {
+		redirectFp, redirectInfo, err := h.Cfg.Storage.GetObject(h.Bucket, filepath.Join(h.ProjectDir, "_redirects"))
+		if err == nil {
+			defer redirectFp.Close()
+			if redirectInfo != nil && redirectInfo.Size > h.Cfg.MaxSpecialFileSize {
+				errMsg := fmt.Sprintf("_redirects file is too large (%d > %d)", redirectInfo.Size, h.Cfg.MaxSpecialFileSize)
+				logger.Error(errMsg)
+				http.Error(w, errMsg, http.StatusInternalServerError)
+				return
+			}
+			buf := new(strings.Builder)
+			lr := io.LimitReader(redirectFp, h.Cfg.MaxSpecialFileSize)
+			_, err := io.Copy(buf, lr)
+			if err != nil {
+				logger.Error("io copy", "err", err.Error())
+				http.Error(w, "cannot read _redirects file", http.StatusInternalServerError)
+				return
+			}
+
+			redirects, err = parseRedirectText(buf.String())
+			if err != nil {
+				logger.Error("could not parse redirect text", "err", err.Error())
+			}
 		}
+
+		redirectsCache.Add(redirectsCacheKey, redirects)
 	}
 
 	routes := calcRoutes(h.ProjectDir, h.Filepath, redirects)
 	defer contents.Close()
 
 	var headers []*HeaderRule
-	headersFp, headersInfo, err := h.Cfg.Storage.GetObject(h.Bucket, filepath.Join(h.ProjectDir, "_headers"))
-	if err == nil {
-		defer headersFp.Close()
-		if headersInfo != nil && headersInfo.Size > h.Cfg.MaxSpecialFileSize {
-			errMsg := fmt.Sprintf("_headers file is too large (%d > %d)", headersInfo.Size, h.Cfg.MaxSpecialFileSize)
-			logger.Error(errMsg)
-			http.Error(w, errMsg, http.StatusInternalServerError)
-			return
-		}
-		buf := new(strings.Builder)
-		lr := io.LimitReader(headersFp, h.Cfg.MaxSpecialFileSize)
-		_, err := io.Copy(buf, lr)
-		if err != nil {
-			logger.Error("io copy", "err", err.Error())
-			http.Error(w, "cannot read _headers file", http.StatusInternalServerError)
-			return
-		}
 
-		headers, err = parseHeaderText(buf.String())
-		if err != nil {
-			logger.Error("could not parse header text", "err", err.Error())
+	headersCacheKey := filepath.Join(h.Bucket.Name, h.ProjectDir, "_headers")
+	if cachedHeaders, found := headersCache.Get(headersCacheKey); found {
+		headers = cachedHeaders
+	} else {
+		headersFp, headersInfo, err := h.Cfg.Storage.GetObject(h.Bucket, filepath.Join(h.ProjectDir, "_headers"))
+		if err == nil {
+			defer headersFp.Close()
+			if headersInfo != nil && headersInfo.Size > h.Cfg.MaxSpecialFileSize {
+				errMsg := fmt.Sprintf("_headers file is too large (%d > %d)", headersInfo.Size, h.Cfg.MaxSpecialFileSize)
+				logger.Error(errMsg)
+				http.Error(w, errMsg, http.StatusInternalServerError)
+				return
+			}
+			buf := new(strings.Builder)
+			lr := io.LimitReader(headersFp, h.Cfg.MaxSpecialFileSize)
+			_, err := io.Copy(buf, lr)
+			if err != nil {
+				logger.Error("io copy", "err", err.Error())
+				http.Error(w, "cannot read _headers file", http.StatusInternalServerError)
+				return
+			}
+
+			headers, err = parseHeaderText(buf.String())
+			if err != nil {
+				logger.Error("could not parse header text", "err", err.Error())
+			}
 		}
+
+		headersCache.Add(headersCacheKey, headers)
 	}
 
 	userHeaders := []*HeaderLine{}
 		return
 	}
 	w.WriteHeader(status)
-	_, err = io.Copy(w, contents)
+	_, err := io.Copy(w, contents)
 
 	if err != nil {
 		logger.Error("io copy", "err", err.Error())