Files
1Panel/core/utils/ttlstore/ttlstore.go
2026-08-17 13:58:53 +08:00

164 lines
2.8 KiB
Go

package ttlstore
import (
"sync"
"time"
)
const maxIDGenerationAttempts = 32
type Item[T any] struct {
Value T
ExpiresAt time.Time
}
type Store[T any] struct {
mu sync.Mutex
items map[string]Item[T]
ttl time.Duration
maxEntries int
newID func() string
now func() time.Time
}
func New[T any](ttl time.Duration, maxEntries int, newID func() string) *Store[T] {
return &Store[T]{
items: make(map[string]Item[T]),
ttl: ttl,
maxEntries: maxEntries,
newID: newID,
now: time.Now,
}
}
func (s *Store[T]) Set(value T) string {
return s.set(value, true)
}
func (s *Store[T]) TrySet(value T) string {
return s.set(value, false)
}
func (s *Store[T]) set(value T, evictOldest bool) string {
s.mu.Lock()
defer s.mu.Unlock()
s.cleanupExpiredLocked()
itemID := ""
for range maxIDGenerationAttempts {
candidate := s.newID()
if candidate == "" {
continue
}
if _, exists := s.items[candidate]; exists {
continue
}
itemID = candidate
break
}
if itemID == "" {
return ""
}
if len(s.items) >= s.maxEntries {
if !evictOldest {
return ""
}
s.removeOldestLocked()
}
s.items[itemID] = Item[T]{
Value: value,
ExpiresAt: s.now().Add(s.ttl),
}
return itemID
}
func (s *Store[T]) Get(itemID string) (T, bool) {
s.mu.Lock()
defer s.mu.Unlock()
item, ok := s.items[itemID]
if !ok {
var zero T
return zero, false
}
if s.now().After(item.ExpiresAt) {
delete(s.items, itemID)
var zero T
return zero, false
}
return item.Value, true
}
// Take atomically returns and removes an item. It is intended for one-time
// credentials such as authentication state and handoff tickets.
func (s *Store[T]) Take(itemID string) (T, bool) {
s.mu.Lock()
defer s.mu.Unlock()
item, ok := s.items[itemID]
if !ok {
var zero T
return zero, false
}
delete(s.items, itemID)
if s.now().After(item.ExpiresAt) {
var zero T
return zero, false
}
return item.Value, true
}
func (s *Store[T]) Update(itemID string, fn func(*T) bool) bool {
s.mu.Lock()
defer s.mu.Unlock()
item, ok := s.items[itemID]
if !ok {
return false
}
if s.now().After(item.ExpiresAt) {
delete(s.items, itemID)
return false
}
keep := fn(&item.Value)
if !keep {
delete(s.items, itemID)
return true
}
s.items[itemID] = item
return true
}
func (s *Store[T]) Delete(itemID string) {
s.mu.Lock()
defer s.mu.Unlock()
delete(s.items, itemID)
}
func (s *Store[T]) cleanupExpiredLocked() {
now := s.now()
for id, item := range s.items {
if now.After(item.ExpiresAt) {
delete(s.items, id)
}
}
}
func (s *Store[T]) removeOldestLocked() {
var oldestID string
var oldestTime time.Time
for id, item := range s.items {
if oldestID == "" || item.ExpiresAt.Before(oldestTime) {
oldestID = id
oldestTime = item.ExpiresAt
}
}
if oldestID != "" {
delete(s.items, oldestID)
}
}