mirror of
https://github.com/lukaszraczylo/go-simple-graphql.git
synced 2026-10-12 00:45:31 +00:00
276 lines
5.3 KiB
Go
276 lines
5.3 KiB
Go
package libpack_cache
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/gzip"
|
|
"hash/fnv"
|
|
"io"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type CacheEntry struct {
|
|
ExpiresAt time.Time
|
|
Value []byte
|
|
IsCompressed bool
|
|
}
|
|
|
|
const shardCount = 256 // Must be power of 2
|
|
|
|
type shard struct {
|
|
entries map[string]CacheEntry
|
|
sync.RWMutex
|
|
}
|
|
|
|
type Cache struct {
|
|
compressPool sync.Pool
|
|
decompressPool sync.Pool
|
|
shards [shardCount]*shard
|
|
globalTTL time.Duration
|
|
cleanupChan chan struct{}
|
|
stopChan chan struct{}
|
|
stopped bool
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
// getShard returns the appropriate shard for a given key
|
|
func (c *Cache) getShard(key string) *shard {
|
|
hash := fnv.New32a()
|
|
hash.Write([]byte(key))
|
|
return c.shards[hash.Sum32()%shardCount]
|
|
}
|
|
|
|
func New(globalTTL time.Duration) *Cache {
|
|
cache := &Cache{
|
|
globalTTL: globalTTL,
|
|
cleanupChan: make(chan struct{}, 1),
|
|
stopChan: make(chan struct{}),
|
|
compressPool: sync.Pool{
|
|
New: func() interface{} {
|
|
w := gzip.NewWriter(nil)
|
|
return w
|
|
},
|
|
},
|
|
decompressPool: sync.Pool{
|
|
New: func() interface{} {
|
|
r, _ := gzip.NewReader(bytes.NewReader([]byte{}))
|
|
return r
|
|
},
|
|
},
|
|
}
|
|
|
|
// Initialize shards
|
|
for i := 0; i < shardCount; i++ {
|
|
cache.shards[i] = &shard{
|
|
entries: make(map[string]CacheEntry),
|
|
}
|
|
}
|
|
|
|
go cache.lazyCleanupWorker()
|
|
go cache.periodicCleanupRoutine(globalTTL)
|
|
return cache
|
|
}
|
|
|
|
func (c *Cache) lazyCleanupWorker() {
|
|
for {
|
|
select {
|
|
case <-c.cleanupChan:
|
|
c.mu.RLock()
|
|
if c.stopped {
|
|
c.mu.RUnlock()
|
|
return
|
|
}
|
|
c.mu.RUnlock()
|
|
c.CleanExpiredEntries()
|
|
case <-c.stopChan:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Cache) periodicCleanupRoutine(globalTTL time.Duration) {
|
|
// Use a shorter cleanup interval for better responsiveness, but not less than 1 second
|
|
cleanupInterval := globalTTL / 4
|
|
if cleanupInterval < time.Second {
|
|
cleanupInterval = time.Second
|
|
}
|
|
|
|
ticker := time.NewTicker(cleanupInterval)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
c.mu.RLock()
|
|
if c.stopped {
|
|
c.mu.RUnlock()
|
|
return
|
|
}
|
|
c.mu.RUnlock()
|
|
c.CleanExpiredEntries()
|
|
case <-c.stopChan:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *Cache) triggerLazyCleanup() {
|
|
c.mu.RLock()
|
|
if c.stopped {
|
|
c.mu.RUnlock()
|
|
return
|
|
}
|
|
c.mu.RUnlock()
|
|
|
|
select {
|
|
case c.cleanupChan <- struct{}{}:
|
|
// Cleanup triggered
|
|
default:
|
|
// Cleanup already pending, skip
|
|
}
|
|
}
|
|
|
|
func (c *Cache) Stop() {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
if c.stopped {
|
|
return // Already stopped
|
|
}
|
|
|
|
c.stopped = true
|
|
close(c.stopChan)
|
|
}
|
|
|
|
func (c *Cache) Set(key string, value []byte, ttl time.Duration) {
|
|
shard := c.getShard(key)
|
|
shard.Lock()
|
|
defer shard.Unlock()
|
|
|
|
const compressionThreshold = 1024 // 1KB threshold
|
|
var finalValue []byte
|
|
var isCompressed bool
|
|
|
|
if len(value) >= compressionThreshold {
|
|
compressedValue, err := c.compress(value)
|
|
if err != nil {
|
|
// If compression fails, store uncompressed
|
|
finalValue = value
|
|
isCompressed = false
|
|
} else {
|
|
// Only use compression if it actually reduces size
|
|
if len(compressedValue) < len(value) {
|
|
finalValue = compressedValue
|
|
isCompressed = true
|
|
} else {
|
|
finalValue = value
|
|
isCompressed = false
|
|
}
|
|
}
|
|
} else {
|
|
finalValue = value
|
|
isCompressed = false
|
|
}
|
|
|
|
shard.entries[key] = CacheEntry{
|
|
Value: finalValue,
|
|
ExpiresAt: time.Now().Add(ttl),
|
|
IsCompressed: isCompressed,
|
|
}
|
|
}
|
|
|
|
func (c *Cache) Get(key string) ([]byte, bool) {
|
|
shard := c.getShard(key)
|
|
shard.RLock()
|
|
entry, ok := shard.entries[key]
|
|
if !ok {
|
|
shard.RUnlock()
|
|
return nil, false
|
|
}
|
|
|
|
if entry.ExpiresAt.Before(time.Now()) {
|
|
shard.RUnlock()
|
|
// Trigger lazy cleanup instead of immediate deletion
|
|
c.triggerLazyCleanup()
|
|
return nil, false
|
|
}
|
|
shard.RUnlock()
|
|
|
|
var value []byte
|
|
var err error
|
|
|
|
if entry.IsCompressed {
|
|
value, err = c.decompress(entry.Value)
|
|
if err != nil {
|
|
return nil, false
|
|
}
|
|
} else {
|
|
value = entry.Value
|
|
}
|
|
|
|
return value, true
|
|
}
|
|
|
|
func (c *Cache) Delete(key string) {
|
|
shard := c.getShard(key)
|
|
shard.Lock()
|
|
delete(shard.entries, key)
|
|
shard.Unlock()
|
|
}
|
|
|
|
func (c *Cache) CleanExpiredEntries() {
|
|
now := time.Now()
|
|
for _, shard := range c.shards {
|
|
shard.Lock()
|
|
for key, entry := range shard.entries {
|
|
if entry.ExpiresAt.Before(now) {
|
|
delete(shard.entries, key)
|
|
}
|
|
}
|
|
shard.Unlock()
|
|
}
|
|
}
|
|
|
|
func (c *Cache) compress(data []byte) ([]byte, error) {
|
|
w := c.compressPool.Get().(*gzip.Writer)
|
|
defer c.compressPool.Put(w)
|
|
|
|
var buf bytes.Buffer
|
|
w.Reset(&buf)
|
|
if _, err := w.Write(data); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := w.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
func (c *Cache) decompress(data []byte) ([]byte, error) {
|
|
r, ok := c.decompressPool.Get().(*gzip.Reader)
|
|
if !ok || r == nil {
|
|
// If r is nil or type assertion fails, create a new gzip.Reader
|
|
var err error
|
|
r, err = gzip.NewReader(bytes.NewReader(data))
|
|
if err != nil {
|
|
return nil, err // Handle the error if gzip.NewReader fails
|
|
}
|
|
} else {
|
|
// Reset the existing reader with new data
|
|
if err := r.Reset(bytes.NewReader(data)); err != nil {
|
|
return nil, err // Handle the error if Reset fails
|
|
}
|
|
}
|
|
defer r.Close()
|
|
|
|
// Ensure the reader is returned to the pool
|
|
defer c.decompressPool.Put(r)
|
|
|
|
// Read all the data from the reader
|
|
decompressedData, err := io.ReadAll(r)
|
|
if err != nil {
|
|
return nil, err // Handle the error if reading fails
|
|
}
|
|
return decompressedData, nil
|
|
}
|