consul/lib/ttlcache/eviction.go

205 lines
5.4 KiB
Go
Raw Permalink Normal View History

// Copyright (c) HashiCorp, Inc.
[COMPLIANCE] License changes (#18443) * Adding explicit MPL license for sub-package This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Adding explicit MPL license for sub-package This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Updating the license from MPL to Business Source License Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at <Blog URL>, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl. * add missing license headers * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 --------- Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
2023-08-11 13:12:13 +00:00
// SPDX-License-Identifier: BUSL-1.1
/*
Package ttlcache provides an ExpiryHeap that can be used by a cache to track the
expiration time of its entries. When an expiry is reached the Timer will fire
and the entry can be removed.
*/
package ttlcache
import (
"container/heap"
"time"
)
// Entry in the ExpiryHeap, tracks the index and expiry time of an item in a
// ttl cache.
type Entry struct {
key string
expiry time.Time
heapIndex int
}
// NotIndexed indicates that the entry does not exist in the heap. Either because
// it is nil, or because it was removed.
const NotIndexed = -1
// Index returns the index of this entry within the heap.
func (e *Entry) Index() int {
if e == nil {
return NotIndexed
}
return e.heapIndex
}
// Key returns the key for the entry in the heap.
func (e *Entry) Key() string {
return e.key
}
// ExpiryHeap is a heap that is ordered by the expiry time of entries. It may
// be used by a cache or storage to expiry items after a TTL.
//
// ExpiryHeap expects the caller to synchronize calls to most of its methods. This
// is necessary because the cache needs to ensure that updates to both its
// storage and the ExpiryHeap are synchronized.
type ExpiryHeap struct {
entries []*Entry
// NotifyCh is sent a value whenever the 0 index value of the heap
// changes. This can be used to detect when the earliest value
// changes.
NotifyCh chan struct{}
}
// NewExpiryHeap creates and returns a new ExpiryHeap.
func NewExpiryHeap() *ExpiryHeap {
h := &ExpiryHeap{NotifyCh: make(chan struct{}, 1)}
heap.Init((*entryHeap)(h))
return h
}
// Add an entry to the heap.
//
// Must be synchronized by the caller.
func (h *ExpiryHeap) Add(key string, expiry time.Duration) *Entry {
entry := &Entry{
key: key,
expiry: time.Now().Add(expiry),
// Set the initial heap index to the last index. If the entry is swapped it
// will have the correct index set, and if it remains at the end the last
// index will be correct.
heapIndex: len(h.entries),
}
heap.Push((*entryHeap)(h), entry)
if entry.heapIndex == 0 {
h.notify()
}
return entry
}
// Update the entry that is currently at idx with the new expiry time, if the new
// expiry time is further into the future. The heap will be rebalanced after the
// entry is updated. If the new expiry time is earlier than the existing expiry
// time than the expiry is not modified.
//
// Must be synchronized by the caller.
func (h *ExpiryHeap) Update(idx int, expiry time.Duration) {
if idx == NotIndexed {
// the previous entry did not have a valid index, its not in the heap
return
}
entry := h.entries[idx]
newExpiry := time.Now().Add(expiry)
// Ignore the new expiry if the time is earlier than the existing expiry.
if entry.expiry.After(newExpiry) {
return
}
entry.expiry = newExpiry
heap.Fix((*entryHeap)(h), idx)
if idx == 0 || entry.heapIndex == 0 {
h.notify()
}
}
// Remove the entry at idx from the heap.
//
// Must be synchronized by the caller.
func (h *ExpiryHeap) Remove(idx int) {
entry := h.entries[idx]
heap.Remove((*entryHeap)(h), idx)
// A goroutine which is fetching a new value will have a reference to this
// entry. When it re-acquires the lock it needs to be informed that
// the entry was expired while it was fetching. Setting heapIndex to -1
// indicates that the entry is no longer in the heap, and must be re-added.
entry.heapIndex = NotIndexed
if idx == 0 {
h.notify()
}
}
type entryHeap ExpiryHeap
func (h *entryHeap) Len() int { return len(h.entries) }
func (h *entryHeap) Swap(i, j int) {
h.entries[i], h.entries[j] = h.entries[j], h.entries[i]
h.entries[i].heapIndex = i
h.entries[j].heapIndex = j
}
func (h *entryHeap) Less(i, j int) bool {
// The usage of Before here is important (despite being obvious):
// this function uses the monotonic time that should be available
// on the time.Time value so the heap is immune to wall clock changes.
return h.entries[i].expiry.Before(h.entries[j].expiry)
}
// heap.Interface, this isn't expected to be called directly.
func (h *entryHeap) Push(x interface{}) {
h.entries = append(h.entries, x.(*Entry))
}
// heap.Interface, this isn't expected to be called directly.
func (h *entryHeap) Pop() interface{} {
n := len(h.entries)
entries := h.entries
last := entries[n-1]
h.entries = entries[0 : n-1]
return last
}
// notify the timer that the head value has changed, so the expiry time has
// also likely changed.
func (h *ExpiryHeap) notify() {
// Send to channel without blocking. Skips sending if there is already
// an item in the buffered channel.
select {
case h.NotifyCh <- struct{}{}:
default:
}
}
// Next returns a Timer that waits until the first entry in the heap expires.
//
// Must be synchronized by the caller.
func (h *ExpiryHeap) Next() Timer {
if len(h.entries) == 0 {
return Timer{}
}
entry := h.entries[0]
return Timer{
timer: time.NewTimer(time.Until(entry.expiry)),
Entry: entry,
}
}
// Timer provides a channel to block on. When the Wait channel receives an
// item the Timer.Entry has expired. The caller is expected to call
// ExpiryHeap.Remove with the Entry.Index().
//
// The caller is responsible for calling Stop to stop the timer if the timer has
// not fired.
type Timer struct {
timer *time.Timer
Entry *Entry
}
func (t *Timer) Wait() <-chan time.Time {
if t.timer == nil {
return nil
}
return t.timer.C
}
func (t *Timer) Stop() {
if t.timer != nil {
t.timer.Stop()
}
}