diff --git a/app/cachedrepo/cached_site_repository.go b/app/cachedrepo/cached_site_repository.go new file mode 100644 index 0000000..60a2e87 --- /dev/null +++ b/app/cachedrepo/cached_site_repository.go @@ -0,0 +1,290 @@ +package cachedrepo + +import ( + "quay/app/models" + "quay/app/repository" + "sync" +) + +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 +} + +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), + } +} + +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() + 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() + 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 + } + c.mu.RUnlock() + + sites, err := c.inner.ListSites() + if err != nil { + return nil, err + } + + c.mu.Lock() + c.siteList = sites + c.siteListValid = true + for i := range sites { + s := sites[i] + c.sites[s.ID] = &s + } + c.mu.Unlock() + return sites, nil +} + +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() + return nil +} + +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() + return 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() + 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() + 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() + return fr, nil +} + +func (c *CachedSiteRepository) CreateForwardRule(siteID string, fr *models.ForwardRule) error { + 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() + return nil +} + +func (c *CachedSiteRepository) UpdateForwardRule(fr *models.ForwardRule) error { + if err := c.inner.UpdateForwardRule(fr); err != nil { + return err + } + c.mu.Lock() + c.forwardRules[fr.ID] = fr + c.mu.Unlock() + return nil +} + +func (c *CachedSiteRepository) DeleteForwardRule(id string) error { + if err := c.inner.DeleteForwardRule(id); err != nil { + return err + } + c.mu.Lock() + delete(c.forwardRules, id) + c.mu.Unlock() + 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() + 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() + return ch, nil +} + +func (c *CachedSiteRepository) CreateCustomHeaders(siteID string, ch *models.CustomHeaders) error { + 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() + return nil +} + +func (c *CachedSiteRepository) UpdateCustomHeaders(ch *models.CustomHeaders) error { + if err := c.inner.UpdateCustomHeaders(ch); err != nil { + return err + } + c.mu.Lock() + c.customHeaders[ch.ID] = ch + c.mu.Unlock() + return nil +} + +func (c *CachedSiteRepository) DeleteCustomHeaders(id string) error { + if err := c.inner.DeleteCustomHeaders(id); err != nil { + return err + } + c.mu.Lock() + delete(c.customHeaders, id) + c.mu.Unlock() + 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() + 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() + return h, nil +} + +func (c *CachedSiteRepository) CreateHeader(customHeaderID string, h *models.Header) error { + if err := c.inner.CreateHeader(customHeaderID, h); err != nil { + return err + } + c.mu.Lock() + c.headers[h.ID] = h + delete(c.customHeaders, customHeaderID) + c.mu.Unlock() + return nil +} + +func (c *CachedSiteRepository) UpdateHeader(h *models.Header) error { + if err := c.inner.UpdateHeader(h); err != nil { + return err + } + c.mu.Lock() + c.headers[h.ID] = h + c.mu.Unlock() + return nil +} + +func (c *CachedSiteRepository) DeleteHeader(id string) error { + if err := c.inner.DeleteHeader(id); err != nil { + return err + } + c.mu.Lock() + delete(c.headers, id) + c.mu.Unlock() + return nil +} + +// 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) +} + +// 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 +} diff --git a/app/routes/routes.go b/app/routes/routes.go index 8d40958..fb0f4cb 100644 --- a/app/routes/routes.go +++ b/app/routes/routes.go @@ -4,6 +4,7 @@ import ( "database/sql" "log" "path/filepath" + "quay/app/cachedrepo" "quay/app/handlers" "quay/internal/config" "quay/internal/database" @@ -13,7 +14,7 @@ import ( ) func Register(app *fiber.App, cfg *config.Config, envCfg *envconfig.EnvConfig, db *sql.DB) { - siteRepository := database.NewSQLiteSiteRepository(db) + siteRepository := cachedrepo.NewCachedSiteRepository(database.NewSQLiteSiteRepository(db)) updateSiteHandler := handlers.NewUpdateSiteHandler(cfg, envCfg) siteHandler := handlers.NewSiteHandler(siteRepository)