hush/vendor/github.com/redis/go-redis/v9/local_cache.go
jx12n 4d9a26498e hush: one-time secret links the server cannot read
Paste a secret, get a link, send it. The first person to open it and press
Reveal sees the secret; the link dies at that moment. The recipient needs a
browser and nothing else — no account, no client, no installed tooling.

The server cannot read what it stores. AES-256-GCM happens in the browser and
the key lives in the URL fragment, which browsers never transmit, so hushd
holds ciphertext and no key material. That is a property of where the key sits
rather than a promise about our conduct, which is why there is deliberately no
endpoint accepting a plaintext secret and no server-side-encryption fallback:
two guarantees behind one URL would be worse than one honest guarantee.

Three decisions carry the design:

  * GET /s/{id} touches NO storage, not even to check existence. Slack, Teams,
    WhatsApp, iMessage and Outlook Safe Links all fetch a URL before a human
    sees it, so destroying on GET would destroy most secrets in transit and the
    recipient's "already used" would be indistinguishable from interception.
    Only POST /reveal consumes. Bot user-agent detection is an arms race;
    removing the side effect from GET is not. Pinned by
    TestGettingTheRevealPageNeverConsumesTheSecret.
  * Destruction is one Redis GETDEL, which is atomic. GET-then-DEL has a window
    where two simultaneous readers both win, and for a one-time secret that
    window is the product. The store contract demands atomicity and the same
    concurrency test runs against both implementations.
  * Missing, already-revealed, expired and evicted are ONE indistinguishable
    410. Separating them would confirm to a prober that a given link was real.

The secret id IS the capability, so secret.ID is a struct whose every
accidental path — %v, %s, String(), slog, json.Marshal — emits a redacted
handle or refuses, and the raw value needs an explicit Value(). The first
version tried to prevent leaks by implementing no String() at all; its own test
caught that Go's fmt prints unexported fields anyway, so forbidding the method
had removed the control rather than the leak.

Operationally: structured JSON on stdout in the fleet's wire format, which
Vector already collects with no annotation; six hush_* metrics on the chassis
registry with no id, IP or path in any label; five alert rules wired into
vmalert. The public Ingress enumerates /, /s/ and /api/ so /metrics, /healthz
and /readyz share the port but are unreachable from the internet — no
basic-auth middleware to maintain and get wrong.

Dependencies are vendored because go-chassis is private: the Woodpecker test
step and the in-cluster Kaniko build both run -mod=vendor with GOPROXY=off and
hold no git credential.

cmd/hush-mcp is a stdio MCP server doing the same client-side crypto locally,
so using hush from an agent preserves the same guarantee as using it from a
browser.
2026-09-03 00:08:38 -06:00

775 lines
22 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package redis
import (
"context"
"math"
"sync"
"sync/atomic"
"time"
)
// cacheEntryState tracks the lifecycle of a local cache entry.
type cacheEntryState uint8
const (
// cacheEntryInProgress marks a placeholder entry while a value is being fetched.
cacheEntryInProgress cacheEntryState = iota
// cacheEntryValid marks an entry that contains a value that can be returned.
cacheEntryValid
)
// cacheEntry represents a cached command reply and its Redis-key associations.
type cacheEntry struct {
cacheKey string
redisKeys []string
value []byte
state cacheEntryState
token uint64
sizeBytes int64
reservedAt time.Time
waitCh chan struct{}
waitClosed bool
// lastAccessNs is a recency token for LRU eviction: a global atomic counter
// bumped on every access, stored atomically so the read path can mark a
// touch under the shard's RLock without upgrading to a write lock.
lastAccessNs atomic.Int64
// validAt retains time.Now's monotonic component for the MaxStaleness
// backstop, so wall-clock corrections cannot extend an entry's lifetime.
// Written under Lock (Set/Fulfill), read under RLock (get).
validAt time.Time
// ownerConnID is the conn that fetched this entry (set by FulfillOwned; 0 =
// none). Default CLIENT TRACKING sends a key's invalidation only to that
// conn, so the entry must be evicted when it goes away (see EvictByConn).
ownerConnID uint64
}
// lruSequence is the global monotonic counter feeding lastAccessNs. It totally
// orders recency across all entries in all shards for approximate-LRU eviction.
var lruSequence atomic.Int64
// nextLRUToken returns the next strictly-greater LRU token.
func nextLRUToken() int64 {
return lruSequence.Add(1)
}
// CacheSizer calculates estimated memory usage in bytes for a cache entry.
//
// Experimental: this API may change in a minor release.
type CacheSizer func(cacheKey string, redisKeys []string, value []byte) int64
// CacheConfig configures a local cache instance.
//
// Experimental: this API may change in a minor release.
type CacheConfig struct {
// MaxEntries limits the number of entries. Zero or negative means unlimited.
MaxEntries int
// MaxMemoryBytes limits estimated memory usage in bytes. Zero or negative means unlimited.
//
// If both MaxEntries and MaxMemoryBytes are unlimited, MaxEntries defaults to
// defaultCacheMaxEntries so the cache cannot grow without bound. The cache is
// sharded 16 ways (above small thresholds) and each shard enforces its 1/16
// share, so an entry larger than MaxMemoryBytes/16 is never admitted —
// size it to at least 16× your largest reply.
MaxMemoryBytes int64
// Sizer estimates memory usage per entry. If nil, a built-in approximation is used.
//
// Sizer may be invoked concurrently from multiple goroutines and must be
// thread-safe. It must return quickly and must not call back into the
// cache (Get, Set, Delete*, Flush, etc.): some call sites hold an internal
// shard lock, so re-entry can deadlock.
Sizer CacheSizer
// StaleTimeout is the duration after which an IN_PROGRESS placeholder is
// considered stale and eligible for takeover by a new Reserve call.
// If zero, defaults to defaultStaleTimeout (5s).
StaleTimeout time.Duration
// DrainInterval is the background-drainer period (default 5ms; zero uses the
// default): how often idle pool conns are swept for buffered "invalidate"
// frames, roughly bounding cache-hit staleness. Values below 1ms are clamped
// to 1ms.
DrainInterval time.Duration
// MaxStaleness caps how long a cached entry is served after it became valid,
// regardless of invalidation. It is a correctness
// BACKSTOP for lost invalidations or connection-lifecycle gaps ("Window 2"), not
// the primary freshness mechanism. Keep it well above the invalidation round-trip
// (e.g. seconds); per-entry refetch overhead scales ~1/MaxStaleness.
//
// Default: 0 (disabled).
MaxStaleness time.Duration
}
// Cache is the thread-safe storage contract used by client-side caching.
//
// All methods may be called concurrently. Cache keys and Redis keys are opaque
// strings and must be preserved exactly. Removing a reservation must wake any
// Get calls waiting for it.
//
// Reserve must allow only one caller to fetch a missing key and return a token
// that is valid until FulfillOwned, Cancel, or an eviction removes that
// reservation. FulfillOwned and Cancel must modify only a reservation with the
// matching token. Get may wait for an in-progress reservation and must stop
// waiting when ctx is done.
//
// Experimental: this API may change in a minor release.
type Cache interface {
Get(ctx context.Context, cacheKey string) ([]byte, bool)
Reserve(cacheKey string, redisKeys []string) (token uint64, shouldFetch bool)
// FulfillOwned publishes a reserved value and records the connection that
// fetched it so the entry can be evicted if that connection loses tracking.
FulfillOwned(cacheKey string, token, ownerConnID uint64, value []byte) bool
Cancel(cacheKey string, token uint64) bool
DeleteByRedisKey(redisKey string) int
DeleteByCacheKey(cacheKey string) bool
// EvictByConn removes every entry fetched by connID.
EvictByConn(connID uint64) int
Flush() int
}
const (
defaultStaleTimeout = 5 * time.Second
defaultCacheShardCount = 16
// defaultCacheMaxEntries bounds the cache when the config leaves both
// MaxEntries and MaxMemoryBytes unlimited (matches the 10k-entry default
// other Redis clients use, e.g. redis-py).
defaultCacheMaxEntries = 10000
// shardingThresholdEntries / shardingThresholdBytes: caches with capacity
// below these thresholds fall back to a single shard so global LRU /
// memory-cap semantics behave exactly as a non-sharded cache would.
shardingThresholdEntries = 64
shardingThresholdBytes = 64 * 1024
)
// NewLocalCache creates a thread-safe local cache with approximate-LRU
// eviction. The cache is internally sharded by cache-key hash to reduce
// mutex contention under high concurrent access.
//
// Experimental: this API may change in a minor release.
func NewLocalCache(cfg CacheConfig) *LocalCache {
sizer := cfg.Sizer
if sizer == nil {
sizer = defaultCacheSizer
}
staleTimeout := cfg.StaleTimeout
if staleTimeout <= 0 {
staleTimeout = defaultStaleTimeout
}
maxEntries := cfg.MaxEntries
maxMemoryBytes := cfg.MaxMemoryBytes
// An unbounded cache can grow until the process OOMs; require at least
// one limit.
if maxEntries <= 0 && maxMemoryBytes <= 0 {
maxEntries = defaultCacheMaxEntries
}
shardCount := defaultCacheShardCount
if maxEntries > 0 && maxEntries < shardingThresholdEntries {
shardCount = 1
}
if maxMemoryBytes > 0 && maxMemoryBytes < int64(shardingThresholdBytes) {
shardCount = 1
}
c := &LocalCache{
shards: make([]cacheShard, shardCount),
shardCount: uint32(shardCount),
shardMask: uint32(shardCount - 1),
sizer: sizer,
}
for i := range c.shards {
s := &c.shards[i]
s.entries = make(map[string]*cacheEntry)
s.byRedisKey = make(map[string]map[string]struct{})
s.byConnID = make(map[uint64]map[string]struct{})
// Distribute capacity so the per-shard caps sum to exactly the
// configured limits; a ceil-per-shard split would let total residency
// exceed MaxEntries/MaxMemoryBytes.
if maxEntries > 0 {
s.maxEntries = maxEntries / shardCount
if i < maxEntries%shardCount {
s.maxEntries++
}
}
if maxMemoryBytes > 0 {
s.maxMemoryBytes = maxMemoryBytes / int64(shardCount)
if int64(i) < maxMemoryBytes%int64(shardCount) {
s.maxMemoryBytes++
}
}
s.maxStaleness = cfg.MaxStaleness
s.sizer = sizer
s.staleTimeout = staleTimeout
}
return c
}
// LocalCache is the built-in sharded approximate-LRU cache.
//
// Experimental: this API may change in a minor release.
type LocalCache struct {
shards []cacheShard
shardCount uint32
shardMask uint32
sizer CacheSizer
nextToken atomic.Uint64
hits atomic.Uint64
misses atomic.Uint64
}
var _ Cache = (*LocalCache)(nil)
// cacheShard holds the state for one shard of LocalCache. The mutex
// protects entries, byRedisKey, byConnID, and usedBytes.
type cacheShard struct {
mu sync.RWMutex
entries map[string]*cacheEntry
byRedisKey map[string]map[string]struct{}
// byConnID is the owning-conn reverse index (twin of byRedisKey): conn id ->
// its cache keys. Populated by FulfillOwned, cleaned in removeEntryLocked,
// consumed by EvictByConn.
byConnID map[uint64]map[string]struct{}
usedBytes int64
maxEntries int
maxMemoryBytes int64
maxStaleness time.Duration
sizer CacheSizer
staleTimeout time.Duration
}
// shardFor returns the shard responsible for cacheKey.
func (c *LocalCache) shardFor(cacheKey string) *cacheShard {
if c.shardCount == 1 {
return &c.shards[0]
}
return &c.shards[fnv1a32(cacheKey)&c.shardMask]
}
// fnv1a32 returns the FNV-1a 32-bit hash of s. Allocation-free.
func fnv1a32(s string) uint32 {
const (
offset uint32 = 2166136261
prime uint32 = 16777619
)
h := offset
for i := 0; i < len(s); i++ {
h ^= uint32(s[i])
h *= prime
}
return h
}
const defaultCacheEntryOverhead int64 = 96
func defaultCacheSizer(cacheKey string, redisKeys []string, value []byte) int64 {
size := defaultCacheEntryOverhead + int64(len(cacheKey)+len(value))
for _, key := range redisKeys {
size += int64(len(key)) + 16
}
if size < 0 {
return 0
}
return size
}
// Get returns a copy of a cached value, waiting for an in-progress fetch when
// necessary.
func (c *LocalCache) Get(ctx context.Context, cacheKey string) ([]byte, bool) {
if ctx == nil {
ctx = context.Background()
}
value, ok := c.shardFor(cacheKey).get(ctx, cacheKey)
if ok {
c.hits.Add(1)
} else {
c.misses.Add(1)
}
return value, ok
}
// get is the read-side hot path. Holds only the shard's read lock; updates
// the LRU recency timestamp via atomic store on the entry — no write-lock
// upgrade is needed.
func (s *cacheShard) get(ctx context.Context, cacheKey string) ([]byte, bool) {
for {
s.mu.RLock()
entry, ok := s.entries[cacheKey]
if !ok {
s.mu.RUnlock()
return nil, false
}
if entry.state == cacheEntryInProgress {
waitCh := entry.waitCh
// Bound the wait by the placeholder's remaining stale window so an
// abandoned reservation cannot block waiters indefinitely.
remaining := s.staleTimeout - time.Since(entry.reservedAt)
s.mu.RUnlock()
if waitCh == nil {
// Defensive: treat a missing waitCh as a miss to avoid busy-looping.
return nil, false
}
if remaining <= 0 {
// Placeholder already stale; miss so the caller refetches.
return nil, false
}
// Wait for the in-flight fetch to either publish (Fulfill) or abort (Cancel/Delete/Flush).
timer := time.NewTimer(remaining)
select {
case <-waitCh:
timer.Stop()
case <-ctx.Done():
timer.Stop()
return nil, false
case <-timer.C:
return nil, false
}
continue
}
if entry.state != cacheEntryValid {
s.mu.RUnlock()
return nil, false
}
// Max-staleness backstop: a Valid entry older than maxStaleness is treated
// as a miss and evicted, so a lost invalidation or connection-lifecycle
// staleness (Window 2) cannot keep a stale value resident past MaxStaleness.
// Evict under the write lock so the next access re-fetches — a stale-but-present
// entry would otherwise suppress the re-fetch via Reserve.
if s.maxStaleness > 0 && time.Since(entry.validAt) > s.maxStaleness {
s.mu.RUnlock()
s.mu.Lock()
if cur, ok := s.entries[cacheKey]; ok && cur == entry {
s.removeEntryLocked(cacheKey)
}
s.mu.Unlock()
return nil, false
}
value := cloneBytes(entry.value)
// Record access timestamp without upgrading the lock. Last writer
// wins; cross-goroutine ordering of timestamps is fine for
// approximate-LRU semantics.
entry.lastAccessNs.Store(nextLRUToken())
s.mu.RUnlock()
return value, true
}
}
// Stats returns cumulative activity and current residency.
func (c *LocalCache) Stats() CSCStats {
return CSCStats{
Hits: c.hits.Load(),
Misses: c.misses.Load(),
Entries: c.Len(),
MemoryUsageBytes: c.MemoryUsage(),
}
}
// Reserve claims a missing cache key for fetching.
func (c *LocalCache) Reserve(cacheKey string, redisKeys []string) (token uint64, shouldFetch bool) {
keysCopy := cloneStrings(redisKeys)
waitCh := make(chan struct{})
reservedAt := time.Now()
sizeBytes := c.sizer(cacheKey, keysCopy, nil)
if sizeBytes < 0 {
sizeBytes = 0
}
newToken := c.nextToken.Add(1)
s := c.shardFor(cacheKey)
s.mu.Lock()
defer s.mu.Unlock()
if entry, ok := s.entries[cacheKey]; ok {
switch entry.state {
case cacheEntryValid:
// Existing-VALID hit: record access; caller will re-Get to
// retrieve.
entry.lastAccessNs.Store(nextLRUToken())
return 0, false
case cacheEntryInProgress:
if time.Since(entry.reservedAt) < s.staleTimeout {
return 0, false
}
s.removeEntryLocked(cacheKey)
default:
return 0, false
}
}
if s.maxMemoryBytes > 0 && sizeBytes > s.maxMemoryBytes {
return 0, true
}
entry := &cacheEntry{
cacheKey: cacheKey,
redisKeys: keysCopy,
state: cacheEntryInProgress,
token: newToken,
reservedAt: reservedAt,
waitCh: waitCh,
sizeBytes: sizeBytes,
}
entry.lastAccessNs.Store(nextLRUToken())
s.setEntryLocked(entry)
// Evict only Valid victims. If still over capacity the shard holds only
// in-flight placeholders: rather than abort a peer's fetch, drop this
// reservation (the caller fetches uncached). The hard cap holds either way.
s.evictValidLocked()
if s.overCapacityLocked() {
s.removeEntryLocked(cacheKey)
return 0, true
}
if s.entries[cacheKey] != entry {
return 0, true
}
return newToken, true
}
// FulfillOwned publishes a reserved value and records ownerConnID so
// EvictByConn can drop it when that connection is removed. ownerConnID == 0
// leaves the value unowned.
func (c *LocalCache) FulfillOwned(cacheKey string, token, ownerConnID uint64, value []byte) bool {
return c.fulfill(cacheKey, token, ownerConnID, value)
}
func (c *LocalCache) fulfill(cacheKey string, token, ownerConnID uint64, value []byte) bool {
valueCopy := cloneBytes(value)
s := c.shardFor(cacheKey)
s.mu.Lock()
defer s.mu.Unlock()
entry, ok := s.entries[cacheKey]
if !ok || entry.state != cacheEntryInProgress || entry.token != token {
return false
}
valueSize := s.sizer(cacheKey, entry.redisKeys, valueCopy)
if valueSize < 0 {
valueSize = 0
}
if s.maxMemoryBytes > 0 && valueSize > s.maxMemoryBytes {
s.removeEntryLocked(cacheKey)
return false
}
s.usedBytes += valueSize - entry.sizeBytes
entry.value = valueCopy
entry.sizeBytes = valueSize
entry.state = cacheEntryValid
entry.validAt = time.Now()
entry.token = 0
entry.lastAccessNs.Store(nextLRUToken())
if ownerConnID != 0 {
entry.ownerConnID = ownerConnID
s.indexConnLocked(ownerConnID, cacheKey)
}
s.closeWaitersLocked(entry)
s.evictIfNeededLocked()
current, stillExists := s.entries[cacheKey]
return stillExists && current == entry && entry.state == cacheEntryValid
}
// EvictByConn removes every entry fetched by connID and returns the count.
// Called when a conn is removed/swapped: the server stops delivering those
// keys' invalidations, so keeping them risks stale serves. Errs toward a miss.
func (c *LocalCache) EvictByConn(connID uint64) int {
if connID == 0 {
return 0
}
removed := 0
for i := range c.shards {
removed += c.shards[i].evictByConn(connID)
}
return removed
}
func (s *cacheShard) evictByConn(connID uint64) int {
s.mu.Lock()
defer s.mu.Unlock()
cacheKeys, ok := s.byConnID[connID]
if !ok {
return 0
}
toRemove := make([]string, 0, len(cacheKeys))
for cacheKey := range cacheKeys {
toRemove = append(toRemove, cacheKey)
}
removed := 0
for _, cacheKey := range toRemove {
if s.removeEntryLocked(cacheKey) {
removed++
}
}
return removed
}
// indexConnLocked records cacheKey under connID in the owning-connection index.
func (s *cacheShard) indexConnLocked(connID uint64, cacheKey string) {
cacheKeys := s.byConnID[connID]
if cacheKeys == nil {
cacheKeys = make(map[string]struct{})
s.byConnID[connID] = cacheKeys
}
cacheKeys[cacheKey] = struct{}{}
}
// Cancel removes the reservation matching token.
func (c *LocalCache) Cancel(cacheKey string, token uint64) bool {
s := c.shardFor(cacheKey)
s.mu.Lock()
defer s.mu.Unlock()
entry, ok := s.entries[cacheKey]
if !ok || entry.state != cacheEntryInProgress || entry.token != token {
return false
}
s.removeEntryLocked(cacheKey)
return true
}
// DeleteByRedisKey removes entries associated with redisKey.
func (c *LocalCache) DeleteByRedisKey(redisKey string) int {
removed := 0
for i := range c.shards {
removed += c.shards[i].deleteByRedisKey(redisKey)
}
return removed
}
func (s *cacheShard) deleteByRedisKey(redisKey string) int {
s.mu.Lock()
defer s.mu.Unlock()
cacheKeys, ok := s.byRedisKey[redisKey]
if !ok {
return 0
}
// Remove IN_PROGRESS placeholders too: an invalidation can arrive on a
// different stream than the in-flight reply (the background drainer), so the
// fetch may predate the write. Removing makes the racing Fulfill fail and
// waiters refetch, so a raced-invalidation value is never published.
toRemove := make([]string, 0, len(cacheKeys))
for cacheKey := range cacheKeys {
toRemove = append(toRemove, cacheKey)
}
removed := 0
for _, cacheKey := range toRemove {
if s.removeEntryLocked(cacheKey) {
removed++
}
}
return removed
}
// DeleteByCacheKey removes one entry by its internal cache key.
func (c *LocalCache) DeleteByCacheKey(cacheKey string) bool {
s := c.shardFor(cacheKey)
s.mu.Lock()
defer s.mu.Unlock()
return s.removeEntryLocked(cacheKey)
}
// Flush removes all entries.
func (c *LocalCache) Flush() int {
removed := 0
for i := range c.shards {
removed += c.shards[i].flush()
}
return removed
}
func (s *cacheShard) flush() int {
s.mu.Lock()
defer s.mu.Unlock()
// Flush placeholders too (see deleteByRedisKey): a flush (FLUSHDB, or the
// owned-cache flush on Close) means everything, including in-flight fetches,
// may be stale.
removed := 0
for cacheKey := range s.entries {
if s.removeEntryLocked(cacheKey) {
removed++
}
}
return removed
}
// Len returns the current number of entries and reservations.
func (c *LocalCache) Len() int {
n := 0
for i := range c.shards {
s := &c.shards[i]
s.mu.RLock()
n += len(s.entries)
s.mu.RUnlock()
}
return n
}
// MemoryUsage returns the cache's estimated memory usage in bytes.
func (c *LocalCache) MemoryUsage() int64 {
var total int64
for i := range c.shards {
s := &c.shards[i]
s.mu.RLock()
total += s.usedBytes
s.mu.RUnlock()
}
return total
}
func (s *cacheShard) setEntryLocked(entry *cacheEntry) {
if old, exists := s.entries[entry.cacheKey]; exists {
s.removeEntryLocked(old.cacheKey)
}
s.entries[entry.cacheKey] = entry
s.usedBytes += entry.sizeBytes
for _, redisKey := range entry.redisKeys {
cacheKeys := s.byRedisKey[redisKey]
if cacheKeys == nil {
cacheKeys = make(map[string]struct{})
s.byRedisKey[redisKey] = cacheKeys
}
cacheKeys[entry.cacheKey] = struct{}{}
}
}
func (s *cacheShard) removeEntryLocked(cacheKey string) bool {
entry, exists := s.entries[cacheKey]
if !exists {
return false
}
delete(s.entries, cacheKey)
s.usedBytes -= entry.sizeBytes
if s.usedBytes < 0 {
s.usedBytes = 0
}
for _, redisKey := range entry.redisKeys {
cacheKeys := s.byRedisKey[redisKey]
if cacheKeys == nil {
continue
}
delete(cacheKeys, cacheKey)
if len(cacheKeys) == 0 {
delete(s.byRedisKey, redisKey)
}
}
if entry.ownerConnID != 0 {
if cacheKeys := s.byConnID[entry.ownerConnID]; cacheKeys != nil {
delete(cacheKeys, cacheKey)
if len(cacheKeys) == 0 {
delete(s.byConnID, entry.ownerConnID)
}
}
}
s.closeWaitersLocked(entry)
return true
}
func (s *cacheShard) closeWaitersLocked(entry *cacheEntry) {
if entry.waitCh != nil && !entry.waitClosed {
close(entry.waitCh)
entry.waitClosed = true
}
}
func (s *cacheShard) overCapacityLocked() bool {
if s.maxEntries > 0 && len(s.entries) > s.maxEntries {
return true
}
if s.maxMemoryBytes > 0 && s.usedBytes > s.maxMemoryBytes {
return true
}
return false
}
// evictIfNeededLocked evicts by approximate LRU (O(N) scan; rare in
// well-sized caches) until under capacity. Used by Set/Fulfill: it prefers a
// Valid victim but falls back to the oldest IN_PROGRESS placeholder to keep the
// hard cap (that placeholder's Fulfill then fails and its waiters refetch).
func (s *cacheShard) evictIfNeededLocked() {
for s.overCapacityLocked() {
victim := s.oldestLocked(cacheEntryValid)
if victim == nil {
victim = s.oldestLocked(cacheEntryInProgress)
}
if victim == nil {
return
}
s.removeEntryLocked(victim.cacheKey)
}
}
// evictValidLocked evicts only Valid entries until under capacity. Unlike
// evictIfNeededLocked it never evicts a placeholder, so Reserve can't abort a
// peer's in-flight fetch.
func (s *cacheShard) evictValidLocked() {
for s.overCapacityLocked() {
victim := s.oldestLocked(cacheEntryValid)
if victim == nil {
return
}
s.removeEntryLocked(victim.cacheKey)
}
}
// oldestLocked returns the entry in the given state with the smallest
// lastAccessNs (the least-recently-used), or nil when none exists.
func (s *cacheShard) oldestLocked(state cacheEntryState) *cacheEntry {
var victim *cacheEntry
var oldestNs int64 = math.MaxInt64
for _, e := range s.entries {
if e.state != state {
continue
}
if ns := e.lastAccessNs.Load(); ns < oldestNs {
oldestNs = ns
victim = e
}
}
return victim
}
func cloneBytes(src []byte) []byte {
if src == nil {
return nil
}
dst := make([]byte, len(src))
copy(dst, src)
return dst
}
func cloneStrings(src []string) []string {
if len(src) == 0 {
return nil
}
dst := make([]string, len(src))
copy(dst, src)
return dst
}