| index 630a183c8..d1c08cb16 100644 |
| --- a/internal/edge/cache.go |
| +++ b/internal/edge/cache.go |
| @@ -63,6 +63,7 @@ type Cache struct { |
| entries map[cacheFullKey]*list.Element |
| byURL map[cacheURLKey]map[cacheFullKey]*list.Element // urlKey -> its Vary variants |
| byPath map[cachePathKey]map[cacheFullKey]*list.Element // pathKey -> every entry under that path |
| + byHost map[string]map[cacheFullKey]*list.Element // host -> every entry for that host |
| } |
| |
| type cacheURLKey struct { |
| @@ -136,6 +137,7 @@ func NewCache(maxEntries, maxBodyBytes int) *Cache { |
| entries: make(map[cacheFullKey]*list.Element), |
| byURL: make(map[cacheURLKey]map[cacheFullKey]*list.Element), |
| byPath: make(map[cachePathKey]map[cacheFullKey]*list.Element), |
| + byHost: make(map[string]map[cacheFullKey]*list.Element), |
| } |
| } |
| |
| @@ -335,6 +337,10 @@ func (c *Cache) Put(key CacheKey, vary string, resp *CachedResponse) { |
| c.byPath[pk] = make(map[cacheFullKey]*list.Element) |
| } |
| c.byPath[pk][fk] = el |
| + if c.byHost[uk.Host] == nil { |
| + c.byHost[uk.Host] = make(map[cacheFullKey]*list.Element) |
| + } |
| + c.byHost[uk.Host][fk] = el |
| |
| c.capGroupLocked(c.byURL[uk], maxVaryVariantsPerURL, el) |
| c.capGroupLocked(c.byPath[pk], maxEntriesPerPath, el) |
| @@ -389,6 +395,12 @@ func (c *Cache) removeLocked(el *list.Element) { |
| delete(c.byPath, pk) |
| } |
| } |
| + if group := c.byHost[uk.Host]; group != nil { |
| + delete(group, e.key) |
| + if len(group) == 0 { |
| + delete(c.byHost, uk.Host) |
| + } |
| + } |
| } |
| |
| // PurgeURL drops every cached variant (every VaryValue) of (host, path, rawQuery) |
| @@ -438,23 +450,25 @@ func (c *Cache) PurgePath(host, path string) int { |
| } |
| |
| // PurgeHost drops every cached entry for host — all paths, all query strings, |
| -// all Vary variants — and returns how many it removed. This is what a deploy |
| -// calls: a fresh process for a host means new code and potentially new |
| -// content, so everything cached from the old one is suspect. A whole-cache |
| -// scan is fine here; it happens once per spawn, not per request. |
| +// all Vary variants — and returns how many it removed. A deploy calls this: a |
| +// fresh process for a host means new code and potentially new content, so |
| +// everything cached from the old one is suspect. |
| +// |
| +// It is no longer only a deploy-time cost, though, and so no longer a |
| +// whole-cache scan: a Carlos-Purge header that names more targets than the |
| +// 64-target/8KiB caps allow degrades to PurgeHost (see purge_header.go), |
| +// which puts it on the request path. It rides the byHost index instead — the |
| +// same discipline byURL and byPath already keep in Put/removeLocked — so a |
| +// misbehaving or hostile app declaring an oversized purge cannot turn every |
| +// response into a scan of the entire cache. |
| func (c *Cache) PurgeHost(host string) int { |
| c.mu.Lock() |
| defer c.mu.Unlock() |
| - var doomed []*list.Element |
| - for key, el := range c.entries { |
| - if key.Host == host { |
| - doomed = append(doomed, el) |
| - } |
| - } |
| - for _, el := range doomed { |
| + els := elementsOf(c.byHost[host]) |
| + for _, el := range els { |
| c.removeLocked(el) |
| } |
| - return len(doomed) |
| + return len(els) |
| } |
| |
| // elementsOf snapshots a group's elements so the caller can remove them while |