Ctrl K
ComunidadGuías…

internal/expiry/index.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// Package expiry orders keys by expiry time, so a bounded cache can find what
// to drop without scanning its whole map.
package expiry

import "time"

// Index is a min-heap of keys ordered by expiry. It holds no values and takes
// no lock: the cache owns both, and tracks a key when it first inserts it.
//
// Extending a key's TTL is deliberately not reported to the index. A cache
// that refreshes on every read would otherwise pay a heap fix on its hottest
// path, so the index is allowed to hold an expiry earlier than the truth.
// That is why Expired and Soonest hand back the expiry they recorded: the
// caller compares it with its own map, and re-Tracks a key that has moved.
// A key the cache has already deleted is simply discarded when it surfaces.
type Index[K comparable] struct {
	queue []node[K]
}

type node[K comparable] struct {
	key     K
	expires time.Time
}

// Track records that key expires at expires. Call it when a key enters the
// cache, and again when a popped key turns out to have been refreshed since.
func (i *Index[K]) Track(key K, expires time.Time) {
	i.queue = append(i.queue, node[K]{key: key, expires: expires})
	i.up(len(i.queue) - 1)
}

// Expired pops the next key due at now, with the expiry the index holds for
// it. It reports false when the soonest key is not due yet.
func (i *Index[K]) Expired(now time.Time) (K, time.Time, bool) {
	if len(i.queue) == 0 || i.queue[0].expires.After(now) {
		var zero K
		return zero, time.Time{}, false
	}
	next := i.pop()
	return next.key, next.expires, true
}

// Soonest pops the key closest to expiry, with the expiry the index holds for
// it. It reports false when the index is empty.
func (i *Index[K]) Soonest() (K, time.Time, bool) {
	if len(i.queue) == 0 {
		var zero K
		return zero, time.Time{}, false
	}
	next := i.pop()
	return next.key, next.expires, true
}

// Len is how many keys are tracked, including ones the cache has deleted and
// the index has not surfaced yet.
func (i *Index[K]) Len() int { return len(i.queue) }

// Stale reports whether the index holds far more keys than the live count,
// which is what happens when a cache deletes keys it never pops. The caller
// answers by resetting and re-tracking what it still holds.
func (i *Index[K]) Stale(live int) bool { return len(i.queue) > 2*live+16 }

// Reset empties the index.
func (i *Index[K]) Reset() { i.queue = i.queue[:0] }

func (i *Index[K]) pop() node[K] {
	top := i.queue[0]
	last := len(i.queue) - 1
	i.queue[0] = i.queue[last]
	var zero node[K]
	i.queue[last] = zero
	i.queue = i.queue[:last]
	if last > 0 {
		i.down(0)
	}
	return top
}

func (i *Index[K]) up(child int) {
	for child > 0 {
		parent := (child - 1) / 2
		if !i.queue[child].expires.Before(i.queue[parent].expires) {
			return
		}
		i.queue[child], i.queue[parent] = i.queue[parent], i.queue[child]
		child = parent
	}
}

func (i *Index[K]) down(parent int) {
	for {
		left := 2*parent + 1
		if left >= len(i.queue) {
			return
		}
		soonest := left
		if right := left + 1; right < len(i.queue) && i.queue[right].expires.Before(i.queue[left].expires) {
			soonest = right
		}
		if !i.queue[soonest].expires.Before(i.queue[parent].expires) {
			return
		}
		i.queue[parent], i.queue[soonest] = i.queue[soonest], i.queue[parent]
		parent = soonest
	}
}