mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-20 13:30:46 +02:00
* cache resolved chunk manifests for Mount * Address PR review: per-mount cache, singleflight, reuse ResolveOneChunkManifest - Own the manifest cache per WFS mount instead of a process-global variable, so manifests from one filer backend are never served to another (Devin/CodeRabbit major bug). - Coalesce concurrent cold misses via singleflight so only one fetch runs during a cold burst (Greptile P2). - Copy cached data after releasing the mutex so a large copy does not block concurrent hits, inserts, and evictions (CodeRabbit nitpick). - Reuse the existing ResolveOneChunkManifest function name instead of introducing a new resolveOneChunkManifest wrapper. - Validate (unmarshal) manifest bytes before caching so malformed manifests do not poison the cache. - Add TestChunkGroupManifestResolutionCoalescesColdMisses covering the singleflight cold-miss path. * Address round 2 review: coalesced-miss cancellation, test overlap - Use singleflight.DoChan in fetchOrLoad and select on ctx.Done() so a caller whose context is canceled while waiting for an in-flight fetch returns ctx.Err() promptly instead of blocking for the leader's result (Devin BUG). - Add TestResolveOneChunkManifestCanceledWaiterReturnsDuringCoalescedMiss covering the canceled-waiter path. - Delay the cold-miss fixture response so the leader's fetch is still in flight when concurrent opens join the singleflight, making the one-fetch assertions reliable (CodeRabbit Minor). * Address review: keep ResolveOneChunkManifest four-argument Restore the exported ResolveOneChunkManifest to its original four-argument signature so external callers keep compiling. Move the cache-aware resolution into an unexported resolveOneChunkManifest helper that accepts the per-mount ChunkManifestCache. The exported function delegates to the helper with a nil cache, preserving the historical uncached behavior for every non-Mount caller. The Mount path (ChunkGroup.SetChunks) now calls the unexported helper with the mount-owned cache. Tests and benchmarks that exercise the cache path call the unexported helper directly. --------- Co-authored-by: Chris Lu <chris.lu@gmail.com> Co-authored-by: Chris Lu <chrislusf@users.noreply.github.com>
147 lines
3.7 KiB
Go
147 lines
3.7 KiB
Go
package filer
|
|
|
|
import (
|
|
"container/list"
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
|
|
"golang.org/x/sync/singleflight"
|
|
)
|
|
|
|
const (
|
|
MaxMountChunkManifestCacheEntries = 256
|
|
MaxMountChunkManifestCacheBytes = 64 << 20
|
|
)
|
|
|
|
type chunkManifestCacheKey struct {
|
|
fileID string
|
|
cipherKey string
|
|
isCompressed bool
|
|
}
|
|
|
|
func (k chunkManifestCacheKey) flightKey() string {
|
|
return fmt.Sprintf("%s\x00%s\x00%t", k.fileID, k.cipherKey, k.isCompressed)
|
|
}
|
|
|
|
type chunkManifestCacheEntry struct {
|
|
key chunkManifestCacheKey
|
|
data []byte
|
|
}
|
|
|
|
// ChunkManifestCache is a bounded, thread-safe LRU cache for chunk manifest
|
|
// bytes. Each mount (WFS) owns its own instance so manifests fetched through
|
|
// one filer backend are never served to another. Concurrent cold misses for
|
|
// the same key are coalesced via singleflight so only one fetch runs.
|
|
type ChunkManifestCache struct {
|
|
mu sync.Mutex
|
|
maxEntries int
|
|
maxBytes int64
|
|
bytes int64
|
|
entries map[chunkManifestCacheKey]*list.Element
|
|
lru *list.List
|
|
flight singleflight.Group
|
|
}
|
|
|
|
// NewChunkManifestCache creates a bounded LRU cache for chunk manifest bytes.
|
|
func NewChunkManifestCache(maxEntries int, maxBytes int64) *ChunkManifestCache {
|
|
return &ChunkManifestCache{
|
|
maxEntries: maxEntries,
|
|
maxBytes: maxBytes,
|
|
entries: make(map[chunkManifestCacheKey]*list.Element),
|
|
lru: list.New(),
|
|
}
|
|
}
|
|
|
|
func (c *ChunkManifestCache) get(key chunkManifestCacheKey) ([]byte, bool) {
|
|
c.mu.Lock()
|
|
element, found := c.entries[key]
|
|
if !found {
|
|
c.mu.Unlock()
|
|
return nil, false
|
|
}
|
|
c.lru.MoveToFront(element)
|
|
// entry.data is immutable after insertion; copy outside the lock so a
|
|
// large copy does not block concurrent hits, inserts, and evictions.
|
|
data := element.Value.(*chunkManifestCacheEntry).data
|
|
c.mu.Unlock()
|
|
return append([]byte(nil), data...), true
|
|
}
|
|
|
|
func (c *ChunkManifestCache) put(key chunkManifestCacheKey, data []byte) {
|
|
if c.maxEntries <= 0 || int64(len(data)) > c.maxBytes {
|
|
return
|
|
}
|
|
|
|
entry := &chunkManifestCacheEntry{
|
|
key: key,
|
|
data: append([]byte(nil), data...),
|
|
}
|
|
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
if old, found := c.entries[key]; found {
|
|
c.removeElement(old)
|
|
}
|
|
|
|
element := c.lru.PushFront(entry)
|
|
c.entries[key] = element
|
|
c.bytes += int64(len(entry.data))
|
|
|
|
for len(c.entries) > c.maxEntries || c.bytes > c.maxBytes {
|
|
c.removeElement(c.lru.Back())
|
|
}
|
|
}
|
|
|
|
// fetchOrLoad returns cached manifest bytes for key, or invokes fetch and
|
|
// caches the result. Concurrent calls for the same key are coalesced via
|
|
// singleflight so only one fetch runs during a cold burst. A caller whose
|
|
// context is canceled while waiting for the in-flight fetch returns
|
|
// ctx.Err() immediately rather than blocking for the leader's result.
|
|
func (c *ChunkManifestCache) fetchOrLoad(ctx context.Context, key chunkManifestCacheKey, fetch func() ([]byte, error)) ([]byte, error) {
|
|
if data, ok := c.get(key); ok {
|
|
return data, nil
|
|
}
|
|
ch := c.flight.DoChan(key.flightKey(), func() (interface{}, error) {
|
|
// Re-check under the flight: another flight may have just populated
|
|
// the cache for this key.
|
|
if data, ok := c.get(key); ok {
|
|
return data, nil
|
|
}
|
|
data, err := fetch()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
c.put(key, data)
|
|
return data, nil
|
|
})
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
case res := <-ch:
|
|
if res.Err != nil {
|
|
return nil, res.Err
|
|
}
|
|
return res.Val.([]byte), nil
|
|
}
|
|
}
|
|
|
|
func (c *ChunkManifestCache) clear() {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.entries = make(map[chunkManifestCacheKey]*list.Element)
|
|
c.lru.Init()
|
|
c.bytes = 0
|
|
}
|
|
|
|
func (c *ChunkManifestCache) removeElement(element *list.Element) {
|
|
if element == nil {
|
|
return
|
|
}
|
|
c.lru.Remove(element)
|
|
entry := element.Value.(*chunkManifestCacheEntry)
|
|
delete(c.entries, entry.key)
|
|
c.bytes -= int64(len(entry.data))
|
|
}
|