From 6b6565caeef3e85ea7146f1d82c04fc328785bde Mon Sep 17 00:00:00 2001 From: KartoffelChipss Date: Mon, 4 May 2026 18:30:44 +0200 Subject: [PATCH] Use otter for site cache --- .../{cached_site_repository.go => site.go} | 241 +++++++----------- backend/app/routes/routes.go | 3 +- backend/go.mod | 4 + backend/go.sum | 2 + 4 files changed, 94 insertions(+), 156 deletions(-) rename backend/app/cachedrepo/{cached_site_repository.go => site.go} (52%) diff --git a/backend/app/cachedrepo/cached_site_repository.go b/backend/app/cachedrepo/site.go similarity index 52% rename from backend/app/cachedrepo/cached_site_repository.go rename to backend/app/cachedrepo/site.go index 9c4a642..e8a77f3 100644 --- a/backend/app/cachedrepo/cached_site_repository.go +++ b/backend/app/cachedrepo/site.go @@ -3,29 +3,39 @@ package cachedrepo import ( "quay/app/models" "quay/app/repository" - "sync" + + "github.com/maypok86/otter/v2" ) type CachedSiteRepository struct { - inner repository.SiteRepository - mu sync.RWMutex - - sites map[string]*models.Site // id -> site - siteList []models.Site // cached ListSites result - siteListValid bool - - forwardRules map[string]*models.ForwardRule // id -> rule - customHeaders map[string]*models.CustomHeaders // id -> custom headers - headers map[string]*models.Header // id -> header + inner repository.SiteRepository + sites *otter.Cache[string, *models.Site] + list *otter.Cache[string, []models.Site] + forwardRules *otter.Cache[string, *models.ForwardRule] + customHeaders *otter.Cache[string, *models.CustomHeaders] + headers *otter.Cache[string, *models.Header] } +const siteListKey = "__list__" + func NewCachedSiteRepository(inner repository.SiteRepository) *CachedSiteRepository { return &CachedSiteRepository{ - inner: inner, - sites: make(map[string]*models.Site), - forwardRules: make(map[string]*models.ForwardRule), - customHeaders: make(map[string]*models.CustomHeaders), - headers: make(map[string]*models.Header), + inner: inner, + sites: otter.Must(&otter.Options[string, *models.Site]{ + MaximumSize: 1_000, + }), + list: otter.Must(&otter.Options[string, []models.Site]{ + MaximumSize: 1, + }), + forwardRules: otter.Must(&otter.Options[string, *models.ForwardRule]{ + MaximumSize: 10_000, + }), + customHeaders: otter.Must(&otter.Options[string, *models.CustomHeaders]{ + MaximumSize: 10_000, + }), + headers: otter.Must(&otter.Options[string, *models.Header]{ + MaximumSize: 10_000, + }), } } @@ -34,66 +44,41 @@ var _ repository.SiteRepository = (*CachedSiteRepository)(nil) // Sites func (c *CachedSiteRepository) GetSite(id string) (*models.Site, error) { - c.mu.RLock() - if s, ok := c.sites[id]; ok { - c.mu.RUnlock() + if s, ok := c.sites.GetIfPresent(id); ok { return s, nil } - c.mu.RUnlock() - s, err := c.inner.GetSite(id) if err != nil { return nil, err } - - c.mu.Lock() - c.sites[id] = s - c.mu.Unlock() + c.sites.Set(id, s) return s, nil } func (c *CachedSiteRepository) GetSiteByDomain(domain string) (*models.Site, error) { - c.mu.RLock() - if s, ok := c.sites[domain]; ok { - c.mu.RUnlock() + if s, ok := c.sites.GetIfPresent(domain); ok { return s, nil } - c.mu.RUnlock() - s, err := c.inner.GetSiteByDomain(domain) if err != nil { return nil, err } - - c.mu.Lock() - c.sites[domain] = s - c.mu.Unlock() + c.sites.Set(domain, s) return s, nil } func (c *CachedSiteRepository) ListSites() ([]models.Site, error) { - c.mu.RLock() - if c.siteListValid { - cp := make([]models.Site, len(c.siteList)) - copy(cp, c.siteList) - c.mu.RUnlock() - return cp, nil + if sites, ok := c.list.GetIfPresent(siteListKey); ok { + return sites, nil } - c.mu.RUnlock() - sites, err := c.inner.ListSites() if err != nil { return nil, err } - - c.mu.Lock() - c.siteList = sites - c.siteListValid = true + c.list.Set(siteListKey, sites) for i := range sites { - s := sites[i] - c.sites[s.ID] = &s + c.sites.Set(sites[i].ID, &sites[i]) } - c.mu.Unlock() return sites, nil } @@ -101,10 +86,8 @@ func (c *CachedSiteRepository) CreateSite(s *models.Site) error { if err := c.inner.CreateSite(s); err != nil { return err } - c.mu.Lock() - c.sites[s.ID] = s - c.siteListValid = false - c.mu.Unlock() + c.sites.Set(s.ID, s) + c.list.Invalidate(siteListKey) return nil } @@ -112,54 +95,41 @@ func (c *CachedSiteRepository) UpdateSite(s *models.Site) error { if err := c.inner.UpdateSite(s); err != nil { return err } - c.mu.Lock() - c.sites[s.ID] = s - c.siteListValid = false - c.mu.Unlock() + c.sites.Set(s.ID, s) + c.list.Invalidate(siteListKey) return nil } -func (c *CachedSiteRepository) ToggleEnabled(id string) (enabledReturn bool, err error) { - enabledReturn, err = c.inner.ToggleEnabled(id) +func (c *CachedSiteRepository) ToggleEnabled(id string) (bool, error) { + enabled, err := c.inner.ToggleEnabled(id) if err != nil { return false, err } - c.mu.Lock() - delete(c.sites, id) - c.siteListValid = false - c.mu.Unlock() - return enabledReturn, nil + c.sites.Invalidate(id) + c.list.Invalidate(siteListKey) + return enabled, nil } func (c *CachedSiteRepository) DeleteSite(id string) error { if err := c.inner.DeleteSite(id); err != nil { return err } - c.mu.Lock() - delete(c.sites, id) - c.siteListValid = false - c.mu.Unlock() + c.sites.Invalidate(id) + c.list.Invalidate(siteListKey) return nil } // Forward Rules func (c *CachedSiteRepository) GetForwardRule(id string) (*models.ForwardRule, error) { - c.mu.RLock() - if fr, ok := c.forwardRules[id]; ok { - c.mu.RUnlock() + if fr, ok := c.forwardRules.GetIfPresent(id); ok { return fr, nil } - c.mu.RUnlock() - fr, err := c.inner.GetForwardRule(id) if err != nil { return nil, err } - - c.mu.Lock() - c.forwardRules[id] = fr - c.mu.Unlock() + c.forwardRules.Set(id, fr) return fr, nil } @@ -167,11 +137,9 @@ func (c *CachedSiteRepository) CreateForwardRule(siteID string, fr *models.Forwa if err := c.inner.CreateForwardRule(siteID, fr); err != nil { return err } - c.mu.Lock() - c.forwardRules[fr.ID] = fr - delete(c.sites, siteID) // site's embedded rules are now stale - c.siteListValid = false - c.mu.Unlock() + c.forwardRules.Set(fr.ID, fr) + c.sites.Invalidate(siteID) + c.list.Invalidate(siteListKey) return nil } @@ -179,11 +147,9 @@ func (c *CachedSiteRepository) UpdateForwardRule(siteID string, fr *models.Forwa if err := c.inner.UpdateForwardRule(siteID, fr); err != nil { return err } - c.mu.Lock() - c.forwardRules[fr.ID] = fr - delete(c.sites, siteID) - c.siteListValid = false - c.mu.Unlock() + c.forwardRules.Set(fr.ID, fr) + c.sites.Invalidate(siteID) + c.list.Invalidate(siteListKey) return nil } @@ -191,32 +157,23 @@ func (c *CachedSiteRepository) DeleteForwardRule(siteID string, id string) error if err := c.inner.DeleteForwardRule(siteID, id); err != nil { return err } - c.mu.Lock() - delete(c.forwardRules, id) - delete(c.sites, siteID) - c.siteListValid = false - c.mu.Unlock() + c.forwardRules.Invalidate(id) + c.sites.Invalidate(siteID) + c.list.Invalidate(siteListKey) return nil } // Custom Headers func (c *CachedSiteRepository) GetCustomHeaders(id string) (*models.CustomHeaders, error) { - c.mu.RLock() - if ch, ok := c.customHeaders[id]; ok { - c.mu.RUnlock() + if ch, ok := c.customHeaders.GetIfPresent(id); ok { return ch, nil } - c.mu.RUnlock() - ch, err := c.inner.GetCustomHeaders(id) if err != nil { return nil, err } - - c.mu.Lock() - c.customHeaders[id] = ch - c.mu.Unlock() + c.customHeaders.Set(id, ch) return ch, nil } @@ -224,11 +181,9 @@ func (c *CachedSiteRepository) CreateCustomHeaders(siteID string, ch *models.Cus if err := c.inner.CreateCustomHeaders(siteID, ch); err != nil { return err } - c.mu.Lock() - c.customHeaders[ch.ID] = ch - delete(c.sites, siteID) - c.siteListValid = false - c.mu.Unlock() + c.customHeaders.Set(ch.ID, ch) + c.sites.Invalidate(siteID) + c.list.Invalidate(siteListKey) return nil } @@ -236,11 +191,9 @@ func (c *CachedSiteRepository) UpdateCustomHeaders(siteID string, ch *models.Cus if err := c.inner.UpdateCustomHeaders(siteID, ch); err != nil { return err } - c.mu.Lock() - c.customHeaders[ch.ID] = ch - delete(c.sites, siteID) - c.siteListValid = false - c.mu.Unlock() + c.customHeaders.Set(ch.ID, ch) + c.sites.Invalidate(siteID) + c.list.Invalidate(siteListKey) return nil } @@ -248,32 +201,23 @@ func (c *CachedSiteRepository) DeleteCustomHeaders(siteID string, id string) err if err := c.inner.DeleteCustomHeaders(siteID, id); err != nil { return err } - c.mu.Lock() - delete(c.customHeaders, id) - delete(c.sites, siteID) - c.siteListValid = false - c.mu.Unlock() + c.customHeaders.Invalidate(id) + c.sites.Invalidate(siteID) + c.list.Invalidate(siteListKey) return nil } // Headers func (c *CachedSiteRepository) GetHeader(id string) (*models.Header, error) { - c.mu.RLock() - if h, ok := c.headers[id]; ok { - c.mu.RUnlock() + if h, ok := c.headers.GetIfPresent(id); ok { return h, nil } - c.mu.RUnlock() - h, err := c.inner.GetHeader(id) if err != nil { return nil, err } - - c.mu.Lock() - c.headers[id] = h - c.mu.Unlock() + c.headers.Set(id, h) return h, nil } @@ -281,12 +225,10 @@ func (c *CachedSiteRepository) CreateHeader(siteID string, customHeaderID string if err := c.inner.CreateHeader(siteID, customHeaderID, h); err != nil { return err } - c.mu.Lock() - c.headers[h.ID] = h - delete(c.customHeaders, customHeaderID) - delete(c.sites, siteID) - c.siteListValid = false - c.mu.Unlock() + c.headers.Set(h.ID, h) + c.customHeaders.Invalidate(customHeaderID) + c.sites.Invalidate(siteID) + c.list.Invalidate(siteListKey) return nil } @@ -294,11 +236,9 @@ func (c *CachedSiteRepository) UpdateHeader(siteID string, h *models.Header) err if err := c.inner.UpdateHeader(siteID, h); err != nil { return err } - c.mu.Lock() - c.headers[h.ID] = h - delete(c.sites, siteID) - c.siteListValid = false - c.mu.Unlock() + c.headers.Set(h.ID, h) + c.sites.Invalidate(siteID) + c.list.Invalidate(siteListKey) return nil } @@ -306,30 +246,23 @@ func (c *CachedSiteRepository) DeleteHeader(siteID string, id string) error { if err := c.inner.DeleteHeader(siteID, id); err != nil { return err } - c.mu.Lock() - delete(c.headers, id) - delete(c.sites, siteID) - c.siteListValid = false - c.mu.Unlock() + c.headers.Invalidate(id) + c.sites.Invalidate(siteID) + c.list.Invalidate(siteListKey) return nil } -// Invalidate explicitly evicts all cached data +// Invalidate explicitly evicts all cached data. func (c *CachedSiteRepository) Invalidate() { - c.mu.Lock() - defer c.mu.Unlock() - c.sites = make(map[string]*models.Site) - c.siteList = nil - c.siteListValid = false - c.forwardRules = make(map[string]*models.ForwardRule) - c.customHeaders = make(map[string]*models.CustomHeaders) - c.headers = make(map[string]*models.Header) + c.sites.InvalidateAll() + c.list.InvalidateAll() + c.forwardRules.InvalidateAll() + c.customHeaders.InvalidateAll() + c.headers.InvalidateAll() } // InvalidateSite evicts a single site and marks the list stale. func (c *CachedSiteRepository) InvalidateSite(id string) { - c.mu.Lock() - defer c.mu.Unlock() - delete(c.sites, id) - c.siteListValid = false + c.sites.Invalidate(id) + c.list.Invalidate(siteListKey) } diff --git a/backend/app/routes/routes.go b/backend/app/routes/routes.go index 7504ecd..89f0320 100644 --- a/backend/app/routes/routes.go +++ b/backend/app/routes/routes.go @@ -4,7 +4,6 @@ import ( "database/sql" "log" "path/filepath" - "quay/app/cachedrepo" "quay/app/handlers" "quay/app/middleware" "quay/app/models" @@ -20,7 +19,7 @@ const BootstrapUserUsername = "admin" const BootstrapUserPassword = "admin" func Register(app *fiber.App, cfg *config.Config, envCfg *envconfig.EnvConfig, db *sql.DB) { - siteRepository := cachedrepo.NewCachedSiteRepository(database.NewSQLiteSiteRepository(db)) + siteRepository := database.NewSQLiteSiteRepository(db) deploymentRepository := database.NewSQLiteDeploymentRepository(db) userRepository := database.NewSQLiteUserRepository(db) gitServerRepository := database.NewSQLiteGitServerRepository(db) diff --git a/backend/go.mod b/backend/go.mod index 0b755c0..4f2378f 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -19,6 +19,7 @@ require ( github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/andybalholm/brotli v1.2.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.19.6 // indirect github.com/go-openapi/spec v0.20.4 // indirect @@ -30,7 +31,10 @@ require ( github.com/mailru/easyjson v0.7.6 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/maypok86/otter/v2 v2.3.0 // indirect github.com/philhofer/fwd v1.2.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/testify v1.11.1 // indirect github.com/tinylib/msgp v1.6.3 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.69.0 // indirect diff --git a/backend/go.sum b/backend/go.sum index 386262f..0da04f6 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -55,6 +55,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.38 h1:tDUzL85kMvOrvpCt8P64SbGgVFtJB11GPi2AdmITgb4= github.com/mattn/go-sqlite3 v1.14.38/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/maypok86/otter/v2 v2.3.0 h1:8H8AVVFUSzJwIegKwv1uF5aGitTY+AIrtktg7OcLs8w= +github.com/maypok86/otter/v2 v2.3.0/go.mod h1:XgIdlpmL6jYz882/CAx1E4C1ukfgDKSaw4mWq59+7l8= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=