From c8c4aa7061273763b3d859416a29567ab4cc0832 Mon Sep 17 00:00:00 2001 From: Bo Thompson Date: Wed, 11 Oct 2023 17:38:16 -0700 Subject: [PATCH 01/20] [go] new package: pkg/cache Package `cache` provides a cache implementation with support for multiple eviction policies. Currently supported eviction policies include: - LRU - LFU - SLRU - TinyLFU --- go/appencryption/pkg/cache/cache.go | 336 ++++++++++++++++++ go/appencryption/pkg/cache/cache_test.go | 45 +++ go/appencryption/pkg/cache/internal/LICENSE | 26 ++ go/appencryption/pkg/cache/internal/doc.go | 7 + go/appencryption/pkg/cache/internal/filter.go | 103 ++++++ .../pkg/cache/internal/filter_test.go | 43 +++ go/appencryption/pkg/cache/internal/hash.go | 130 +++++++ .../pkg/cache/internal/hash_test.go | 59 +++ go/appencryption/pkg/cache/internal/sketch.go | 91 +++++ .../pkg/cache/internal/sketch_test.go | 67 ++++ go/appencryption/pkg/cache/lfu.go | 152 ++++++++ .../pkg/cache/lfu_example_test.go | 44 +++ go/appencryption/pkg/cache/lfu_test.go | 201 +++++++++++ go/appencryption/pkg/cache/lru.go | 168 +++++++++ go/appencryption/pkg/cache/lru_test.go | 284 +++++++++++++++ go/appencryption/pkg/cache/tlfu.go | 202 +++++++++++ go/appencryption/pkg/cache/tlfu_test.go | 134 +++++++ 17 files changed, 2092 insertions(+) create mode 100644 go/appencryption/pkg/cache/cache.go create mode 100644 go/appencryption/pkg/cache/cache_test.go create mode 100644 go/appencryption/pkg/cache/internal/LICENSE create mode 100644 go/appencryption/pkg/cache/internal/doc.go create mode 100644 go/appencryption/pkg/cache/internal/filter.go create mode 100644 go/appencryption/pkg/cache/internal/filter_test.go create mode 100644 go/appencryption/pkg/cache/internal/hash.go create mode 100644 go/appencryption/pkg/cache/internal/hash_test.go create mode 100644 go/appencryption/pkg/cache/internal/sketch.go create mode 100644 go/appencryption/pkg/cache/internal/sketch_test.go create mode 100644 go/appencryption/pkg/cache/lfu.go create mode 100644 go/appencryption/pkg/cache/lfu_example_test.go create mode 100644 go/appencryption/pkg/cache/lfu_test.go create mode 100644 go/appencryption/pkg/cache/lru.go create mode 100644 go/appencryption/pkg/cache/lru_test.go create mode 100644 go/appencryption/pkg/cache/tlfu.go create mode 100644 go/appencryption/pkg/cache/tlfu_test.go diff --git a/go/appencryption/pkg/cache/cache.go b/go/appencryption/pkg/cache/cache.go new file mode 100644 index 000000000..2c28f0f20 --- /dev/null +++ b/go/appencryption/pkg/cache/cache.go @@ -0,0 +1,336 @@ +// Package cache provides a cache implementation with support for multiple +// eviction policies. +// +// Currently supported eviction policies: +// - LRU (least recently used) +// - LFU (least frequently used) +// - SLRU (segmented least recently used) +// - TinyLFU (tiny least frequently used) +// +// The cache is safe for concurrent access. +package cache + +import ( + "container/list" + "fmt" + "sync" +) + +// Interface is intended to be a generic interface for cache implementations. +type Interface[K comparable, V any] interface { + Get(key K) (V, bool) + GetOrPanic(key K) V + Set(key K, value V) + Delete(key K) bool + Len() int + Capacity() int + Close() error +} + +// CachePolicy is an enum for the different eviction policies. +type CachePolicy string + +const ( + // LRU is the least recently used cache policy. + LRU CachePolicy = "lru" + // LFU is the least frequently used cache policy. + LFU CachePolicy = "lfu" + // SLRU is the segmented least recently used cache policy. + SLRU CachePolicy = "slru" + // TinyLFU is the tiny least frequently used cache policy. + TinyLFU CachePolicy = "tinylfu" +) + +// String returns the string representation of the eviction policy. +func (e CachePolicy) String() string { + return string(e) +} + +// EvictFunc is called when an item is evicted from the cache. The key and +// value of the evicted item are passed to the function. +type EvictFunc[K comparable, V any] func(key K, value V) + +// NopEvict is a no-op EvictFunc. +func NopEvict[K comparable, V any](K, V) {} + +// Option is a functional option for the cache. +type Option[K comparable, V any] func(*cache[K, V]) + +// WithEvictFunc sets the EvictFunc for the cache. +func WithEvictFunc[K comparable, V any](fn EvictFunc[K, V]) Option[K, V] { + return func(c *cache[K, V]) { + c.onEvictCallback = fn + } +} + +// WithPolicy sets the eviction policy for the cache. +func WithPolicy[K comparable, V any](policy CachePolicy) Option[K, V] { + return func(c *cache[K, V]) { + switch policy { + case LRU: + c.policy = new(lru[K, V]) + case LFU: + c.policy = new(lfu[K, V]) + case SLRU: + c.policy = new(slru[K, V]) + case TinyLFU: + c.policy = new(tinyLFU[K, V]) + default: + panic("cache: unsupported policy " + policy.String()) + } + } +} + +// event is the cache event (evictItem or closeCache). +type event int + +const ( + // evictItem is sent on the events channel when an item is evicted from the cache. + evictItem event = iota + // closeCache is sent on the events channel when the cache is closed. + closeCache +) + +type cacheItem[K comparable, V any] struct { + key K + value V + parent *list.Element // Pointer to the frequencyParent +} + +// cacheEvent is the event sent on the events channel. +type cacheEvent[K comparable, V any] struct { + event event + item *cacheItem[K, V] +} + +// policy is the generic interface for eviction policies. +type policy[K comparable, V any] interface { + // init initializes the policy with the given capacity. + init(int) + // capacity returns the capacity of the policy. + capacity() int + // close removes all items from the cache, sends a close event to the event + // processing goroutine, and waits for it to exit. + close() + // admit is called when an item is admit to the cache. + admit(item *cacheItem[K, V]) + // access is called when an item is access. + access(item *cacheItem[K, V]) + // victim returns the victim item to be evicted. + victim() *cacheItem[K, V] + // remove is called when an item is remove from the cache. + remove(item *cacheItem[K, V]) +} + +// cache is the generic cache type. +type cache[K comparable, V any] struct { + byKey map[K]*cacheItem[K, V] // Hashmap containing *CacheItems for O(1) access + size int // Current number of items in the cache + events chan cacheEvent[K, V] // Channel to events when an item is evicted + policy policy[K, V] // Eviction policy + + mux sync.RWMutex // synchronize access to the cache + + closing bool + closeWG sync.WaitGroup + + // onEvictCallback is called when an item is evicted from the cache. The key, value, + // and frequency of the evicted item are passed to the function. Set to + // a custom function to handle evicted items. The default is a no-op. + onEvictCallback EvictFunc[K, V] +} + +// New returns a new cache with the given capacity, eviction policy, and +// options. +func New[K comparable, V any](capacity int, options ...Option[K, V]) Interface[K, V] { + return new(cache[K, V]).init(capacity, options...) +} + +// init initializes the cache with the given capacity and options. It must be +// called before the cache can be used. +func (c *cache[K, V]) init(capacity int, opts ...Option[K, V]) *cache[K, V] { + c.byKey = make(map[K]*cacheItem[K, V]) + c.events = make(chan cacheEvent[K, V], 100) + c.onEvictCallback = NopEvict[K, V] + + for _, opt := range opts { + opt(c) + } + + if c.policy == nil { + c.policy = new(lru[K, V]) + } + + c.policy.init(capacity) + + c.closeWG.Add(1) + go c.processEvents() + + return c +} + +// processEvents processes events in a separate goroutine. +func (c *cache[K, V]) processEvents() { + defer c.closeWG.Done() + + for event := range c.events { + switch event.event { + case evictItem: + c.onEvictCallback(event.item.key, event.item.value) + case closeCache: + return + } + } +} + +// Close the cache and remove all items. The cache cannot be used after it is +// closed. +func (c *cache[K, V]) Close() error { + c.mux.Lock() + defer c.mux.Unlock() + + // if the cache is already closed, do nothing + if c.closing { + return nil + } + + c.closing = true + + for c.size > 0 { + c.evict() + } + + c.events <- cacheEvent[K, V]{event: closeCache} + + c.closeWG.Wait() + + close(c.events) + + c.byKey = nil + c.events = nil + + c.policy.close() + + return nil +} + +// Len returns the number of items in the cache. +func (c *cache[K, V]) Len() int { + c.mux.RLock() + defer c.mux.RUnlock() + + return c.size +} + +// Capacity returns the maximum number of items in the cache. +func (c *cache[K, V]) Capacity() int { + c.mux.RLock() + defer c.mux.RUnlock() + + return c.policy.capacity() +} + +// Set adds a value to the cache. If an item with the given key already exists, +// its value is updated. +func (c *cache[K, V]) Set(key K, value V) { + c.mux.Lock() + defer c.mux.Unlock() + + if c.closing { + return + } + + if item, ok := c.byKey[key]; ok { + item.value = value + + c.policy.access(item) + + return + } + + // if the cache is full, evict the least recently used item + if c.size == c.policy.capacity() { + c.evict() + } + + item := &cacheItem[K, V]{key: key, value: value} + + c.byKey[key] = item + + c.size++ + + c.policy.admit(item) +} + +// Get returns a value from the cache. If an item with the given key does not +// exist, the second return value will be false. +func (c *cache[K, V]) Get(key K) (V, bool) { + c.mux.Lock() + defer c.mux.Unlock() + + if c.closing { + return c.zeroValue(), false + } + + item, ok := c.byKey[key] + if !ok { + return c.zeroValue(), false + } + + c.policy.access(item) + + return item.value, true +} + +// GetOrPanic returns the value for the given key. If the key does not exist, a +// panic is raised. +func (c *cache[K, V]) GetOrPanic(key K) V { + if item, ok := c.Get(key); ok { + return item + } + + panic(fmt.Sprintf("key does not exist: %v", key)) +} + +// Delete removes the given key from the cache. If the key does not exist, the +// return value is false. +func (c *cache[K, V]) Delete(key K) bool { + c.mux.Lock() + defer c.mux.Unlock() + + if c.closing { + return false + } + + item, ok := c.byKey[key] + if !ok { + return false + } + + delete(c.byKey, key) + + c.size-- + + c.policy.remove(item) + + return true +} + +// zeroValue returns the zero value for type V. +func (c *cache[K, V]) zeroValue() V { + var v V + return v +} + +// evict removes an item from the cache and sends an evict event. +func (c *cache[K, V]) evict() { + item := c.policy.victim() + + delete(c.byKey, item.key) + + c.size-- + + c.policy.remove(item) + + c.events <- cacheEvent[K, V]{event: evictItem, item: item} +} diff --git a/go/appencryption/pkg/cache/cache_test.go b/go/appencryption/pkg/cache/cache_test.go new file mode 100644 index 000000000..50e95ad51 --- /dev/null +++ b/go/appencryption/pkg/cache/cache_test.go @@ -0,0 +1,45 @@ +package cache_test + +import ( + "testing" + + "github.com/stretchr/testify/suite" + + "github.com/godaddy/asherah/go/appencryption/pkg/cache" +) + +type CacheSuite struct { + suite.Suite + cache cache.Interface[int, string] +} + +func TestCacheSuite(t *testing.T) { + suite.Run(t, new(CacheSuite)) +} + +func (suite *CacheSuite) SetupTest() { + suite.cache = cache.New[int, string](2) +} + +func (suite *CacheSuite) TestNew() { + suite.Assert().Equal(0, suite.cache.Len()) + suite.Assert().Equal(2, suite.cache.Capacity()) +} + +func (suite *CacheSuite) TestClosing() { + suite.Assert().NoError(suite.cache.Close()) + + // set/get do nothing after closing + suite.cache.Set(1, "one") + suite.Assert().Equal(0, suite.cache.Len()) + + // getting a value does nothing, returns false + _, ok := suite.cache.Get(1) + suite.Assert().False(ok) + + // delete does nothing + suite.Assert().False(suite.cache.Delete(1)) + + // closing again does nothing + suite.Assert().NoError(suite.cache.Close()) +} diff --git a/go/appencryption/pkg/cache/internal/LICENSE b/go/appencryption/pkg/cache/internal/LICENSE new file mode 100644 index 000000000..1ab653f38 --- /dev/null +++ b/go/appencryption/pkg/cache/internal/LICENSE @@ -0,0 +1,26 @@ +Copyright (c) 2016, Quoc-Viet Nguyen. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +3. Neither the names of the copyright holders nor the names of its contributors +may be used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/go/appencryption/pkg/cache/internal/doc.go b/go/appencryption/pkg/cache/internal/doc.go new file mode 100644 index 000000000..2615d20ba --- /dev/null +++ b/go/appencryption/pkg/cache/internal/doc.go @@ -0,0 +1,7 @@ +// Package internal contains data structures used by cache implementations. +// +// These data structures are derived from the [Mango Cache] source code. +// See LICENSE for important copyright and licensing information. +// +// [Mango Cache]: https://github.com/goburrow/cache +package internal diff --git a/go/appencryption/pkg/cache/internal/filter.go b/go/appencryption/pkg/cache/internal/filter.go new file mode 100644 index 000000000..4eefbb671 --- /dev/null +++ b/go/appencryption/pkg/cache/internal/filter.go @@ -0,0 +1,103 @@ +package internal + +import ( + "math" +) + +// BloomFilter is Bloom Filter implementation used as a cache admission policy. +// See http://billmill.org/bloomfilter-tutorial/ +type BloomFilter struct { + numHashes uint32 // number of hashes per element + bitsMask uint32 // size of bit vector + bits []uint64 // filter bit vector +} + +// Init initializes bloomFilter with the given expected insertions ins and +// false positive probability fpp. +func (f *BloomFilter) Init(ins int, fpp float64) { + ln2 := math.Log(2.0) + factor := -math.Log(fpp) / (ln2 * ln2) + + numBits := nextPowerOfTwo(uint32(float64(ins) * factor)) + if numBits == 0 { + numBits = 1 + } + + f.bitsMask = numBits - 1 + + if ins == 0 { + f.numHashes = 1 + } else { + f.numHashes = uint32(ln2 * float64(numBits) / float64(ins)) + } + + if size := int(numBits+63) / 64; len(f.bits) != size { + f.bits = make([]uint64, size) + } else { + f.Reset() + } +} + +// nextPowerOfTwo returns the smallest power of two which is greater than or equal to i. +func nextPowerOfTwo(i uint32) uint32 { + n := i - 1 + n |= n >> 1 + n |= n >> 2 + n |= n >> 4 + n |= n >> 8 + n |= n >> 16 + n++ + + return n +} + +// Put inserts a hash value into the bloom filter. +// It returns true if the value may already in the filter. +func (f *BloomFilter) Put(h uint64) bool { + h1, h2 := uint32(h), uint32(h>>32) + + var o uint = 1 + for i := uint32(0); i < f.numHashes; i++ { + o &= f.set((h1 + (i * h2)) & f.bitsMask) + } + + return o == 1 +} + +// contains returns true if the given hash is may be in the filter. +func (f *BloomFilter) Contains(h uint64) bool { + h1, h2 := uint32(h), uint32(h>>32) + + var o uint = 1 + for i := uint32(0); i < f.numHashes; i++ { + o &= f.get((h1 + (i * h2)) & f.bitsMask) + } + + return o == 1 +} + +// set sets bit at index i and returns previous value. +func (f *BloomFilter) set(i uint32) uint { + idx, shift := i/64, i%64 + val := f.bits[idx] + mask := uint64(1) << shift + f.bits[idx] |= mask + + return uint((val & mask) >> shift) +} + +// get returns bit set at index i. +func (f *BloomFilter) get(i uint32) uint { + idx, shift := i/64, i%64 + val := f.bits[idx] + mask := uint64(1) << shift + + return uint((val & mask) >> shift) +} + +// Reset clears the bloom filter. +func (f *BloomFilter) Reset() { + for i := range f.bits { + f.bits[i] = 0 + } +} diff --git a/go/appencryption/pkg/cache/internal/filter_test.go b/go/appencryption/pkg/cache/internal/filter_test.go new file mode 100644 index 000000000..8d1e94d3c --- /dev/null +++ b/go/appencryption/pkg/cache/internal/filter_test.go @@ -0,0 +1,43 @@ +package internal_test + +import ( + "testing" + + "github.com/godaddy/asherah/go/appencryption/pkg/cache/internal" +) + +func TestBloomFilter(t *testing.T) { + const numIns = 100000 + + f := internal.BloomFilter{} + f.Init(numIns, 0.01) + + var i uint64 + for i = 0; i < numIns; i += 2 { + existed := f.Put(i) + if existed { + t.Fatalf("unexpected put(%d): %v, want: false", i, existed) + } + } + + for i = 0; i < numIns; i += 2 { + existed := f.Contains(i) + if !existed { + t.Fatalf("unexpected contains(%d): %v, want: true", i, existed) + } + } + + for i = 1; i < numIns; i += 2 { + existed := f.Contains(i) + if existed { + t.Fatalf("unexpected contains(%d): %v, want: false", i, existed) + } + } + + for i = 0; i < numIns; i += 2 { + existed := f.Put(i) + if !existed { + t.Fatalf("unexpected put(%d): %v, want: true", i, existed) + } + } +} diff --git a/go/appencryption/pkg/cache/internal/hash.go b/go/appencryption/pkg/cache/internal/hash.go new file mode 100644 index 000000000..fd0ce5e8b --- /dev/null +++ b/go/appencryption/pkg/cache/internal/hash.go @@ -0,0 +1,130 @@ +package internal + +import ( + "math" + "reflect" +) + +// Hash is an interface implemented by cache keys to +// override default hash function. +type Hash interface { + Sum64() uint64 +} + +// ComputeHash calculates hash value of the given key. +// +//nolint:gocyclo +func ComputeHash(k interface{}) uint64 { + switch h := k.(type) { + case Hash: + return h.Sum64() + case int: + return hashU64(uint64(h)) + case int8: + return hashU32(uint32(h)) + case int16: + return hashU32(uint32(h)) + case int32: + return hashU32(uint32(h)) + case int64: + return hashU64(uint64(h)) + case uint: + return hashU64(uint64(h)) + case uint8: + return hashU32(uint32(h)) + case uint16: + return hashU32(uint32(h)) + case uint32: + return hashU32(h) + case uint64: + return hashU64(h) + case uintptr: + return hashU64(uint64(h)) + case float32: + return hashU32(math.Float32bits(h)) + case float64: + return hashU64(math.Float64bits(h)) + case bool: + if h { + return 1 + } + + return 0 + case string: + return hashString(h) + } + // TODO: complex64 and complex128 + if h, ok := hashPointer(k); ok { + return h + } + // TODO: use gob to encode k to bytes then hash. + return 0 +} + +const ( + fnvOffset uint64 = 14695981039346656037 + fnvPrime uint64 = 1099511628211 +) + +func hashU64(v uint64) uint64 { + // Inline code from hash/fnv to reduce memory allocations + h := fnvOffset + // for i := uint(0); i < 64; i += 8 { + // h ^= (v >> i) & 0xFF + // h *= fnvPrime + // } + h ^= (v >> 0) & 0xFF + h *= fnvPrime + h ^= (v >> 8) & 0xFF + h *= fnvPrime + h ^= (v >> 16) & 0xFF + h *= fnvPrime + h ^= (v >> 24) & 0xFF + h *= fnvPrime + h ^= (v >> 32) & 0xFF + h *= fnvPrime + h ^= (v >> 40) & 0xFF + h *= fnvPrime + h ^= (v >> 48) & 0xFF + h *= fnvPrime + h ^= (v >> 56) & 0xFF + h *= fnvPrime + + return h +} + +func hashU32(v uint32) uint64 { + h := fnvOffset + h ^= uint64(v>>0) & 0xFF + h *= fnvPrime + h ^= uint64(v>>8) & 0xFF + h *= fnvPrime + h ^= uint64(v>>16) & 0xFF + h *= fnvPrime + h ^= uint64(v>>24) & 0xFF + h *= fnvPrime + + return h +} + +// hashString calculates hash value using FNV-1a algorithm. +func hashString(data string) uint64 { + // Inline code from hash/fnv to reduce memory allocations + h := fnvOffset + for _, b := range data { + h ^= uint64(b) + h *= fnvPrime + } + + return h +} + +func hashPointer(k interface{}) (uint64, bool) { + v := reflect.ValueOf(k) + switch v.Kind() { + case reflect.Ptr, reflect.UnsafePointer, reflect.Func, reflect.Slice, reflect.Map, reflect.Chan: + return hashU64(uint64(v.Pointer())), true + default: + return 0, false + } +} diff --git a/go/appencryption/pkg/cache/internal/hash_test.go b/go/appencryption/pkg/cache/internal/hash_test.go new file mode 100644 index 000000000..89ae484e8 --- /dev/null +++ b/go/appencryption/pkg/cache/internal/hash_test.go @@ -0,0 +1,59 @@ +package internal_test + +import ( + "testing" + + "github.com/stretchr/testify/suite" + + "github.com/godaddy/asherah/go/appencryption/pkg/cache/internal" +) + +type HashSuite struct { + suite.Suite +} + +func TestHashSuite(t *testing.T) { + suite.Run(t, new(HashSuite)) +} + +type hashable struct{} + +func (h hashable) Sum64() uint64 { + return 42 +} + +func (suite *HashSuite) TestComputeHash() { + tests := []struct { + input interface{} + expected uint64 + }{ + {input: -1, expected: 0x8cf51a8bfca3883d}, + {input: int8(-8), expected: 0xc49d767d487ba59e}, + {input: int16(-16), expected: 0xbff576369e732626}, + {input: int32(-32), expected: 0xfc0775b30ed9a536}, + {input: int64(-64), expected: 0xd1bdb52ab00c8d2}, + {input: uint(1), expected: 0x89cd31291d2aefa4}, + {input: uint8(8), expected: 0x4cfad6c24f7bf87d}, + {input: uint16(16), expected: 0x4cd037050129dd05}, + {input: uint32(32), expected: 0x4dcff574d71681d5}, + {input: uint64(64), expected: 0x6779ba74e3ecc205}, + {input: uintptr(uint64(64)), expected: 0x6779ba74e3ecc205}, + {input: float32(2.5), expected: 0x4cb8767f9d714215}, + {input: float64(2.5), expected: 0xa8ba2032280e4061}, + {input: true, expected: 1}, + {input: "1", expected: 0xaf63ac4c86019afc}, + {input: hashable{}, expected: 42}, + } + + for i, test := range tests { + i := i + suite.Assert().Equal(test.expected, internal.ComputeHash(test.input), "test %d", i) + } +} + +func (suite *HashSuite) TestComputeHashForPointer() { + input := make([]byte, 0) + + h := internal.ComputeHash(input) + suite.Assert().NotEqual(uint64(0), h) +} diff --git a/go/appencryption/pkg/cache/internal/sketch.go b/go/appencryption/pkg/cache/internal/sketch.go new file mode 100644 index 000000000..72ee2f859 --- /dev/null +++ b/go/appencryption/pkg/cache/internal/sketch.go @@ -0,0 +1,91 @@ +package internal + +const sketchDepth = 4 + +// CountMinSketch is an implementation of count-min sketch with 4-bit counters. +// See http://dimacs.rutgers.edu/~graham/pubs/papers/cmsoft.pdf +type CountMinSketch struct { + counters []uint64 + mask uint32 +} + +// init initialize count-min sketch with the given width. +func (c *CountMinSketch) Init(width int) { + // Need (width x 4 x 4) bits = width/4 x uint64 + size := nextPowerOfTwo(uint32(width)) >> 2 + if size < 1 { + size = 1 + } + + c.mask = size - 1 + if len(c.counters) == int(size) { + c.clear() + } else { + c.counters = make([]uint64, size) + } +} + +// Add increases counters associated with the given hash. +func (c *CountMinSketch) Add(h uint64) { + h1, h2 := uint32(h), uint32(h>>32) + + for i := uint32(0); i < sketchDepth; i++ { + idx, off := c.position(h1 + i*h2) + c.inc(idx, (16*i)+off) + } +} + +// Estimate returns minimum value of counters associated with the given hash. +func (c *CountMinSketch) Estimate(h uint64) uint8 { + h1, h2 := uint32(h), uint32(h>>32) + + var min uint8 = 0xFF + + for i := uint32(0); i < sketchDepth; i++ { + idx, off := c.position(h1 + i*h2) + + count := c.val(idx, (16*i)+off) + if count < min { + min = count + } + } + + return min +} + +// Reset divides all counters by two. +func (c *CountMinSketch) Reset() { + for i, v := range c.counters { + if v != 0 { + c.counters[i] = (v >> 1) & 0x7777777777777777 + } + } +} + +func (c *CountMinSketch) position(h uint32) (idx uint32, off uint32) { + idx = (h >> 2) & c.mask + off = (h & 3) << 2 + + return +} + +// inc increases value at index idx. +func (c *CountMinSketch) inc(idx, off uint32) { + v := c.counters[idx] + + if count := uint8(v>>off) & 0x0F; count < 15 { + c.counters[idx] = v + (1 << off) + } +} + +// val returns value at index idx. +func (c *CountMinSketch) val(idx, off uint32) uint8 { + v := c.counters[idx] + return uint8(v>>off) & 0x0F +} + +func (c *CountMinSketch) clear() { + for i := range c.counters { + c.counters[i] = 0 + } +} diff --git a/go/appencryption/pkg/cache/internal/sketch_test.go b/go/appencryption/pkg/cache/internal/sketch_test.go new file mode 100644 index 000000000..ecae31db7 --- /dev/null +++ b/go/appencryption/pkg/cache/internal/sketch_test.go @@ -0,0 +1,67 @@ +package internal_test + +import ( + "testing" + + "github.com/godaddy/asherah/go/appencryption/pkg/cache/internal" +) + +func TestCountMinSketch(t *testing.T) { + const max = 15 + + cm := &internal.CountMinSketch{} + cm.Init(max) + + for i := 0; i < max; i++ { + // Increase value at i j times + for j := i; j > 0; j-- { + cm.Add(uint64(i)) + } + } + + for i := 0; i < max; i++ { + n := cm.Estimate(uint64(i)) + if int(n) != i { + t.Fatalf("unexpected estimate(%d): %d, want: %d", i, n, i) + } + } + + cm.Reset() + + for i := 0; i < max; i++ { + n := cm.Estimate(uint64(i)) + if int(n) != i/2 { + t.Fatalf("unexpected estimate(%d): %d, want: %d", i, n, i/2) + } + } + + cm.Reset() + + for i := 0; i < max; i++ { + n := cm.Estimate(uint64(i)) + if int(n) != i/4 { + t.Fatalf("unexpected estimate(%d): %d, want: %d", i, n, i/4) + } + } + + for i := 0; i < 100; i++ { + cm.Add(1) + } + + if n := cm.Estimate(1); n != 15 { + t.Fatalf("unexpected estimate(%d): %d, want: %d", 1, n, 15) + } +} + +func BenchmarkCountMinSketchReset(b *testing.B) { + cm := &internal.CountMinSketch{} + cm.Init(1<<15 - 1) + + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + cm.Add(0xCAFECAFECAFECAFE) + cm.Reset() + } +} diff --git a/go/appencryption/pkg/cache/lfu.go b/go/appencryption/pkg/cache/lfu.go new file mode 100644 index 000000000..d809cdca1 --- /dev/null +++ b/go/appencryption/pkg/cache/lfu.go @@ -0,0 +1,152 @@ +//nolint:forcetypeassert // we know the type of the value +package cache + +import ( + "container/list" +) + +type frequencyParent[K comparable, V any] struct { + entries map[*cacheItem[K, V]]*list.Element // entries in this frequency to pointer to access list + frequency int + byAccess *list.List // linked list of all entries in access order +} + +// lfu implements a cache policy as described in +// ["An O(1) algorithm for implementing the lfu cache eviction scheme"]. +// +// A cache utilizing this policy is safe for concurrent use and has a +// runtime complexity of O(1) for all operations. +// +// ["An O(1) algorithm for implementing the lfu cache eviction scheme"]: https://arxiv.org/pdf/2110.11602.pdf +type lfu[K comparable, V any] struct { + cap int + frequencies *list.List // Linked list containing all frequencyParents in order of least frequently used +} + +// init initializes the LFU cache policy. +func (c *lfu[K, V]) init(capacity int) { + c.cap = capacity + c.frequencies = list.New() +} + +// capacity returns the capacity of the cache. +func (c *lfu[K, V]) capacity() int { + return c.cap +} + +// access is called when an item is accessed in the cache. It increments the +// frequency of the item. +func (c *lfu[K, V]) access(item *cacheItem[K, V]) { + c.increment(item) +} + +// admit is called when an item is added to the cache. It increments the +// frequency of the item. +func (c *lfu[K, V]) admit(item *cacheItem[K, V]) { + c.increment(item) +} + +// remove is called when an item is removed from the cache. It removes the item +// from the frequency. +func (c *lfu[K, V]) remove(item *cacheItem[K, V]) { + c.delete(item.parent, item) +} + +// victim returns the least frequently used item in the cache. +func (c *lfu[K, V]) victim() *cacheItem[K, V] { + if frequency := c.frequencies.Front(); frequency != nil { + elem := frequency.Value.(*frequencyParent[K, V]).byAccess.Front() + if elem != nil { + return elem.Value.(*cacheItem[K, V]) + } + } + + return nil +} + +// increment the frequency of the given item. If the frequency parent +// does not exist, it is created. +func (c *lfu[K, V]) increment(item *cacheItem[K, V]) { + current := item.parent + + // next will be this item's new parent + var next *list.Element + + // nextAmount will be the new frequency for this item + var nextAmount int + + if current == nil { + // the item has not yet been assigned a frequency so + // this is the first time it is being accessed + nextAmount = 1 + + // set next to the first frequency + next = c.frequencies.Front() + } else { + // increment the access frequency for the item + nextAmount = current.Value.(*frequencyParent[K, V]).frequency + 1 + + // set next to the next greater frequency + next = current.Next() + } + + // if the next frequency does not exist or the next frequency is not the + // next frequency amount, create a new frequency item and insert it + // after the current frequency + if next == nil || next.Value.(*frequencyParent[K, V]).frequency != nextAmount { + newFrequencyParent := &frequencyParent[K, V]{ + entries: make(map[*cacheItem[K, V]]*list.Element), + frequency: nextAmount, + byAccess: list.New(), + } + + if current == nil { + // current is nil so insert the new frequency item at the front + next = c.frequencies.PushFront(newFrequencyParent) + } else { + // otherwise insert the new frequency item after the current + next = c.frequencies.InsertAfter(newFrequencyParent, current) + } + } + + // set the item's parent to the next frequency + item.parent = next + + // add the item to the frequency's access list + nextAccess := next.Value.(*frequencyParent[K, V]).byAccess.PushBack(item) + + // add the item to the frequency's entries with a pointer to the access list + next.Value.(*frequencyParent[K, V]).entries[item] = nextAccess + + // if the item was previously assigned a frequency, remove it from the + // old frequency's entries + if current != nil { + c.delete(current, item) + } +} + +// delete removes the given item from the frequency and removes the frequency +// if it is empty. +func (c *lfu[K, V]) delete(frequency *list.Element, item *cacheItem[K, V]) { + frequencyParent := frequency.Value.(*frequencyParent[K, V]) + + // remove the item from the frequency's access list + frequencyParent.byAccess.Remove(frequencyParent.entries[item]) + + // remove the item from the frequency's entries + delete(frequencyParent.entries, item) + + if len(frequencyParent.entries) == 0 { + frequencyParent.entries = nil + frequencyParent.byAccess = nil + + c.frequencies.Remove(frequency) + } +} + +// close removes all items from the cache, sends a close event on the events +// channel, and waits for the cache to close. +func (c *lfu[K, V]) close() { + c.frequencies = nil + c.cap = 0 +} diff --git a/go/appencryption/pkg/cache/lfu_example_test.go b/go/appencryption/pkg/cache/lfu_example_test.go new file mode 100644 index 000000000..7ca28fc26 --- /dev/null +++ b/go/appencryption/pkg/cache/lfu_example_test.go @@ -0,0 +1,44 @@ +package cache_test + +import ( + "fmt" + + "github.com/godaddy/asherah/go/appencryption/pkg/cache" +) + +func ExampleNew() { + evictionMsg := make(chan string) + + // Create a new LFU cache with a capacity of 3 items and an eviction callback. + cache := cache.New[int, string](3, cache.WithPolicy[int, string](cache.LFU), cache.WithEvictFunc(func(key int, value string) { + // This callback is executed via a background goroutine whenever an + // item is evicted from the cache. We use a channel to synchronize + // the goroutine with this example function so we can verify the + // item that was evicted. + evictionMsg <- fmt.Sprintln("evicted:", key, value) + })) + + // Add some items to the cache. + cache.Set(1, "foo") + cache.Set(2, "bar") + cache.Set(3, "baz") + + // Get an item from the cache. + value, ok := cache.Get(1) + if ok { + fmt.Println("got:", value) + } + + // Set a new value for an existing key + cache.Set(2, "two") + + // Add another item to the cache which will evict the least frequently used + // item (3). + cache.Set(4, "qux") + + // Print the eviction message sent via the callback above. + fmt.Print(<-evictionMsg) + // Output: + // got: foo + // evicted: 3 baz +} diff --git a/go/appencryption/pkg/cache/lfu_test.go b/go/appencryption/pkg/cache/lfu_test.go new file mode 100644 index 000000000..a7aad1492 --- /dev/null +++ b/go/appencryption/pkg/cache/lfu_test.go @@ -0,0 +1,201 @@ +package cache_test + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/suite" + + "github.com/godaddy/asherah/go/appencryption/pkg/cache" +) + +type LFUSuite struct { + suite.Suite + cache cache.Interface[int, string] +} + +func TestLFUSuite(t *testing.T) { + suite.Run(t, new(LFUSuite)) +} + +func (suite *LFUSuite) SetupTest() { + suite.cache = cache.New[int, string](2, cache.WithPolicy[int, string](cache.LFU)) +} + +func (suite *LFUSuite) TestNewLFU() { + suite.Assert().Equal(0, suite.cache.Len()) + suite.Assert().Equal(2, suite.cache.Capacity()) +} + +func (suite *LFUSuite) TestSet() { + suite.cache.Set(1, "one") + suite.Assert().Equal(1, suite.cache.Len()) + + suite.cache.Set(2, "two") + suite.Assert().Equal(2, suite.cache.Len()) + + suite.cache.Set(3, "three") + suite.Assert().Equal(2, suite.cache.Len()) +} + +func (suite *LFUSuite) TestGet() { + suite.cache.Set(1, "one") + suite.cache.Set(2, "two") + + one, ok := suite.cache.Get(1) + suite.Assert().Equal("one", one) + suite.Assert().True(ok) + + two, ok := suite.cache.Get(2) + suite.Assert().Equal("two", two) + suite.Assert().True(ok) + + val, ok := suite.cache.Get(3) + suite.Assert().False(ok) + suite.Assert().Equal("", val) +} + +func (suite *LFUSuite) TestGetOrPanic() { + suite.cache.Set(1, "one") + suite.cache.Set(2, "two") + + suite.Assert().Equal("one", suite.cache.GetOrPanic(1)) + suite.Assert().Equal("two", suite.cache.GetOrPanic(2)) + + suite.Assert().Panics(func() { suite.cache.GetOrPanic(3) }) +} + +func (suite *LFUSuite) TestDelete() { + suite.cache.Set(1, "one") + suite.cache.Set(2, "two") + + suite.Assert().Equal(2, suite.cache.Len()) + + // ensure the key is deleted and the size is decremented + ok := suite.cache.Delete(1) + suite.Assert().True(ok) + suite.Assert().Equal(1, suite.cache.Len()) + + // subsequent delete should return false + ok = suite.cache.Delete(1) + suite.Assert().False(ok) + + // ensure the key is no longer in the cache + one, ok := suite.cache.Get(1) + suite.Assert().Equal("", one) + suite.Assert().False(ok) + + suite.cache.Delete(2) + suite.Assert().Equal(0, suite.cache.Len()) +} + +// func (suite *LFUSuite) TestEach() { +// suite.cache.Set(1, "one") +// suite.cache.Set(1, "one") // increment frequency +// suite.cache.Set(2, "two") +// suite.cache.Set(3, "three") // evict 2 + +// var ( +// keys []int +// values []string +// ) + +// suite.cache.Each(func(key int, value string) bool { +// keys = append(keys, key) +// values = append(values, value) + +// return true +// }) + +// // Each() iterates in order of least frequently used +// suite.Assert().Equal([]int{3, 1}, keys) +// suite.Assert().Equal([]string{"three", "one"}, values) +// } + +// func (suite *LFUSuite) TestEachWithEarlyExit() { +// suite.cache.Set(1, "one") +// suite.cache.Set(1, "one") // increment frequency +// suite.cache.Set(2, "two") +// suite.cache.Set(3, "three") // evict 2 + +// var ( +// keys []int +// values []string +// ) + +// suite.cache.Each(func(key int, value string) bool { +// keys = append(keys, key) +// values = append(values, value) + +// // early exit +// return false +// }) + +// // Each() iterates in order of least frequently used +// suite.Assert().Equal([]int{3}, keys) +// suite.Assert().Equal([]string{"three"}, values) +// } + +func (suite *LFUSuite) TestEviction() { + suite.cache.Set(1, "one") + suite.cache.Set(2, "two") + + // access 1 to increase frequency + suite.cache.Set(1, "one") + + suite.cache.Set(3, "three") + + _, ok := suite.cache.Get(1) + suite.Assert().True(ok) + + // 2 should be evicted as it has the lowest frequency + _, ok = suite.cache.Get(2) + suite.Assert().False(ok) + + _, ok = suite.cache.Get(3) + suite.Assert().True(ok) +} + +func (suite *LFUSuite) TestClose() { + suite.cache.Set(1, "one") + suite.cache.Set(2, "two") + + suite.cache.Close() + + suite.Assert().Equal(0, suite.cache.Len()) + suite.Assert().Equal(0, suite.cache.Capacity()) +} + +func (suite *LFUSuite) TestWithEvictFunc() { + evicted := map[int]int{} + + suite.cache = cache.New[int, string]( + 100, + cache.WithPolicy[int, string](cache.LFU), + cache.WithEvictFunc(func(key int, _ string) { + evicted[key] = 1 + }), + ) + + // overfill the cache + for i := 0; i < 105; i++ { + suite.cache.Set(i, fmt.Sprintf("value-%d", i)) + } + + // wait for the background goroutine to evict items + suite.Assert().Eventually(func() bool { + return len(evicted) == 5 + }, 100*time.Millisecond, 10*time.Millisecond, "eviction callback was not called") + + // verify the first five items were evicted + for i := 0; i < 5; i++ { + suite.Assert().Contains(evicted, i) + } + + // close the cache and evict the remaining items + suite.cache.Close() + + suite.Assert().Equal(0, suite.cache.Len()) + suite.Assert().Equal(105, len(evicted)) +} diff --git a/go/appencryption/pkg/cache/lru.go b/go/appencryption/pkg/cache/lru.go new file mode 100644 index 000000000..301bd062f --- /dev/null +++ b/go/appencryption/pkg/cache/lru.go @@ -0,0 +1,168 @@ +//nolint:forcetypeassert // we know the type of the value +package cache + +import ( + "container/list" +) + +// lru is a least recently used cache policy implementation. +type lru[K comparable, V any] struct { + cap int + evictList *list.List +} + +// init initializes the LRU cache policy. +func (c *lru[K, V]) init(capacity int) { + c.cap = capacity + c.evictList = list.New() +} + +// capacity returns the capacity of the cache. +func (c *lru[K, V]) capacity() int { + return c.cap +} + +// len returns the number of items in the cache. +func (c *lru[K, V]) len() int { + return c.evictList.Len() +} + +// access is called when an item is accessed in the cache. It moves the item to +// the front of the eviction list. +func (c *lru[K, V]) access(item *cacheItem[K, V]) { + c.evictList.MoveToFront(item.parent) +} + +// admit is called when an item is added to the cache. It adds the item to the +// front of the eviction list. +func (c *lru[K, V]) admit(item *cacheItem[K, V]) { + item.parent = c.evictList.PushFront(item) +} + +// remove is called when an item is removed from the cache. It removes the item +// from the eviction list. +func (c *lru[K, V]) remove(item *cacheItem[K, V]) { + c.evictList.Remove(item.parent) +} + +// victim returns the least recently used item in the cache. +func (c *lru[K, V]) victim() *cacheItem[K, V] { + oldest := c.evictList.Back() + if oldest == nil { + return nil + } + + return oldest.Value.(*cacheItem[K, V]) +} + +// close implements the policy interface. +func (c *lru[K, V]) close() { + c.evictList = nil + c.cap = 0 +} + +const protectedRatio = 0.8 + +// slruItem is an item in the SLRU cache. +type slruItem[K comparable, V any] struct { + *cacheItem[K, V] + protected bool +} + +// slru is a Segmented LRU cache policy implementation. +type slru[K comparable, V any] struct { + cap int + + protectedCapacity int + protectedList *list.List + + probationCapacity int + probationList *list.List +} + +// init initializes the SLRU cache policy. +func (c *slru[K, V]) init(capacity int) { + c.cap = capacity + + c.protectedList = list.New() + c.probationList = list.New() + + c.protectedCapacity = int(float64(capacity) * protectedRatio) + c.probationCapacity = capacity - c.protectedCapacity +} + +// capacity returns the capacity of the cache. +func (c *slru[K, V]) capacity() int { + return c.cap +} + +// access is called when an item is accessed in the cache. It moves the item to +// the front of its respective eviction list. +func (c *slru[K, V]) access(item *cacheItem[K, V]) { + sitem := item.parent.Value.(*slruItem[K, V]) + if sitem.protected { + c.protectedList.MoveToFront(item.parent) + return + } + + // must be in probation list, promote to protected list + sitem.protected = true + + c.probationList.Remove(item.parent) + + item.parent = c.protectedList.PushFront(sitem) + + // if the protected list is too big, demote the oldest item to the probation list + if c.protectedList.Len() > c.protectedCapacity { + b := c.protectedList.Back() + c.protectedList.Remove(b) + + bitem := b.Value.(*slruItem[K, V]) + bitem.protected = false + + bitem.parent = c.probationList.PushFront(bitem) + } +} + +// admit is called when an item is added to the cache. It adds the item to the +// front of the probation list. +func (c *slru[K, V]) admit(item *cacheItem[K, V]) { + newItem := &slruItem[K, V]{ + cacheItem: item, + protected: false, + } + + item.parent = c.probationList.PushFront(newItem) +} + +// victim returns the least recently used item in the cache. +func (c *slru[K, V]) victim() *cacheItem[K, V] { + if c.probationList.Len() > 0 { + return c.probationList.Back().Value.(*slruItem[K, V]).cacheItem + } + + if c.protectedList.Len() > 0 { + return c.protectedList.Back().Value.(*slruItem[K, V]).cacheItem + } + + return nil +} + +// remove is called when an item is removed from the cache. It removes the item +// from the eviction list. +func (c *slru[K, V]) remove(item *cacheItem[K, V]) { + sitem := item.parent.Value.(*slruItem[K, V]) + if sitem.protected { + c.protectedList.Remove(item.parent) + return + } + + c.probationList.Remove(item.parent) +} + +// close implements the policy interface. +func (c *slru[K, V]) close() { + c.protectedList = nil + c.probationList = nil + c.cap = 0 +} diff --git a/go/appencryption/pkg/cache/lru_test.go b/go/appencryption/pkg/cache/lru_test.go new file mode 100644 index 000000000..c126d11fb --- /dev/null +++ b/go/appencryption/pkg/cache/lru_test.go @@ -0,0 +1,284 @@ +package cache_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/suite" + + "github.com/godaddy/asherah/go/appencryption/pkg/cache" +) + +type LRUSuite struct { + suite.Suite + cache cache.Interface[int, string] +} + +func TestLRUSuite(t *testing.T) { + suite.Run(t, new(LRUSuite)) +} + +func (suite *LRUSuite) SetupTest() { + suite.cache = cache.New[int, string](10) +} + +func (suite *LRUSuite) TestNewLRU() { + suite.Assert().Equal(0, suite.cache.Len()) + suite.Assert().Equal(10, suite.cache.Capacity()) +} + +func (suite *LRUSuite) TestSet() { + // fill to capacity + for i := 0; i < suite.cache.Capacity(); i++ { + suite.cache.Set(i, fmt.Sprintf("#%d", i)) + } + + // verify size + suite.Assert().Equal(suite.cache.Capacity(), suite.cache.Len()) +} + +func (suite *LRUSuite) TestGet() { + suite.cache.Set(1, "one") + suite.cache.Set(2, "two") + + one, ok := suite.cache.Get(1) + suite.Assert().Equal("one", one) + suite.Assert().True(ok) + + two, ok := suite.cache.Get(2) + suite.Assert().Equal("two", two) + suite.Assert().True(ok) + + val, ok := suite.cache.Get(3) + suite.Assert().False(ok) + suite.Assert().Equal("", val) +} + +func (suite *LRUSuite) TestGetOrPanic() { + suite.cache.Set(1, "one") + suite.cache.Set(2, "two") + + one := suite.cache.GetOrPanic(1) + suite.Assert().Equal("one", one) + + two := suite.cache.GetOrPanic(2) + suite.Assert().Equal("two", two) + + suite.Assert().Panics(func() { suite.cache.GetOrPanic(3) }) +} + +func (suite *LRUSuite) TestDelete() { + suite.cache.Set(1, "one") + suite.cache.Set(2, "two") + + suite.Assert().Equal(2, suite.cache.Len()) + + suite.cache.Delete(1) + suite.Assert().Equal(1, suite.cache.Len()) + + suite.cache.Delete(2) + suite.Assert().Equal(0, suite.cache.Len()) +} + +func (suite *LRUSuite) TestClose() { + suite.cache.Set(1, "one") + suite.cache.Set(2, "two") + + suite.Assert().Equal(2, suite.cache.Len()) + + suite.cache.Close() + suite.Assert().Equal(0, suite.cache.Len()) +} + +func (suite *LRUSuite) TestEviction() { + // fill the cache to capacity + for i := 0; i < suite.cache.Capacity(); i++ { + suite.cache.Set(i, fmt.Sprintf("#%d", i)) + } + + // access the first item to make it the most recently used + suite.cache.Get(0) + + // add a new item to the cache + suite.cache.Set(10, "#10") + + // the least recently used item should have been evicted + _, ok := suite.cache.Get(1) + suite.Assert().False(ok) + + // the most recently used item should still be in the cache + _, ok = suite.cache.Get(0) + suite.Assert().True(ok) + + suite.Assert().Equal(10, suite.cache.Len()) +} + +func (suite *LRUSuite) TestWithEvictFunc() { + done := make(chan struct{}) + + evicted := false + cache := cache.New[int, string](1, cache.WithEvictFunc(func(key int, value string) { + evicted = true + + suite.Assert().Equal(1, key) + suite.Assert().Equal("one", value) + + close(done) + })) + + cache.Set(1, "one") + cache.Set(2, "two") + + <-done + + suite.Assert().True(evicted) + suite.Assert().Equal(1, cache.Len()) +} + +type SLRUSuite struct { + suite.Suite + cache cache.Interface[int, string] +} + +func TestSLRUSuite(t *testing.T) { + suite.Run(t, new(SLRUSuite)) +} + +func (suite *SLRUSuite) SetupTest() { + suite.cache = cache.New[int, string](10, cache.WithPolicy[int, string](cache.SLRU)) +} + +func (suite *SLRUSuite) TestNewSLRU() { + suite.Assert().Equal(0, suite.cache.Len()) + suite.Assert().Equal(10, suite.cache.Capacity()) +} + +func (suite *SLRUSuite) TestSet() { + suite.cache.Set(1, "one") + suite.Assert().Equal(1, suite.cache.Len()) + + suite.cache.Set(2, "two") + suite.Assert().Equal(2, suite.cache.Len()) + + suite.cache.Set(3, "three") + suite.Assert().Equal(3, suite.cache.Len()) +} + +func (suite *SLRUSuite) TestGet() { + suite.cache.Set(1, "one") + suite.cache.Set(2, "two") + + one, ok := suite.cache.Get(1) + suite.Assert().Equal("one", one) + suite.Assert().True(ok) + + two, ok := suite.cache.Get(2) + suite.Assert().Equal("two", two) + suite.Assert().True(ok) + + val, ok := suite.cache.Get(3) + suite.Assert().False(ok) + suite.Assert().Equal("", val) +} + +func (suite *SLRUSuite) TestGetOrPanic() { + suite.cache.Set(1, "one") + suite.cache.Set(2, "two") + + one := suite.cache.GetOrPanic(1) + suite.Assert().Equal("one", one) + + two := suite.cache.GetOrPanic(2) + suite.Assert().Equal("two", two) + + suite.Assert().Panics(func() { suite.cache.GetOrPanic(3) }) +} + +func (suite *SLRUSuite) TestDelete() { + suite.cache.Set(1, "one") + suite.cache.Set(2, "two") + + suite.Assert().Equal(2, suite.cache.Len()) + + suite.cache.Delete(1) + suite.Assert().Equal(1, suite.cache.Len()) + + suite.cache.Delete(2) + suite.Assert().Equal(0, suite.cache.Len()) +} + +func (suite *SLRUSuite) TestClose() { + suite.cache.Set(1, "one") + suite.cache.Set(2, "two") + + // throw in a get for good measure + suite.cache.Get(1) + + suite.Assert().Equal(2, suite.cache.Len()) + + suite.cache.Close() + suite.Assert().Equal(0, suite.cache.Len()) +} + +func (suite *SLRUSuite) TestCloseEmpty() { + suite.cache.Close() +} + +func (suite *SLRUSuite) TestEviction() { + // fill the cache to capacity + for i := 0; i < suite.cache.Capacity(); i++ { + suite.cache.Set(i, fmt.Sprintf("#%d", i)) + } + + // access the first item to make it the most recently used + suite.cache.Get(0) + + // add a new item to the cache + suite.cache.Set(10, "#10") + + // the least recently used item should have been evicted + _, ok := suite.cache.Get(1) + suite.Assert().False(ok) + + // the most recently used item should still be in the cache + _, ok = suite.cache.Get(0) + suite.Assert().True(ok) + + // verify other items are still in the cache + for i := 2; i < suite.cache.Capacity(); i++ { + _, ok := suite.cache.Get(i) + suite.Assert().True(ok) + } + + suite.Assert().Equal(10, suite.cache.Len()) +} + +func (suite *SLRUSuite) TestWithEvictFunc() { + done := make(chan struct{}) + + evicted := false + cache := cache.New[int, string](10, cache.WithPolicy[int, string](cache.SLRU), cache.WithEvictFunc(func(key int, value string) { + evicted = true + + suite.Assert().Equal(1, key) + suite.Assert().Equal("#1", value) + + close(done) + })) + + // fill the cache to capacity + for i := 0; i < cache.Capacity(); i++ { + cache.Set(i, fmt.Sprintf("#%d", i)) + } + + // access the first item to make it the most recently used + cache.Get(0) + + // add a new item to the cache + cache.Set(10, "#10") + + <-done + + suite.Assert().True(evicted) + suite.Assert().Equal(10, cache.Len()) +} diff --git a/go/appencryption/pkg/cache/tlfu.go b/go/appencryption/pkg/cache/tlfu.go new file mode 100644 index 000000000..3cb87d6ab --- /dev/null +++ b/go/appencryption/pkg/cache/tlfu.go @@ -0,0 +1,202 @@ +package cache + +import ( + "github.com/godaddy/asherah/go/appencryption/pkg/cache/internal" +) + +const ( + samplesMultiplier = 8 + insertionsMultiplier = 2 + countersMultiplier = 1 + falsePositiveProbability = 0.1 + admissionRatio = 0.01 +) + +// tinyLFUEntry is an entry in the tinyLFU cache. +type tinyLFUEntry[K comparable, V any] struct { + hash uint64 + parent policy[K, V] +} + +// tinyLFU is a tiny LFU cache policy implementation derived from +// [Mango Cache] and based on the algorithm described in the paper +// ["TinyLFU: A Highly Efficient Cache Admission Policy"] by Gil Einziger, +// Roy Friedman, and Ben Manes. +// +// [Mango Cache]: https://github.com/goburrow/cache +// ["TinyLFU: A Highly Efficient Cache Admission Policy"]: https://arxiv.org/pdf/1512.00727v2.pdf +type tinyLFU[K comparable, V any] struct { + cap int + + filter internal.BloomFilter // 1bit counter + counter internal.CountMinSketch // 4bit counter + + additions int + samples int + + lru lru[K, V] + slru slru[K, V] + + keys map[K]tinyLFUEntry[K, V] // Hashmap containing *tinyLFUEntry for O(1) access +} + +// init initializes the tinyLFU cache policy. +func (c *tinyLFU[K, V]) init(capacity int) { + c.cap = capacity + + c.keys = make(map[K]tinyLFUEntry[K, V]) + + c.samples = capacity * samplesMultiplier + + c.filter.Init(capacity*insertionsMultiplier, falsePositiveProbability) + c.counter.Init(capacity * countersMultiplier) + + // The admission window is a fixed percentage of the cache capacity. + // The LRU is the first part of the admission window, and the SLRU is + // the second part. + // + // Note that for small cache sizes the admission window may be 0, in which + // case the SLRU is the entire cache and the doorkeeper is not used. + lruCap := int(float64(capacity) * admissionRatio) + c.lru.init(lruCap) + + slruCap := capacity - lruCap + c.slru.init(slruCap) +} + +// capacity returns the capacity of the cache. +func (c *tinyLFU[K, V]) capacity() int { + return c.cap +} + +// access is called when an item is accessed in the cache. It increments the +// frequency of the item. +func (c *tinyLFU[K, V]) access(item *cacheItem[K, V]) { + c.increment(item) + + c.keys[item.key].parent.access(item) +} + +// admit is called when an item is added to the cache. It increments the +// frequency of the item. +func (c *tinyLFU[K, V]) admit(item *cacheItem[K, V]) { + if c.bypassed() { + c.slru.admit(item) + return + } + + c.increment(item) + + // If there's room in the admission window, add it to the LRU + if c.lru.len() < c.lru.cap { + c.admitTo(item, &c.lru) + + return + } + + victim := c.lru.victim() + + // Otherwise, promote the victim from the LRU to the SLRU + c.lru.remove(victim) + c.admitTo(victim, &c.slru) + + // then add the new item to the LRU + c.admitTo(item, &c.lru) +} + +// bypassed returns true if the doorkeeper is not in use. +func (c *tinyLFU[K, V]) bypassed() bool { + return c.lru.cap == 0 +} + +// admitTo adds the item to the provided eviction list. +func (c *tinyLFU[K, V]) admitTo(item *cacheItem[K, V], list policy[K, V]) { + list.admit(item) + + c.keys[item.key] = tinyLFUEntry[K, V]{ + hash: internal.ComputeHash(item.key), + parent: list, + } +} + +// victim returns the victim item to be evicted. +func (c *tinyLFU[K, V]) victim() *cacheItem[K, V] { + candidate := c.lru.victim() + + // If the LRU is empty, just return the SLRU victim. + // This is the case when the cache is closing and + // the items are being purged. + if candidate == nil { + return c.slru.victim() + } + + victim := c.slru.victim() + + // If the SLRU is empty, just return the LRU victim. + if victim == nil { + return candidate + } + + // we have both a candidate and a victim + // ...may the best item win! + candidateFreq := c.estimate(c.keys[candidate.key].hash) + victimFreq := c.estimate(c.keys[victim.key].hash) + + // If the candidate is more frequently accessed than the victim, + // remove the candidate from the LRU and add it to the SLRU. + if candidateFreq > victimFreq { + c.lru.remove(candidate) + + c.admitTo(candidate, &c.slru) + + return victim + } + + return candidate +} + +// estimate returns the estimated frequency of the item. +func (c *tinyLFU[K, V]) estimate(h uint64) uint8 { + freq := c.counter.Estimate(h) + if c.filter.Contains(h) { + freq++ + } + + return freq +} + +// remove is called when an item is removed from the cache. It removes the item +// from the appropriate eviction list. +func (c *tinyLFU[K, V]) remove(item *cacheItem[K, V]) { + c.keys[item.key].parent.remove(item) +} + +// increment increments the frequency of the item. +func (c *tinyLFU[K, V]) increment(item *cacheItem[K, V]) { + if c.bypassed() { + return + } + + c.additions++ + + if c.additions >= c.samples { + c.filter.Reset() + c.counter.Reset() + + c.additions = 0 + } + + k := c.keys[item.key] + + if c.filter.Put(k.hash) { + c.counter.Add(k.hash) + } +} + +// close removes all items from the cache. +func (c *tinyLFU[K, V]) close() { + c.lru.close() + c.slru.close() + + c.cap = 0 +} diff --git a/go/appencryption/pkg/cache/tlfu_test.go b/go/appencryption/pkg/cache/tlfu_test.go new file mode 100644 index 000000000..d1ae5a3b5 --- /dev/null +++ b/go/appencryption/pkg/cache/tlfu_test.go @@ -0,0 +1,134 @@ +package cache_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/suite" + + "github.com/godaddy/asherah/go/appencryption/pkg/cache" +) + +type TinyLFUSuite struct { + suite.Suite + cache cache.Interface[int, string] +} + +func TestTinyLFUSuite(t *testing.T) { + // t.SkipNow() + suite.Run(t, new(TinyLFUSuite)) +} + +func (suite *TinyLFUSuite) SetupTest() { + suite.cache = cache.New[int, string](100, cache.WithPolicy[int, string](cache.TinyLFU)) +} + +func (suite *TinyLFUSuite) TestNewTinyLFU() { + suite.Assert().Equal(0, suite.cache.Len()) + suite.Assert().Equal(100, suite.cache.Capacity()) +} + +func (suite *TinyLFUSuite) TestSet() { + // fill cache + for i := 0; i < suite.cache.Capacity(); i++ { + suite.cache.Set(i, fmt.Sprintf("%d", i)) + } + + suite.Assert().Equal(suite.cache.Capacity(), suite.cache.Len()) + + // add one more + suite.cache.Set(100, "one hundred") + suite.Assert().Equal(suite.cache.Capacity(), suite.cache.Len()) +} + +func (suite *TinyLFUSuite) TestGet() { + suite.cache.Set(1, "one") + suite.cache.Set(2, "two") + + one, ok := suite.cache.Get(1) + suite.Assert().Equal("one", one) + suite.Assert().True(ok) + + two, ok := suite.cache.Get(2) + suite.Assert().Equal("two", two) + suite.Assert().True(ok) + + val, ok := suite.cache.Get(3) + suite.Assert().False(ok) + suite.Assert().Equal("", val) +} + +func (suite *TinyLFUSuite) TestGetOrPanic() { + suite.cache.Set(1, "one") + suite.cache.Set(2, "two") + + one := suite.cache.GetOrPanic(1) + suite.Assert().Equal("one", one) + + two := suite.cache.GetOrPanic(2) + suite.Assert().Equal("two", two) + + suite.Assert().Panics(func() { suite.cache.GetOrPanic(3) }) +} + +func (suite *TinyLFUSuite) TestDelete() { + suite.cache.Set(1, "one") + suite.cache.Set(2, "two") + + suite.Assert().True(suite.cache.Delete(1)) + suite.Assert().Equal(1, suite.cache.Len()) + + suite.Assert().False(suite.cache.Delete(3)) + suite.Assert().Equal(1, suite.cache.Len()) +} + +func (suite *TinyLFUSuite) TestEvict() { + // fill the cache to capacity + for i := 0; i < suite.cache.Capacity(); i++ { + suite.cache.Set(i, fmt.Sprintf("#%d", i)) + } + + // access half of the items + for i := 0; i < suite.cache.Capacity()/2; i++ { + _, ok := suite.cache.Get(i) + suite.Assert().True(ok) + } + + // add one more + suite.cache.Set(999, "nine ninety nine") + + // access the new item + _, ok := suite.cache.Get(999) + suite.Assert().True(ok) + + // verify the cache is at capacity + suite.Assert().Equal(suite.cache.Capacity(), suite.cache.Len()) + + // overwrite half of the items + for i := 0; i < suite.cache.Capacity(); i++ { + key := i + 1000 + suite.cache.Set(key, fmt.Sprintf("##%d", key)) + } + + // verify 999 is still in the cache + _, ok = suite.cache.Get(999) + suite.Assert().True(ok, "item 999 should be in the cache") + + // verify all of the previously accessed items are still in the cache + for i := 0; i < suite.cache.Capacity()/2; i++ { + _, ok := suite.cache.Get(i) + suite.Assert().True(ok, "item %d should be in the cache", i) + } +} + +func (suite *TinyLFUSuite) TestClose() { + suite.cache.Set(1, "one") + suite.cache.Set(2, "two") + + suite.Assert().Equal(2, suite.cache.Len()) + + suite.cache.Close() + + suite.Assert().Equal(0, suite.cache.Len()) + suite.Assert().Equal(0, suite.cache.Capacity()) +} From 12dc704e832872ff177df5a50840ede550f430b9 Mon Sep 17 00:00:00 2001 From: Bo Thompson Date: Thu, 12 Oct 2023 14:22:52 -0700 Subject: [PATCH 02/20] [go] new package: integrationtest/traces This package benchmarks the performance of the `SessionFactory` class and its dependencies. It compares Metastore and KMS access patterns with different cache configurations by playing back access traces. --- go/appencryption/.gitignore | 7 + .../integrationtest/traces/README.md | 23 ++ .../integrationtest/traces/cache2k.go | 40 +++ .../integrationtest/traces/cache2k_test.go | 201 +++++++++++++ .../integrationtest/traces/combine-png.sh | 10 + .../integrationtest/traces/combine.sh | 11 + .../integrationtest/traces/data/dl-address.sh | 4 + .../integrationtest/traces/data/dl-cache2k.sh | 9 + .../integrationtest/traces/data/dl-storage.sh | 8 + .../traces/data/dl-wikipedia.sh | 8 + .../integrationtest/traces/data/dl-youtube.sh | 16 + .../integrationtest/traces/files.go | 167 ++++++++++ .../traces/out/report-session-cache.png | Bin 0 -> 46134 bytes .../integrationtest/traces/report.go | 284 ++++++++++++++++++ .../integrationtest/traces/report.sh | 29 ++ .../integrationtest/traces/report_test.go | 55 ++++ .../integrationtest/traces/storage.go | 63 ++++ .../integrationtest/traces/storage_test.go | 71 +++++ .../traces/visualize-request.sh | 31 ++ .../integrationtest/traces/visualize-size.sh | 31 ++ .../integrationtest/traces/wikipedia.go | 62 ++++ .../integrationtest/traces/wikipedia_test.go | 36 +++ .../integrationtest/traces/youtube.go | 55 ++++ .../integrationtest/traces/youtube_test.go | 36 +++ .../integrationtest/traces/zipf.go | 37 +++ .../integrationtest/traces/zipf_test.go | 70 +++++ 26 files changed, 1364 insertions(+) create mode 100644 go/appencryption/integrationtest/traces/README.md create mode 100644 go/appencryption/integrationtest/traces/cache2k.go create mode 100644 go/appencryption/integrationtest/traces/cache2k_test.go create mode 100755 go/appencryption/integrationtest/traces/combine-png.sh create mode 100755 go/appencryption/integrationtest/traces/combine.sh create mode 100755 go/appencryption/integrationtest/traces/data/dl-address.sh create mode 100755 go/appencryption/integrationtest/traces/data/dl-cache2k.sh create mode 100755 go/appencryption/integrationtest/traces/data/dl-storage.sh create mode 100755 go/appencryption/integrationtest/traces/data/dl-wikipedia.sh create mode 100755 go/appencryption/integrationtest/traces/data/dl-youtube.sh create mode 100644 go/appencryption/integrationtest/traces/files.go create mode 100644 go/appencryption/integrationtest/traces/out/report-session-cache.png create mode 100644 go/appencryption/integrationtest/traces/report.go create mode 100755 go/appencryption/integrationtest/traces/report.sh create mode 100644 go/appencryption/integrationtest/traces/report_test.go create mode 100644 go/appencryption/integrationtest/traces/storage.go create mode 100644 go/appencryption/integrationtest/traces/storage_test.go create mode 100755 go/appencryption/integrationtest/traces/visualize-request.sh create mode 100755 go/appencryption/integrationtest/traces/visualize-size.sh create mode 100644 go/appencryption/integrationtest/traces/wikipedia.go create mode 100644 go/appencryption/integrationtest/traces/wikipedia_test.go create mode 100644 go/appencryption/integrationtest/traces/youtube.go create mode 100644 go/appencryption/integrationtest/traces/youtube_test.go create mode 100644 go/appencryption/integrationtest/traces/zipf.go create mode 100644 go/appencryption/integrationtest/traces/zipf_test.go diff --git a/go/appencryption/.gitignore b/go/appencryption/.gitignore index 730925ca2..d1a9e135c 100644 --- a/go/appencryption/.gitignore +++ b/go/appencryption/.gitignore @@ -12,3 +12,10 @@ cmd/example/log.log hack .DS_Store **/checkstyle-result.xml + +integrationtest/traces/data/*.dat +integrationtest/traces/data/*.bz2 +integrationtest/traces/data/*.gz +integrationtest/traces/data/*.xz +integrationtest/traces/data/*.tgz +integrationtest/traces/out/ diff --git a/go/appencryption/integrationtest/traces/README.md b/go/appencryption/integrationtest/traces/README.md new file mode 100644 index 000000000..c3ed5ce52 --- /dev/null +++ b/go/appencryption/integrationtest/traces/README.md @@ -0,0 +1,23 @@ +# SessionFactory Performance Report + +This package benchmarks the performance of the `SessionFactory` class and +its dependencies. It compares Metastore and KMS access patterns with +different cache configurations. + +The source code for this package is derived from the package of the same +name in the [Mango Cache](https://github.com/goburrow/cache) project. See +[LICENSE](../../pkg/cache/internal/LICENSE) for copyright and +licensing information. + +## Traces + +Name | Source +------------ | ------ +Glimpse | Authors of the LIRS algorithm - retrieved from [Cache2k](https://github.com/cache2k/cache2k-benchmark) +Multi2 | Authors of the LIRS algorithm - retrieved from [Cache2k](https://github.com/cache2k/cache2k-benchmark) +OLTP | Authors of the ARC algorithm - retrieved from [Cache2k](https://github.com/cache2k/cache2k-benchmark) +ORMBusy | GmbH - retrieved from [Cache2k](https://github.com/cache2k/cache2k-benchmark) +Sprite | Authors of the LIRS algorithm - retrieved from [Cache2k](https://github.com/cache2k/cache2k-benchmark) +Wikipedia | [WikiBench](http://www.wikibench.eu/) +YouTube | [University of Massachusetts](http://traces.cs.umass.edu/index.php/Network/Network) +WebSearch | [University of Massachusetts](http://traces.cs.umass.edu/index.php/Storage/Storage) diff --git a/go/appencryption/integrationtest/traces/cache2k.go b/go/appencryption/integrationtest/traces/cache2k.go new file mode 100644 index 000000000..6ab86a36e --- /dev/null +++ b/go/appencryption/integrationtest/traces/cache2k.go @@ -0,0 +1,40 @@ +package traces + +import ( + "bufio" + "context" + "encoding/binary" + "io" +) + +type cache2kProvider struct { + r *bufio.Reader +} + +// NewCache2kProvider returns a Provider which items are from traces +// in Cache2k repository (https://github.com/cache2k/cache2k-benchmark). +func NewCache2kProvider(r io.Reader) Provider { + return &cache2kProvider{ + r: bufio.NewReader(r), + } +} + +func (p *cache2kProvider) Provide(ctx context.Context, keys chan<- interface{}) { + defer close(keys) + + v := make([]byte, 4) + + for { + _, err := p.r.Read(v) + if err != nil { + return + } + + k := binary.LittleEndian.Uint32(v) + select { + case <-ctx.Done(): + return + case keys <- k: + } + } +} diff --git a/go/appencryption/integrationtest/traces/cache2k_test.go b/go/appencryption/integrationtest/traces/cache2k_test.go new file mode 100644 index 000000000..eeafd8b31 --- /dev/null +++ b/go/appencryption/integrationtest/traces/cache2k_test.go @@ -0,0 +1,201 @@ +package traces + +import "testing" + +func TestRequestORMBusy(t *testing.T) { + for _, p := range policies { + p := p + t.Run(p, func(t *testing.T) { + t.Parallel() + opt := options{ + policy: p, + cacheSize: 1000, + reportInterval: 40000, + maxItems: 4000000, + } + testRequest(t, NewCache2kProvider, opt, + "trace-mt-db-*-busy.trc.bin.bz2", "request_ormbusy-"+p+".txt") + }) + } +} + +func TestSizeORMBusy(t *testing.T) { + for _, p := range policies { + p := p + t.Run(p, func(t *testing.T) { + t.Parallel() + opt := options{ + policy: p, + cacheSize: 250, + maxItems: 1000000, + } + testSize(t, NewCache2kProvider, opt, + "trace-mt-db-*-busy.trc.bin.bz2", "size_ormbusy-"+p+".txt") + }) + } +} + +func TestRequestORMNight(t *testing.T) { + for _, p := range policies { + p := p + t.Run(p, func(t *testing.T) { + t.Parallel() + opt := options{ + policy: p, + cacheSize: 1000, + reportInterval: 40000, + maxItems: 4000000, + } + testRequest(t, NewCache2kProvider, opt, + "trace-mt-db-*-night.trc.bin.bz2", "request_ormnight-"+p+".txt") + }) + } +} + +func TestSizeORMNight(t *testing.T) { + for _, p := range policies { + p := p + t.Run(p, func(t *testing.T) { + t.Parallel() + opt := options{ + policy: p, + cacheSize: 250, + maxItems: 1000000, + } + testSize(t, NewCache2kProvider, opt, + "trace-mt-db-*-night.trc.bin.bz2", "size_ormnight-"+p+".txt") + }) + } +} + +func TestRequestGlimpse(t *testing.T) { + for _, p := range policies { + p := p + t.Run(p, func(t *testing.T) { + t.Parallel() + opt := options{ + policy: p, + cacheSize: 512, + reportInterval: 100, + maxItems: 6000, + } + testRequest(t, NewCache2kProvider, opt, + "trace-glimpse.trc.bin.gz", "request_glimpse-"+p+".txt") + }) + } +} + +func TestSizeGlimpse(t *testing.T) { + for _, p := range policies { + p := p + t.Run(p, func(t *testing.T) { + t.Parallel() + opt := options{ + policy: p, + cacheSize: 125, + maxItems: 6000, + } + testSize(t, NewCache2kProvider, opt, + "trace-glimpse.trc.bin.gz", "size_glimpse-"+p+".txt") + }) + } +} + +func TestRequestOLTP(t *testing.T) { + for _, p := range policies { + p := p + t.Run(p, func(t *testing.T) { + t.Parallel() + opt := options{ + policy: p, + cacheSize: 1000, + reportInterval: 1000, + maxItems: 900000, + } + testRequest(t, NewCache2kProvider, opt, + "trace-oltp.trc.bin.gz", "request_oltp-"+p+".txt") + }) + } +} + +func TestSizeOLTP(t *testing.T) { + for _, p := range policies { + p := p + t.Run(p, func(t *testing.T) { + t.Parallel() + opt := options{ + policy: p, + cacheSize: 250, + maxItems: 500000, + } + testSize(t, NewCache2kProvider, opt, + "trace-oltp.trc.bin.gz", "size_oltp-"+p+".txt") + }) + } +} + +func TestRequestSprite(t *testing.T) { + for _, p := range policies { + p := p + t.Run(p, func(t *testing.T) { + t.Parallel() + opt := options{ + policy: p, + cacheSize: 1000, + reportInterval: 1000, + maxItems: 120000, + } + testRequest(t, NewCache2kProvider, opt, + "trace-sprite.trc.bin.gz", "request_sprite-"+p+".txt") + }) + } +} + +func TestSizeSprite(t *testing.T) { + for _, p := range policies { + p := p + t.Run(p, func(t *testing.T) { + t.Parallel() + opt := options{ + policy: p, + cacheSize: 25, + maxItems: 120000, + } + testSize(t, NewCache2kProvider, opt, + "trace-sprite.trc.bin.gz", "size_sprite-"+p+".txt") + }) + } +} + +func TestRequestMulti2(t *testing.T) { + for _, p := range policies { + p := p + t.Run(p, func(t *testing.T) { + t.Parallel() + opt := options{ + policy: p, + cacheSize: 1000, + reportInterval: 200, + maxItems: 25000, + } + testRequest(t, NewCache2kProvider, opt, + "trace-multi2.trc.bin.gz", "request_multi2-"+p+".txt") + }) + } +} + +func TestSizeMulti2(t *testing.T) { + for _, p := range policies { + p := p + t.Run(p, func(t *testing.T) { + t.Parallel() + opt := options{ + policy: p, + cacheSize: 250, + maxItems: 25000, + } + testSize(t, NewCache2kProvider, opt, + "trace-multi2.trc.bin.gz", "size_multi2-"+p+".txt") + }) + } +} diff --git a/go/appencryption/integrationtest/traces/combine-png.sh b/go/appencryption/integrationtest/traces/combine-png.sh new file mode 100755 index 000000000..08c49d8d7 --- /dev/null +++ b/go/appencryption/integrationtest/traces/combine-png.sh @@ -0,0 +1,10 @@ +#!/bin/sh +set -e +# NAMES="financial zipf" +NAMES="financial oltp ormbusy ormnight multi2 youtube websearch zipf" +FORMAT="png" +FILES="" +for N in $NAMES; do + FILES="$FILES out/$N-requests.$FORMAT out/$N-cachesize.$FORMAT" +done +gm montage -mode concatenate -tile 4x $FILES "out/report-session-cache.$FORMAT" diff --git a/go/appencryption/integrationtest/traces/combine.sh b/go/appencryption/integrationtest/traces/combine.sh new file mode 100755 index 000000000..3c16a36b4 --- /dev/null +++ b/go/appencryption/integrationtest/traces/combine.sh @@ -0,0 +1,11 @@ +#!/bin/sh +set -e + +if [ -z "$FORMAT" ]; then + FORMAT="png" +else + FORMAT="${FORMAT%% *}" +fi + +FILES=$(ls out/*-requests.$FORMAT out/*-cachesize.$FORMAT | sort) +gm montage -mode concatenate -tile 4x $FILES "out/report.$FORMAT" diff --git a/go/appencryption/integrationtest/traces/data/dl-address.sh b/go/appencryption/integrationtest/traces/data/dl-address.sh new file mode 100755 index 000000000..b7e05af6b --- /dev/null +++ b/go/appencryption/integrationtest/traces/data/dl-address.sh @@ -0,0 +1,4 @@ +#!/bin/sh +FILE="proj1-traces.tar.gz" +curl -O "http://cseweb.ucsd.edu/classes/fa07/cse240a/$FILE" +tar xvzf "$FILE" diff --git a/go/appencryption/integrationtest/traces/data/dl-cache2k.sh b/go/appencryption/integrationtest/traces/data/dl-cache2k.sh new file mode 100755 index 000000000..c32a6a0cd --- /dev/null +++ b/go/appencryption/integrationtest/traces/data/dl-cache2k.sh @@ -0,0 +1,9 @@ +#!/bin/sh +set -e + +FILES="trace-cpp.trc.bin.gz trace-glimpse.trc.bin.gz trace-mt-db-20160419-busy.trc.bin.bz2 trace-multi2.trc.bin.gz trace-oltp.trc.bin.gz trace-sprite.trc.bin.gz" +for F in $FILES; do + if [ ! -f "$F" ]; then + curl -L -O "https://github.com/cache2k/cache2k-benchmark/raw/master/traces/src/main/resources/org/cache2k/benchmark/traces/$F" + fi +done diff --git a/go/appencryption/integrationtest/traces/data/dl-storage.sh b/go/appencryption/integrationtest/traces/data/dl-storage.sh new file mode 100755 index 000000000..aba198ffc --- /dev/null +++ b/go/appencryption/integrationtest/traces/data/dl-storage.sh @@ -0,0 +1,8 @@ +#!/bin/sh +set -e +FILES="WebSearch1.spc.bz2 Financial2.spc.bz2" +for F in $FILES; do + if [ ! -f "$F" ]; then + curl -O "http://skuld.cs.umass.edu/traces/storage/$F" + fi +done diff --git a/go/appencryption/integrationtest/traces/data/dl-wikipedia.sh b/go/appencryption/integrationtest/traces/data/dl-wikipedia.sh new file mode 100755 index 000000000..5ac5f59ed --- /dev/null +++ b/go/appencryption/integrationtest/traces/data/dl-wikipedia.sh @@ -0,0 +1,8 @@ +#!/bin/sh +set -e +FILES="wiki.1191201596.gz" +for F in $FILES; do + if [ ! -f "$F" ]; then + curl -O "http://www.wikibench.eu/wiki/2007-10/$F" + fi +done diff --git a/go/appencryption/integrationtest/traces/data/dl-youtube.sh b/go/appencryption/integrationtest/traces/data/dl-youtube.sh new file mode 100755 index 000000000..2b5334973 --- /dev/null +++ b/go/appencryption/integrationtest/traces/data/dl-youtube.sh @@ -0,0 +1,16 @@ +#!/bin/sh +set -e +FILE="youtube_traces.tgz" +if [ ! -f "$FILE" ]; then + curl -O "http://skuld.cs.umass.edu/traces/network/$FILE" +fi +tar xzf "$FILE" + +rm youtube.parsed.*.24.dat +rm youtube.parsed.*.S1.dat + +for FILE in youtube.parsed.*.dat; do + # YYMMDD + NAME="$(echo "$FILE" | sed -e 's/\([0-9]\{2\}\)\([0-9]\{2\}\)\([0-9]\{2\}\)\(\.dat\)/\3\1\2\4/')" + mv "$FILE" "$NAME" +done diff --git a/go/appencryption/integrationtest/traces/files.go b/go/appencryption/integrationtest/traces/files.go new file mode 100644 index 000000000..a5e44cdd6 --- /dev/null +++ b/go/appencryption/integrationtest/traces/files.go @@ -0,0 +1,167 @@ +package traces + +import ( + "compress/bzip2" + "compress/gzip" + "fmt" + "io" + "os" + "path/filepath" + "strings" +) + +type readSeekCloser interface { + io.ReadCloser + io.Seeker +} + +type gzipFile struct { + r *gzip.Reader + f *os.File +} + +func newGzipFile(f *os.File) *gzipFile { + r, err := gzip.NewReader(f) + if err != nil { + panic(err) + } + + return &gzipFile{ + r: r, + f: f, + } +} + +func (f *gzipFile) Read(p []byte) (int, error) { + return f.r.Read(p) +} + +func (f *gzipFile) Seek(offset int64, whence int) (int64, error) { + n, err := f.f.Seek(offset, whence) + if err != nil { + return n, err + } + + f.r.Reset(f.f) + + return n, nil +} + +func (f *gzipFile) Close() error { + err1 := f.r.Close() + + if err2 := f.f.Close(); err2 != nil { + return err2 + } + + return err1 +} + +type bzip2File struct { + r io.Reader + f *os.File +} + +func newBzip2File(f *os.File) *bzip2File { + return &bzip2File{ + r: bzip2.NewReader(f), + f: f, + } +} + +func (f *bzip2File) Read(p []byte) (int, error) { + return f.r.Read(p) +} + +func (f *bzip2File) Seek(offset int64, whence int) (int64, error) { + n, err := f.f.Seek(offset, whence) + if err != nil { + return n, err + } + + f.r = bzip2.NewReader(f.f) + + return n, nil +} + +func (f *bzip2File) Close() error { + return f.f.Close() +} + +type filesReader struct { + io.Reader + files []readSeekCloser +} + +func openFilesGlob(pattern string) (*filesReader, error) { + files, err := filepath.Glob(pattern) + if err != nil { + return nil, err + } + + if len(files) == 0 { + return nil, fmt.Errorf("%s not found", pattern) + } + + return openFiles(files...) +} + +func openFiles(files ...string) (*filesReader, error) { + r := &filesReader{} + r.files = make([]readSeekCloser, 0, len(files)) + readers := make([]io.Reader, 0, len(files)) + + for _, name := range files { + f, err := os.Open(name) + if err != nil { + r.Close() + return nil, err + } + + var rs readSeekCloser + if strings.HasSuffix(name, ".gz") { + rs = newGzipFile(f) + } else if strings.HasSuffix(name, ".bz2") { + rs = newBzip2File(f) + } else { + rs = f + } + + r.files = append(r.files, rs) + readers = append(readers, rs) + } + + r.Reader = io.MultiReader(readers...) + + return r, nil +} + +func (r *filesReader) Close() error { + var err error + + for _, f := range r.files { + e := f.Close() + if err != nil && e != nil { + err = e + } + } + + return err +} + +func (r *filesReader) Reset() error { + readers := make([]io.Reader, 0, len(r.files)) + + for _, f := range r.files { + _, err := f.Seek(0, 0) + if err != nil { + return err + } + + readers = append(readers, f) + } + + r.Reader = io.MultiReader(readers...) + + return nil +} diff --git a/go/appencryption/integrationtest/traces/out/report-session-cache.png b/go/appencryption/integrationtest/traces/out/report-session-cache.png new file mode 100644 index 0000000000000000000000000000000000000000..f60cfabdeae7c503723f8f279b08c80d339ada6f GIT binary patch literal 46134 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

4B!iUUtIhs9|rfq-D7i7R_2Hw44Q{ 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 0 { + randJitter = rand.Int63n(int64(d.jitter)) + } + + if d.delay > 0 { + time.Sleep(d.delay + time.Duration(randJitter)) + } + + ch <- 1 + }() + + <-ch +} + +func (d *delayedMetastore) Load(ctx context.Context, keyID string, created int64) (*appencryption.EnvelopeKeyRecord, error) { + d.loadCounter.Inc(1) + + d.delayWithJitter() + + return d.m.Load(ctx, keyID, created) +} + +func (d *delayedMetastore) LoadLatest(ctx context.Context, keyID string) (*appencryption.EnvelopeKeyRecord, error) { + d.loadLatestCounter.Inc(1) + + d.delayWithJitter() + + return d.m.LoadLatest(ctx, keyID) +} + +func (d *delayedMetastore) Store(ctx context.Context, keyID string, created int64, envelope *appencryption.EnvelopeKeyRecord) (bool, error) { + d.storeCounter.Inc(1) + + d.delayWithJitter() + + return d.m.Store(ctx, keyID, created, envelope) +} + +type options struct { + policy string + cacheSize int + reportInterval int + maxItems int +} + +var policies = []string{ + "session-legacy", +} + +const ( + product = "enclibrary" + service = "asherah" + staticKey = "thisIsAStaticMasterKeyForTesting" + payloadSizeBytes = 100 +) + +var c = aead.NewAES256GCM() + +//nolint:gocyclo +func benchmarkSessionFactory(p Provider, r Reporter, opt options) { + static, err := kms.NewStatic(staticKey, c) + if err != nil { + panic(err) + } + + km := newTrackedKMS(static) + config := getConfig(opt) + ms := newDelayedMetastore(5, 5) + + factory := appencryption.NewSessionFactory( + config, + ms, + km, + c, + ) + defer factory.Close() + + randomBytes := internal.GetRandBytes(payloadSizeBytes) + + keys := make(chan interface{}, 100) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + go p.Provide(ctx, keys) + + stats := Stats{} + + for i := 0; ; { + if opt.maxItems > 0 && i >= opt.maxItems { + break + } + + k, ok := <-keys + if !ok { + break + } + + sess, err := factory.GetSession(fmt.Sprintf("partition-%v", k)) + if err != nil { + panic(err) + } + + _, err = sess.Encrypt(ctx, randomBytes) + sess.Close() + + if err != nil { + fmt.Printf("encrypt fail: i=%d, err=%v\n", i, err) + continue + } + + i++ + if opt.reportInterval > 0 && i%opt.reportInterval == 0 { + metastoreStats(&stats, ms, km, uint64(i)) + r.Report(stats, opt) + } + } + + if opt.reportInterval == 0 { + metastoreStats(&stats, ms, km, uint64(opt.maxItems)) + r.Report(stats, opt) + } +} + +func getConfig(opt options) *appencryption.Config { + policy := appencryption.NewCryptoPolicy( + // appencryption.WithRevokeCheckInterval(10 * time.Second), + ) + + policy.CreateDatePrecision = time.Minute + + switch opt.policy { + case "session-legacy": + policy.CacheSessions = true + policy.SessionCacheMaxSize = opt.cacheSize + default: + panic(fmt.Sprintf("unknown policy: %s", opt.policy)) + } + + return &appencryption.Config{ + Policy: policy, + Product: product, + Service: service, + } +} + +// metastoreStats populates the cache stats for the appencryption metastore. +func metastoreStats(stats *Stats, ms *delayedMetastore, kms *trackedKMS, requests uint64) { + stats.RequestCount = requests + + stats.MetastoreLoadCount = uint64(ms.loadCounter.Count()) + stats.MetastoreLoadLatestCount = uint64(ms.loadLatestCounter.Count()) + stats.MetastoreStoreCount = uint64(ms.storeCounter.Count()) + stats.MetastoreOpCount = stats.MetastoreLoadCount + stats.MetastoreLoadLatestCount + stats.MetastoreStoreCount + + stats.KMSDecryptCount = uint64(kms.decryptCounter.Count()) + stats.KMSEncryptCount = uint64(kms.encryptCounter.Count()) + stats.KMSOpCount = stats.KMSDecryptCount + stats.KMSEncryptCount + + stats.OpRate = float64(stats.MetastoreOpCount+stats.KMSOpCount) / float64(stats.RequestCount) +} diff --git a/go/appencryption/integrationtest/traces/report.sh b/go/appencryption/integrationtest/traces/report.sh new file mode 100755 index 000000000..6d856ecc8 --- /dev/null +++ b/go/appencryption/integrationtest/traces/report.sh @@ -0,0 +1,29 @@ +#!/bin/bash +set -e + +report() { + NAME="$1" + TESTARGS="-p 1 -timeout=3h -run=$NAME" + go test -v $TESTARGS | tee "out/$NAME.txt" + + NAME=$(echo "$NAME" | tr '[:upper:]' '[:lower:]') + ./visualize-request.sh out/request_$NAME-*.txt + for OUTPUT in out.*; do + mv -v "$OUTPUT" "out/$NAME-requests.${OUTPUT#*.}" + done + ./visualize-size.sh out/size_$NAME-*.txt + for OUTPUT in out.*; do + mv -v "$OUTPUT" "out/$NAME-cachesize.${OUTPUT#*.}" + done +} + +# use first arg or default to a small subset +TRACES="$@" +if [ -z "$TRACES" ]; then + TRACES="Financial OLTP ORMBusy Zipf" +fi + +# TRACES="Multi2 ORMBusy ORMNight Glimpse OLTP Sprite Financial WebSearch Wikipedia YouTube Zipf" +for TRACE in $TRACES; do + report $TRACE +done diff --git a/go/appencryption/integrationtest/traces/report_test.go b/go/appencryption/integrationtest/traces/report_test.go new file mode 100644 index 000000000..a32fb006e --- /dev/null +++ b/go/appencryption/integrationtest/traces/report_test.go @@ -0,0 +1,55 @@ +package traces + +import ( + "io" + "os" + "path/filepath" + "testing" +) + +func testRequest(t *testing.T, newProvider func(io.Reader) Provider, opt options, traceFiles string, reportFile string) { + r, err := openFilesGlob(filepath.Join("data", traceFiles)) + if err != nil { + t.Skip(err) + } + defer r.Close() + provider := newProvider(r) + + w, err := os.Create(filepath.Join("out", reportFile)) + if err != nil { + t.Fatal(err) + } + defer w.Close() + reporter := NewReporter(w) + + benchmarkSessionFactory(provider, reporter, opt) +} + +func testSize(t *testing.T, newProvider func(io.Reader) Provider, opt options, traceFiles, reportFile string) { + r, err := openFilesGlob(filepath.Join("data", traceFiles)) + if err != nil { + t.Skip(err) + } + defer r.Close() + + w, err := os.Create(filepath.Join("out", reportFile)) + if err != nil { + t.Fatal(err) + } + defer w.Close() + + reporter := NewReporter(w) + + for i := 0; i < 5; i++ { + provider := newProvider(r) + + benchmarkSessionFactory(provider, reporter, opt) + + err = r.Reset() + if err != nil { + t.Fatal(err) + } + + opt.cacheSize += opt.cacheSize + } +} diff --git a/go/appencryption/integrationtest/traces/storage.go b/go/appencryption/integrationtest/traces/storage.go new file mode 100644 index 000000000..b1622ed48 --- /dev/null +++ b/go/appencryption/integrationtest/traces/storage.go @@ -0,0 +1,63 @@ +package traces + +import ( + "bufio" + "bytes" + "context" + "io" + "strconv" +) + +type storageProvider struct { + r *bufio.Reader +} + +// NewStorageProvider returns a Provider with items are from +// Storage traces by the University of Massachusetts +// (http://traces.cs.umass.edu/index.php/Storage/Storage). +func NewStorageProvider(r io.Reader) Provider { + return &storageProvider{ + r: bufio.NewReader(r), + } +} + +func (p *storageProvider) Provide(ctx context.Context, keys chan<- interface{}) { + defer close(keys) + + for { + b, err := p.r.ReadBytes('\n') + if err != nil { + return + } + + k := p.parse(b) + if k > 0 { + select { + case <-ctx.Done(): + return + case keys <- k: + } + } + } +} + +func (p *storageProvider) parse(b []byte) uint64 { + idx := bytes.IndexByte(b, ',') + if idx < 0 { + return 0 + } + + b = b[idx+1:] + + idx = bytes.IndexByte(b, ',') + if idx < 0 { + return 0 + } + + k, err := strconv.ParseUint(string(b[:idx]), 10, 64) + if err != nil { + return 0 + } + + return k +} diff --git a/go/appencryption/integrationtest/traces/storage_test.go b/go/appencryption/integrationtest/traces/storage_test.go new file mode 100644 index 000000000..17fbd683f --- /dev/null +++ b/go/appencryption/integrationtest/traces/storage_test.go @@ -0,0 +1,71 @@ +package traces + +import "testing" + +func TestRequestWebSearch(t *testing.T) { + for _, p := range policies { + p := p + t.Run(p, func(t *testing.T) { + t.Parallel() + opt := options{ + policy: p, + cacheSize: 256000, + reportInterval: 10000, + maxItems: 1000000, + } + testRequest(t, NewStorageProvider, opt, + "WebSearch*.spc.bz2", "request_websearch-"+p+".txt") + }) + } +} + +func TestRequestFinancial(t *testing.T) { + for _, p := range policies { + p := p + t.Run(p, func(t *testing.T) { + t.Parallel() + opt := options{ + policy: p, + cacheSize: 1000, + reportInterval: 30000, + maxItems: 3000000, + } + testRequest(t, NewStorageProvider, opt, + "Financial*.spc.bz2", "request_financial-"+p+".txt") + }) + } +} + +func TestSizeWebSearch(t *testing.T) { + for _, p := range policies { + p := p + opt := options{ + policy: p, + cacheSize: 25000, + maxItems: 1000000, + } + + t.Run(p, func(t *testing.T) { + t.Parallel() + testSize(t, NewStorageProvider, opt, + "WebSearch*.spc.bz2", "size_websearch-"+p+".txt") + }) + } +} + +func TestSizeFinancial(t *testing.T) { + for _, p := range policies { + p := p + opt := options{ + policy: p, + cacheSize: 250, + maxItems: 1000000, + } + + t.Run(p, func(t *testing.T) { + t.Parallel() + testSize(t, NewStorageProvider, opt, + "Financial*.spc.bz2", "size_financial-"+p+".txt") + }) + } +} diff --git a/go/appencryption/integrationtest/traces/visualize-request.sh b/go/appencryption/integrationtest/traces/visualize-request.sh new file mode 100755 index 000000000..392edcaae --- /dev/null +++ b/go/appencryption/integrationtest/traces/visualize-request.sh @@ -0,0 +1,31 @@ +#!/bin/bash +if [ -z "$FORMAT" ]; then + #FORMAT='svg size 400,300 font "Helvetica,10"' + # FORMAT='png size 220,180 small noenhanced' + FORMAT='png size 400,300 small noenhanced' +fi +OUTPUT="out.${FORMAT%% *}" +PLOTARG="" + +for f in "$@"; do + if [ ! -z "$PLOTARG" ]; then + PLOTARG="$PLOTARG," + fi + NAME="$(basename "$f")" + NAME="${NAME%.*}" + NAME="${NAME#*_}" + PLOTARG="$PLOTARG '$f' every ::1 using 1:9 with lines title '$NAME'" +done + +ARG="set datafile separator ',';\ + set xlabel 'Requests';\ + set xtics rotate by 45 right;\ + set ylabel 'Op Rate' offset 1;\ + set yrange [0:];\ + set key bottom right;\ + set colors classic;\ + set terminal $FORMAT;\ + set output '$OUTPUT';\ + plot $PLOTARG" + +gnuplot -e "$ARG" diff --git a/go/appencryption/integrationtest/traces/visualize-size.sh b/go/appencryption/integrationtest/traces/visualize-size.sh new file mode 100755 index 000000000..2ca8dcd73 --- /dev/null +++ b/go/appencryption/integrationtest/traces/visualize-size.sh @@ -0,0 +1,31 @@ +#!/bin/bash +if [ -z "$FORMAT" ]; then + #FORMAT='svg size 400,300 font "Helvetica,10"' + # FORMAT='png size 220,180 small noenhanced' + FORMAT='png size 400,300 small noenhanced' +fi +OUTPUT="out.${FORMAT%% *}" +PLOTARG="" + +for f in "$@"; do + if [ ! -z "$PLOTARG" ]; then + PLOTARG="$PLOTARG," + fi + NAME="$(basename "$f")" # remove path + NAME="${NAME%.*}" # remove extension + NAME="${NAME#*_}" # remove prefix + PLOTARG="$PLOTARG '$f' every ::1 using 10:9:xtic(10) with lines title '$NAME'" # 10:9 is cache size:op rate +done + +ARG="set datafile separator ',';\ + set xlabel 'Cache Size';\ + set xtics rotate by 45 right;\ + set ylabel 'Op Rate' offset 1;\ + set yrange [0:];\ + set key bottom right;\ + set colors classic;\ + set terminal $FORMAT;\ + set output '$OUTPUT';\ + plot $PLOTARG" + +gnuplot -e "$ARG" diff --git a/go/appencryption/integrationtest/traces/wikipedia.go b/go/appencryption/integrationtest/traces/wikipedia.go new file mode 100644 index 000000000..f6a890447 --- /dev/null +++ b/go/appencryption/integrationtest/traces/wikipedia.go @@ -0,0 +1,62 @@ +package traces + +import ( + "bufio" + "bytes" + "context" + "io" +) + +type wikipediaProvider struct { + r *bufio.Reader +} + +func NewWikipediaProvider(r io.Reader) Provider { + return &wikipediaProvider{ + r: bufio.NewReader(r), + } +} + +func (p *wikipediaProvider) Provide(ctx context.Context, keys chan<- interface{}) { + defer close(keys) + + for { + b, err := p.r.ReadBytes('\n') + if err != nil { + return + } + + v := p.parse(b) + if v != "" { + select { + case <-ctx.Done(): + return + case keys <- v: + } + } + } +} + +func (p *wikipediaProvider) parse(b []byte) string { + // Get url + idx := bytes.Index(b, []byte("http://")) + if idx < 0 { + return "" + } + + b = b[idx+len("http://"):] + + // Get path + idx = bytes.IndexByte(b, '/') + if idx > 0 { + b = b[idx:] + } + + // Skip params + idx = bytes.IndexAny(b, "? ") + if idx > 0 { + b = b[:idx] + } + + return string(b) +} diff --git a/go/appencryption/integrationtest/traces/wikipedia_test.go b/go/appencryption/integrationtest/traces/wikipedia_test.go new file mode 100644 index 000000000..a49c801c4 --- /dev/null +++ b/go/appencryption/integrationtest/traces/wikipedia_test.go @@ -0,0 +1,36 @@ +package traces + +import "testing" + +func TestRequestWikipedia(t *testing.T) { + for _, p := range policies { + p := p + t.Run(p, func(t *testing.T) { + t.Parallel() + opt := options{ + policy: p, + cacheSize: 512, + reportInterval: 10000, + maxItems: 1000000, + } + testRequest(t, NewWikipediaProvider, opt, + "wiki.*.gz", "request_wikipedia-"+p+".txt") + }) + } +} + +func TestSizeWikipedia(t *testing.T) { + for _, p := range policies { + p := p + t.Run(p, func(t *testing.T) { + t.Parallel() + opt := options{ + policy: p, + cacheSize: 250, + maxItems: 100000, + } + testSize(t, NewWikipediaProvider, opt, + "wiki.*.gz", "size_wikipedia-"+p+".txt") + }) + } +} diff --git a/go/appencryption/integrationtest/traces/youtube.go b/go/appencryption/integrationtest/traces/youtube.go new file mode 100644 index 000000000..72a1c975b --- /dev/null +++ b/go/appencryption/integrationtest/traces/youtube.go @@ -0,0 +1,55 @@ +package traces + +import ( + "bufio" + "bytes" + "context" + "io" +) + +type youtubeProvider struct { + r *bufio.Reader +} + +func NewYoutubeProvider(r io.Reader) Provider { + return &youtubeProvider{ + r: bufio.NewReader(r), + } +} + +func (p *youtubeProvider) Provide(ctx context.Context, keys chan<- interface{}) { + defer close(keys) + + for { + b, err := p.r.ReadBytes('\n') + if err != nil { + return + } + + v := p.parse(b) + if v != "" { + select { + case <-ctx.Done(): + return + case keys <- v: + } + } + } +} + +func (p *youtubeProvider) parse(b []byte) string { + // Get video id + idx := bytes.Index(b, []byte("GETVIDEO ")) + if idx < 0 { + return "" + } + + b = b[idx+len("GETVIDEO "):] + + idx = bytes.IndexAny(b, "& ") + if idx > 0 { + b = b[:idx] + } + + return string(b) +} diff --git a/go/appencryption/integrationtest/traces/youtube_test.go b/go/appencryption/integrationtest/traces/youtube_test.go new file mode 100644 index 000000000..16e8482f8 --- /dev/null +++ b/go/appencryption/integrationtest/traces/youtube_test.go @@ -0,0 +1,36 @@ +package traces + +import "testing" + +func TestRequestYouTube(t *testing.T) { + for _, p := range policies { + p := p + t.Run(p, func(t *testing.T) { + t.Parallel() + opt := options{ + policy: p, + cacheSize: 1000, + reportInterval: 2000, + maxItems: 200000, + } + testRequest(t, NewYoutubeProvider, opt, + "youtube.parsed.0803*.dat", "request_youtube-"+p+".txt") + }) + } +} + +func TestSizeYouTube(t *testing.T) { + for _, p := range policies { + p := p + t.Run(p, func(t *testing.T) { + t.Parallel() + opt := options{ + policy: p, + cacheSize: 250, + maxItems: 100000, + } + testSize(t, NewYoutubeProvider, opt, + "youtube.parsed.0803*.dat", "size_youtube-"+p+".txt") + }) + } +} diff --git a/go/appencryption/integrationtest/traces/zipf.go b/go/appencryption/integrationtest/traces/zipf.go new file mode 100644 index 000000000..b18dc9643 --- /dev/null +++ b/go/appencryption/integrationtest/traces/zipf.go @@ -0,0 +1,37 @@ +package traces + +import ( + "context" + "math/rand" +) + +type zipfProvider struct { + r *rand.Zipf + n int +} + +func NewZipfProvider(s float64, num int) Provider { + if s <= 1.0 || num <= 0 { + panic("invalid zipf parameters") + } + + r := rand.New(rand.NewSource(1)) + + return &zipfProvider{ + r: rand.NewZipf(r, s, 1.0, 1<<16-1), + n: num, + } +} + +func (p *zipfProvider) Provide(ctx context.Context, keys chan<- interface{}) { + defer close(keys) + + for i := 0; i < p.n; i++ { + v := p.r.Uint64() + select { + case <-ctx.Done(): + return + case keys <- v: + } + } +} diff --git a/go/appencryption/integrationtest/traces/zipf_test.go b/go/appencryption/integrationtest/traces/zipf_test.go new file mode 100644 index 000000000..6673072a4 --- /dev/null +++ b/go/appencryption/integrationtest/traces/zipf_test.go @@ -0,0 +1,70 @@ +package traces + +import ( + "os" + "path/filepath" + "testing" +) + +func TestRequestZipf(t *testing.T) { + for _, p := range policies { + p := p + t.Run(p, func(t *testing.T) { + t.Parallel() + testRequestZipf(t, p, "request_zipf-"+p+".txt") + }) + } +} + +func testRequestZipf(t *testing.T, policy, reportFile string) { + opt := options{ + policy: policy, + cacheSize: 1000, + reportInterval: 1000, + maxItems: 100000, + } + + provider := NewZipfProvider(1.01, opt.maxItems) + + w, err := os.Create(filepath.Join("out", reportFile)) + if err != nil { + t.Fatal(err) + } + defer w.Close() + reporter := NewReporter(w) + // benchmarkCache(provider, reporter, opt) + benchmarkSessionFactory(provider, reporter, opt) +} + +func TestSizeZipf(t *testing.T) { + for _, p := range policies { + p := p + t.Run(p, func(t *testing.T) { + t.Parallel() + testSizeZipf(t, p, "size_zipf-"+p+".txt") + }) + } +} + +func testSizeZipf(t *testing.T, policy, reportFile string) { + opt := options{ + cacheSize: 250, + policy: policy, + maxItems: 100000, + } + + w, err := os.Create(filepath.Join("out", reportFile)) + if err != nil { + t.Fatal(err) + } + defer w.Close() + + reporter := NewReporter(w) + + for i := 0; i < 5; i++ { + provider := NewZipfProvider(1.01, opt.maxItems) + // benchmarkCache(provider, reporter, opt) + benchmarkSessionFactory(provider, reporter, opt) + opt.cacheSize += opt.cacheSize + } +} From 46e4364276008c078f5c61b255e9d83531773f16 Mon Sep 17 00:00:00 2001 From: Bo Thompson Date: Thu, 12 Oct 2023 15:53:32 -0700 Subject: [PATCH 03/20] rename cache* to key_cache* --- go/appencryption/{cache.go => key_cache.go} | 0 .../{cache_benchmark_test.go => key_cache_benchmark_test.go} | 0 go/appencryption/{cache_test.go => key_cache_test.go} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename go/appencryption/{cache.go => key_cache.go} (100%) rename go/appencryption/{cache_benchmark_test.go => key_cache_benchmark_test.go} (100%) rename go/appencryption/{cache_test.go => key_cache_test.go} (100%) diff --git a/go/appencryption/cache.go b/go/appencryption/key_cache.go similarity index 100% rename from go/appencryption/cache.go rename to go/appencryption/key_cache.go diff --git a/go/appencryption/cache_benchmark_test.go b/go/appencryption/key_cache_benchmark_test.go similarity index 100% rename from go/appencryption/cache_benchmark_test.go rename to go/appencryption/key_cache_benchmark_test.go diff --git a/go/appencryption/cache_test.go b/go/appencryption/key_cache_test.go similarity index 100% rename from go/appencryption/cache_test.go rename to go/appencryption/key_cache_test.go From a142003d2ecf29e78efd1fc7d8441b8afc299f46 Mon Sep 17 00:00:00 2001 From: Bo Thompson Date: Fri, 13 Oct 2023 13:41:07 -0700 Subject: [PATCH 04/20] [go] caching updates Key caching has been refactored to utilize the new cache package. Added support for a shared intermediate key cache. When enabled, and session caching disabled, caching performance mirrors that of the legacy session cache. See integrationtest/traces/out/report-session-cache.png. --- go/appencryption/envelope.go | 201 ++++------- go/appencryption/envelope_test.go | 92 +---- .../traces/out/report-session-cache.png | Bin 46134 -> 57231 bytes .../integrationtest/traces/report.go | 11 + go/appencryption/internal/key.go | 63 +++- go/appencryption/key_cache.go | 340 +++++++++++------- go/appencryption/key_cache_benchmark_test.go | 303 ++++++++++------ go/appencryption/key_cache_test.go | 319 ++++++++-------- go/appencryption/parameterized_test.go | 34 +- go/appencryption/pkg/cache/cache.go | 7 +- go/appencryption/policy.go | 57 ++- go/appencryption/policy_test.go | 35 +- go/appencryption/session.go | 77 ++-- go/appencryption/session_cache.go | 103 +++++- go/appencryption/session_cache_test.go | 22 +- go/appencryption/session_test.go | 12 +- 16 files changed, 978 insertions(+), 698 deletions(-) diff --git a/go/appencryption/envelope.go b/go/appencryption/envelope.go index 8c859d952..f5835abbd 100644 --- a/go/appencryption/envelope.go +++ b/go/appencryption/envelope.go @@ -3,7 +3,6 @@ package appencryption import ( "context" "fmt" - "sync" "time" "github.com/godaddy/asherah/go/securememory" @@ -33,6 +32,19 @@ func (m KeyMeta) String() string { return fmt.Sprintf("KeyMeta [keyId=%s created=%d]", m.ID, m.Created) } +// IsLatest returns true if the key meta is the latest version of the key. +func (m KeyMeta) IsLatest() bool { + return m.Created == 0 +} + +// AsLatest returns a copy of the key meta with the Created timestamp set to 0. +func (m KeyMeta) AsLatest() KeyMeta { + return KeyMeta{ + ID: m.ID, + Created: 0, + } +} + // DataRowRecord contains the encrypted key and provided data, as well as the information // required to decrypt the key encryption key. This struct should be stored in your // data persistence as it's required to decrypt data. @@ -57,14 +69,14 @@ var _ Encryption = (*envelopeEncryption)(nil) // envelopeEncryption is used to encrypt and decrypt data related to a specific partition ID. type envelopeEncryption struct { - partition partition - Metastore Metastore - KMS KeyManagementService - Policy *CryptoPolicy - Crypto AEAD - SecretFactory securememory.SecretFactory - systemKeys cache - intermediateKeys cache + partition partition + Metastore Metastore + KMS KeyManagementService + Policy *CryptoPolicy + Crypto AEAD + SecretFactory securememory.SecretFactory + skCache keyCacher + ikCache keyCacher } // loadSystemKey fetches a known system key from the metastore and decrypts it using the key management service. @@ -91,12 +103,17 @@ func (e *envelopeEncryption) systemKeyFromEKR(ctx context.Context, ekr *Envelope return internal.NewCryptoKey(e.SecretFactory, ekr.Created, ekr.Revoked, bytes) } +type accessorRevokable interface { + internal.Revokable + internal.BytesFuncAccessor +} + // intermediateKeyFromEKR decrypts ekr using sk and returns a new CryptoKey containing the decrypted key data. -func (e *envelopeEncryption) intermediateKeyFromEKR(sk *internal.CryptoKey, ekr *EnvelopeKeyRecord) (*internal.CryptoKey, error) { +func (e *envelopeEncryption) intermediateKeyFromEKR(sk accessorRevokable, ekr *EnvelopeKeyRecord) (*internal.CryptoKey, error) { if ekr != nil && ekr.ParentKeyMeta != nil && sk.Created() != ekr.ParentKeyMeta.Created { - //In this case, the system key just rotated and this EKR was encrypted with the prior SK. - //A duplicate IK would have been attempted to create with the correct SK but would create a duplicate so is discarded. - //Lookup the correct system key so the ik decryption can succeed. + // In this case, the system key just rotated and this EKR was encrypted with the prior SK. + // A duplicate IK would have been attempted to create with the correct SK but would create a duplicate so is discarded. + // Lookup the correct system key so the ik decryption can succeed. skLoaded, err := e.getOrLoadSystemKey(context.Background(), *ekr.ParentKeyMeta) if err != nil { return nil, err @@ -177,105 +194,10 @@ func (e *envelopeEncryption) tryStoreSystemKey(ctx context.Context, sk *internal return e.tryStore(ctx, ekr), nil } -var _ keyReloader = (*reloader)(nil) - -type reloader struct { - loadedKeys []*internal.CryptoKey - mu sync.Mutex - loader keyLoader - isInvalidFunc func(key *internal.CryptoKey) bool - keyID string - isCached bool -} - -// Load implements keyLoader. -func (r *reloader) Load() (*internal.CryptoKey, error) { - k, err := r.loader.Load() - if err != nil { - return nil, err - } - - r.append(k) - - return k, nil -} - -// append a key to the list of loaded keys. A call to -// Close will close all appended keys. -func (r *reloader) append(key *internal.CryptoKey) { - r.mu.Lock() - r.loadedKeys = append(r.loadedKeys, key) - r.mu.Unlock() -} - -// IsInvalid implements keyReloader -func (r *reloader) IsInvalid(key *internal.CryptoKey) bool { - return r.isInvalidFunc(key) -} - -// Close calls maybeCloseKey for all keys previously loaded by a reloader instance. -func (r *reloader) Close() { - r.mu.Lock() - defer r.mu.Unlock() - - for k := range r.loadedKeys { - key := r.loadedKeys[k] - - maybeCloseKey(r.isCached, key) - } -} - -// GetOrLoadLatest wraps the GetOrLoadLatest of c using r as the loader. -func (r *reloader) GetOrLoadLatest(c cache) (*internal.CryptoKey, error) { - return c.GetOrLoadLatest(r.keyID, r) -} - -// newIntermediateKeyReloader returns a new reloader for intermediate keys. -func (e *envelopeEncryption) newIntermediateKeyReloader(ctx context.Context) *reloader { - return e.newKeyReloader( - ctx, - e.partition.IntermediateKeyID(), - e.Policy.CacheIntermediateKeys, - e.loadLatestOrCreateIntermediateKey, - ) -} - -// newSystemKeyReloader returns a new reloader for system keys. -func (e *envelopeEncryption) newSystemKeyReloader(ctx context.Context) *reloader { - return e.newKeyReloader( - ctx, - e.partition.SystemKeyID(), - e.Policy.CacheSystemKeys, - e.loadLatestOrCreateSystemKey, - ) -} - -// newKeyReloader returns a new reloader. -func (e *envelopeEncryption) newKeyReloader( - ctx context.Context, - id string, - isCached bool, - loader func(context.Context, string) (*internal.CryptoKey, error), -) *reloader { - return &reloader{ - keyID: id, - isCached: isCached, - loader: keyLoaderFunc(func() (*internal.CryptoKey, error) { - return loader(ctx, id) - }), - isInvalidFunc: e.isKeyInvalid, - } -} - -// isKeyInvalid checks if the key is revoked or expired. -func (e *envelopeEncryption) isKeyInvalid(key *internal.CryptoKey) bool { - return key.Revoked() || isKeyExpired(key.Created(), e.Policy.ExpireKeyAfter) -} - // isEnvelopeInvalid checks if the envelope key record is revoked or has an expired key. func (e *envelopeEncryption) isEnvelopeInvalid(ekr *EnvelopeKeyRecord) bool { // TODO Add key rotation policy check. If not inline, then can return valid even if expired - return e == nil || isKeyExpired(ekr.Created, e.Policy.ExpireKeyAfter) || ekr.Revoked + return e == nil || internal.IsKeyExpired(ekr.Created, e.Policy.ExpireKeyAfter) || ekr.Revoked } func (e *envelopeEncryption) generateKey() (*internal.CryptoKey, error) { @@ -318,15 +240,16 @@ func (e *envelopeEncryption) mustLoadLatest(ctx context.Context, id string) (*En // createIntermediateKey creates a new IK and attempts to persist the new key to the metastore. // If unsuccessful createIntermediateKey will attempt to fetch the latest IK from the metastore. func (e *envelopeEncryption) createIntermediateKey(ctx context.Context) (*internal.CryptoKey, error) { - r := e.newSystemKeyReloader(ctx) - defer r.Close() - // Try to get latest from cache. - sk, err := r.GetOrLoadLatest(e.systemKeys) + sk, err := e.skCache.GetOrLoadLatest(e.partition.SystemKeyID(), func(meta KeyMeta) (*internal.CryptoKey, error) { + return e.loadLatestOrCreateSystemKey(ctx, meta.ID) + }) if err != nil { return nil, err } + defer sk.Close() + ik, err := e.generateKey() if err != nil { return nil, err @@ -358,7 +281,7 @@ func (e *envelopeEncryption) createIntermediateKey(ctx context.Context) (*intern // tryStoreIntermediateKey attempts to persist the encrypted ik to the metastore ignoring all persistence related errors. // err will be non-nil only if encryption fails. -func (e *envelopeEncryption) tryStoreIntermediateKey(ctx context.Context, ik, sk *internal.CryptoKey) (success bool, err error) { +func (e *envelopeEncryption) tryStoreIntermediateKey(ctx context.Context, ik, sk accessorRevokable) (success bool, err error) { encBytes, err := internal.WithKeyFunc(ik, func(keyBytes []byte) ([]byte, error) { return internal.WithKeyFunc(sk, func(systemKeyBytes []byte) ([]byte, error) { return e.Crypto.Encrypt(keyBytes, systemKeyBytes) @@ -398,7 +321,7 @@ func (e *envelopeEncryption) loadLatestOrCreateIntermediateKey(ctx context.Conte return e.createIntermediateKey(ctx) } - defer maybeCloseKey(e.Policy.CacheSystemKeys, sk) + defer sk.Close() // Only use the loaded IK if it and its parent key is valid. if ik := e.getValidIntermediateKey(sk, ikEkr); ik != nil { @@ -411,18 +334,16 @@ func (e *envelopeEncryption) loadLatestOrCreateIntermediateKey(ctx context.Conte // getOrLoadSystemKey returns a system key from cache if it's already been loaded. Otherwise it retrieves the key // from the metastore. -func (e *envelopeEncryption) getOrLoadSystemKey(ctx context.Context, meta KeyMeta) (*internal.CryptoKey, error) { - loader := keyLoaderFunc(func() (*internal.CryptoKey, error) { - return e.loadSystemKey(ctx, meta) +func (e *envelopeEncryption) getOrLoadSystemKey(ctx context.Context, meta KeyMeta) (*cachedCryptoKey, error) { + return e.skCache.GetOrLoad(meta, func(m KeyMeta) (*internal.CryptoKey, error) { + return e.loadSystemKey(ctx, m) }) - - return e.systemKeys.GetOrLoad(meta, loader) } // getValidIntermediateKey returns a new CryptoKey constructed from ekr. It returns nil if sk is invalid or if key initialization fails. -func (e *envelopeEncryption) getValidIntermediateKey(sk *internal.CryptoKey, ekr *EnvelopeKeyRecord) *internal.CryptoKey { +func (e *envelopeEncryption) getValidIntermediateKey(sk accessorRevokable, ekr *EnvelopeKeyRecord) *internal.CryptoKey { // IK is only valid if its parent is valid - if e.isKeyInvalid(sk) { + if internal.IsKeyInvalid(sk, e.Policy.ExpireKeyAfter) { return nil } @@ -436,7 +357,7 @@ func (e *envelopeEncryption) getValidIntermediateKey(sk *internal.CryptoKey, ekr } // decryptRow decrypts drr using ik as the parent key and returns the decrypted data. -func decryptRow(ik *internal.CryptoKey, drr DataRowRecord, crypto AEAD) ([]byte, error) { +func decryptRow(ik internal.BytesFuncAccessor, drr DataRowRecord, crypto AEAD) ([]byte, error) { return internal.WithKeyFunc(ik, func(bytes []byte) ([]byte, error) { // TODO Consider having separate DecryptKey that is functional and handles wiping bytes rawDrk, err := crypto.Decrypt(drr.Key.EncryptedKey, bytes) @@ -450,27 +371,23 @@ func decryptRow(ik *internal.CryptoKey, drr DataRowRecord, crypto AEAD) ([]byte, }) } -// maybeCloseKey closes key if isCached is false. -func maybeCloseKey(isCached bool, key *internal.CryptoKey) { - if !isCached { - key.Close() - } -} - // EncryptPayload encrypts a provided slice of bytes and returns the data with the data row key and required // parent information to decrypt the data in the future. It also takes a context used for cancellation. func (e *envelopeEncryption) EncryptPayload(ctx context.Context, data []byte) (*DataRowRecord, error) { defer encryptTimer.UpdateSince(time.Now()) - reloader := e.newIntermediateKeyReloader(ctx) - defer reloader.Close() + loader := func(meta KeyMeta) (*internal.CryptoKey, error) { + return e.loadLatestOrCreateIntermediateKey(ctx, meta.ID) + } // Try to get latest from cache. - ik, err := reloader.GetOrLoadLatest(e.intermediateKeys) + ik, err := e.ikCache.GetOrLoadLatest(e.partition.IntermediateKeyID(), loader) if err != nil { return nil, err } + defer ik.Close() + // Note the id doesn't mean anything for DRK. Don't need to truncate created since that is intended // to prevent excessive IK/SK creation (we always create new DRK on each write, so not a concern there) drk, err := internal.GenerateKey(e.SecretFactory, time.Now().Unix(), AES256KeySize) @@ -526,16 +443,16 @@ func (e *envelopeEncryption) DecryptDataRowRecord(ctx context.Context, drr DataR return nil, errors.New("unable to decrypt record") } - loader := keyLoaderFunc(func() (*internal.CryptoKey, error) { - return e.loadIntermediateKey(ctx, *drr.Key.ParentKeyMeta) - }) + loader := func(meta KeyMeta) (*internal.CryptoKey, error) { + return e.loadIntermediateKey(ctx, meta) + } - ik, err := e.intermediateKeys.GetOrLoad(*drr.Key.ParentKeyMeta, loader) + ik, err := e.ikCache.GetOrLoad(*drr.Key.ParentKeyMeta, loader) if err != nil { return nil, err } - defer maybeCloseKey(e.Policy.CacheIntermediateKeys, ik) + defer ik.Close() return decryptRow(ik, drr, e.Crypto) } @@ -556,7 +473,7 @@ func (e *envelopeEncryption) loadIntermediateKey(ctx context.Context, meta KeyMe return nil, err } - defer maybeCloseKey(e.Policy.CacheSystemKeys, sk) + defer sk.Close() return e.intermediateKeyFromEKR(sk, ekr) } @@ -564,5 +481,9 @@ func (e *envelopeEncryption) loadIntermediateKey(ctx context.Context, meta KeyMe // Close frees all memory locked by the keys in the session. It should be called // as soon as its no longer in use. func (e *envelopeEncryption) Close() error { - return e.intermediateKeys.Close() + if e.Policy != nil && e.Policy.SharedIntermediateKeyCache { + return nil + } + + return e.ikCache.Close() } diff --git a/go/appencryption/envelope_test.go b/go/appencryption/envelope_test.go index cdec8c8ed..1d69f65c7 100644 --- a/go/appencryption/envelope_test.go +++ b/go/appencryption/envelope_test.go @@ -29,8 +29,8 @@ var ( type EnvelopeSuite struct { suite.Suite crypto AEAD - ikCache cache - skCache cache + ikCache keyCacher + skCache keyCacher partition partition e envelopeEncryption metastore Metastore @@ -53,14 +53,14 @@ func (suite *EnvelopeSuite) SetupTest() { suite.secretFactory = new(MockSecretFactory) suite.e = envelopeEncryption{ - partition: suite.partition, - Metastore: suite.metastore, - KMS: suite.kms, - Policy: NewCryptoPolicy(), - Crypto: suite.crypto, - SecretFactory: suite.secretFactory, - systemKeys: suite.skCache, - intermediateKeys: suite.ikCache, + partition: suite.partition, + Metastore: suite.metastore, + KMS: suite.kms, + Policy: NewCryptoPolicy(), + Crypto: suite.crypto, + SecretFactory: suite.secretFactory, + skCache: suite.skCache, + ikCache: suite.ikCache, } var err error @@ -874,66 +874,6 @@ func (suite *EnvelopeSuite) TestEnvelopeEncryption_DecryptDataRowRecord_ReturnsE mock.AssertExpectationsForObjects(suite.T(), suite.ikCache) } -func (suite *EnvelopeSuite) Test_KeyReloader_Load() { - called := false - - reloader := &reloader{ - loader: keyLoaderFunc(func() (*internal.CryptoKey, error) { - called = true - return nil, nil - }), - } - - k, err := reloader.Load() - assert.Nil(suite.T(), k) - assert.NoError(suite.T(), err) - assert.True(suite.T(), called) -} - -func (suite *EnvelopeSuite) Test_KeyReloader_IsInvalid() { - k, _ := getKeyAndKeyBytes(suite.T()) - called := false - - reloader := &reloader{ - isInvalidFunc: func(key *internal.CryptoKey) bool { - called = true - - assert.Equal(suite.T(), k, key) - return false - }, - } - - reloader.IsInvalid(k) - - assert.True(suite.T(), called) -} - -func (suite *EnvelopeSuite) Test_KeyReloader_Close() { - reloader := &reloader{ - loader: keyLoaderFunc(func() (*internal.CryptoKey, error) { - k, _ := getKeyAndKeyBytes(suite.T()) - return k, nil - }), - } - loadTestKey := func() *internal.CryptoKey { - k, _ := reloader.Load() - return k - } - - var keys []*internal.CryptoKey - keys = append(keys, loadTestKey(), loadTestKey()) - - for _, k := range keys { - assert.False(suite.T(), k.IsClosed()) - } - - reloader.Close() - - for _, k := range keys { - assert.True(suite.T(), k.IsClosed()) - } -} - func TestKeyMeta_String(t *testing.T) { meta := KeyMeta{ Created: someTimestamp, @@ -968,17 +908,19 @@ func TestEnvelopeEncryption_Close(t *testing.T) { sec, err := secretFactory.New(data) if assert.NoError(t, err) { m := new(MockSecretFactory) + m.On("New", data).Return(sec, nil) - cache := newKeyCache(NewCryptoPolicy()) + cache := newKeyCache(CacheTypeIntermediateKeys, NewCryptoPolicy()) + key, _ := internal.NewCryptoKey(m, 123456, false, data) - cache.keys["testing"] = cacheEntry{ - key: key, - } + cache.keys.Set("testing", cacheEntry{ + key: &cachedCryptoKey{CryptoKey: key, refs: 1}, + }) e := &envelopeEncryption{ - intermediateKeys: cache, + ikCache: cache, } assert.False(t, sec.IsClosed()) diff --git a/go/appencryption/integrationtest/traces/out/report-session-cache.png b/go/appencryption/integrationtest/traces/out/report-session-cache.png index f60cfabdeae7c503723f8f279b08c80d339ada6f..8111bc731f2615ff522dfbcf19fa82b5ec6d5896 100644 GIT binary patch literal 57231 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

4B!iUUtIhs9|rfq-D7i7R_2Hw44Q{ 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 0 { + return + } + + c.CryptoKey.Close() +} + +// increment the reference count for this key. +func (c *cachedCryptoKey) increment() { + c.rw.Lock() + defer c.rw.Unlock() + + c.refs++ +} + // cacheEntry contains a key and the time it was loaded from the metastore. type cacheEntry struct { loadedAt time.Time - key *internal.CryptoKey + key *cachedCryptoKey } // newCacheEntry returns a cacheEntry with the current time and key. func newCacheEntry(k *internal.CryptoKey) cacheEntry { return cacheEntry{ loadedAt: time.Now(), - key: k, + key: &cachedCryptoKey{ + CryptoKey: k, + + // initialize with a reference count of 1 to represent the + // reference held by the cache + refs: 1, + }, } } @@ -29,56 +67,88 @@ func cacheKey(id string, create int64) string { return fmt.Sprintf("%s-%d", id, create) } -// keyLoaderFunc is an adapter to allow the use of ordinary functions as key loaders. -// If f is a function with the appropriate signature, keyLoaderFunc(f) is a keyLoader -// that calls f. -type keyLoaderFunc func() (*internal.CryptoKey, error) - -// Load calls f(). -func (f keyLoaderFunc) Load() (*internal.CryptoKey, error) { - return f() -} - -// keyLoader is used by cache objects to retrieve keys on an as-needed basis. -type keyLoader interface { - Load() (*internal.CryptoKey, error) -} - -// keyReloader extends keyLoader by adding the ability to inspect loaded keys -// and reload them when needed -type keyReloader interface { - keyLoader - - // IsInvalid returns true if the provided key is no longer valid - IsInvalid(*internal.CryptoKey) bool -} - -// cache contains cached keys for reuse. -type cache interface { - GetOrLoad(id KeyMeta, loader keyLoader) (*internal.CryptoKey, error) - GetOrLoadLatest(id string, loader keyLoader) (*internal.CryptoKey, error) +// keyCacher contains cached keys for reuse. +type keyCacher interface { + GetOrLoad(id KeyMeta, loader func(KeyMeta) (*internal.CryptoKey, error)) (*cachedCryptoKey, error) + GetOrLoadLatest(id string, loader func(KeyMeta) (*internal.CryptoKey, error)) (*cachedCryptoKey, error) Close() error } // Verify keyCache implements the cache interface. -var _ cache = (*keyCache)(nil) +var _ keyCacher = (*keyCache)(nil) // keyCache is used to persist session based keys and destroys them on a call to close. type keyCache struct { - once sync.Once - rw sync.RWMutex policy *CryptoPolicy - keys map[string]cacheEntry + + keys cache.Interface[string, cacheEntry] + rw sync.RWMutex // protects concurrent access to the cache + + latest map[string]KeyMeta + + cacheType cacheKeyType } -// newKeyCache constructs a cache object that is ready to use. -func newKeyCache(policy *CryptoPolicy) *keyCache { - keys := make(map[string]cacheEntry) +// cacheKeyType is used to identify the type of key cache. +type cacheKeyType int + +// String returns a string representation of the cacheKeyType. +func (t cacheKeyType) String() string { + switch t { + case CacheTypeSystemKeys: + return "system" + case CacheTypeIntermediateKeys: + return "intermediate" + default: + return "unknown" + } +} + +const ( + // CacheTypeSystemKeys is used to cache system keys. + CacheTypeSystemKeys cacheKeyType = iota + // CacheTypeIntermediateKeys is used to cache intermediate keys. + CacheTypeIntermediateKeys +) + +// newKeyCacheWithCacheOptions constructs a cache object that is ready to use. +func newKeyCacheWithCacheOptions(t cacheKeyType, policy *CryptoPolicy, opts ...cache.Option[string, cacheEntry]) *keyCache { + cacheMaxSize := DefaultKeyCacheMaxSize + cachePolicy := "" + + switch t { + case CacheTypeSystemKeys: + cacheMaxSize = policy.SystemKeyCacheMaxSize + cachePolicy = policy.SystemKeyCacheEvictionPolicy + case CacheTypeIntermediateKeys: + cacheMaxSize = policy.IntermediateKeyCacheMaxSize + cachePolicy = policy.IntermediateKeyCacheEvictionPolicy + } + + if cachePolicy != "" { + opts = append(opts, cache.WithPolicy[string, cacheEntry](cache.CachePolicy(cachePolicy))) + } + + keys := cache.New(cacheMaxSize, opts...) return &keyCache{ policy: policy, keys: keys, + latest: make(map[string]KeyMeta), + + cacheType: t, + } +} + +// newKeyCache constructs a cache object that is ready to use. +func newKeyCache(t cacheKeyType, policy *CryptoPolicy) (c *keyCache) { + onEvict := func(key string, value cacheEntry) { + log.Debugf("%s eviction -- key: %s, id: %s\n", c, value.key, key) + + value.key.Close() } + + return newKeyCacheWithCacheOptions(t, policy, cache.WithEvictFunc[string, cacheEntry](onEvict)) } // isReloadRequired returns true if the check interval has elapsed @@ -93,121 +163,145 @@ func isReloadRequired(entry cacheEntry, checkInterval time.Duration) bool { } // GetOrLoad returns a key from the cache if it's already been loaded. If the key -// is not present in the cache it will retrieve the key using the provided keyLoader +// is not present in the cache it will retrieve the key using the provided loader // and store the key if an error is not returned. -func (c *keyCache) GetOrLoad(id KeyMeta, loader keyLoader) (*internal.CryptoKey, error) { - // get with "light" lock - c.rw.RLock() - k, ok := c.get(id) - c.rw.RUnlock() - - if ok { - return k, nil - } - - // load with heavy lock +func (c *keyCache) GetOrLoad(id KeyMeta, loader func(KeyMeta) (*internal.CryptoKey, error)) (*cachedCryptoKey, error) { c.rw.Lock() defer c.rw.Unlock() - // exit early if the key doesn't need to be reloaded just in case it has been loaded by rw lock in front of us - if k, ok := c.get(id); ok { - return k, nil + + if k, ok := c.getFresh(id); ok { + return tracked(k), nil } - return c.load(id, loader) + k, err := c.load(id, loader) + if err != nil { + return nil, err + } + + return tracked(k), nil +} + +// tracked increments the reference count for the provided key, then returns it. +func tracked(key *cachedCryptoKey) *cachedCryptoKey { + key.increment() + return key } -// get returns a key from the cache if present AND fresh. +// getFresh returns a key from the cache if present AND fresh. // A cached value is considered stale if its time in cache // has exceeded the RevokeCheckInterval. // The second return value indicates the successful retrieval of a // fresh key. -func (c *keyCache) get(id KeyMeta) (*internal.CryptoKey, bool) { - key := cacheKey(id.ID, id.Created) - - if e, ok := c.read(key); ok && !isReloadRequired(e, c.policy.RevokeCheckInterval) { +func (c *keyCache) getFresh(meta KeyMeta) (*cachedCryptoKey, bool) { + if e, ok := c.read(meta); ok && !isReloadRequired(e, c.policy.RevokeCheckInterval) { return e.key, true + } else if ok { + log.Debugf("%s stale -- id: %s-%d\n", c, meta.ID, e.key.Created()) + return e.key, false } return nil, false } -// load returns a key from the cache if it's already been loaded. If the key is -// not present in the cache, or the cached entry needs to be reloaded, it will -// retrieve the key using the provided keyLoader and cache the key for future use. -// load maintains the latest entry for each distinct ID which can be accessed using -// id.Created == 0. -func (c *keyCache) load(id KeyMeta, loader keyLoader) (*internal.CryptoKey, error) { - key := cacheKey(id.ID, id.Created) - - k, err := loader.Load() +// load retrieves a key using the provided loader. If the key is present in the cache +// it will be updated with the latest revocation status and last loaded time. Otherwise +// a new cache entry will be created and stored in the cache. +// +// load maintains the latest entry for each distinct KeyMeta.ID which can be accessed using +// KeyMeta.Created == 0. +func (c *keyCache) load(meta KeyMeta, loader func(KeyMeta) (*internal.CryptoKey, error)) (*cachedCryptoKey, error) { + k, err := loader(meta) if err != nil { return nil, err } - e, ok := c.read(key) - if ok && e.key.Created() == k.Created() { + e, ok := c.read(meta) + + switch { + case ok: // existing key in cache. update revoked status and last loaded time and close key // we just loaded since we don't need it e.key.SetRevoked(k.Revoked()) e.loadedAt = time.Now() - c.write(key, e) k.Close() - } else { + default: // first time loading this key into cache or we have an ID-only key with mismatched // create timestamps e = newCacheEntry(k) - c.write(key, e) } - latestKey := cacheKey(id.ID, 0) - if key == latestKey { - // we've loaded a key using ID-only, ensure we've got a cache entry with a fully - // qualified cache key - c.write(cacheKey(id.ID, k.Created()), e) - } else if latest, ok := c.read(latestKey); !ok || latest.key.Created() < k.Created() { - // we've loaded a key using a fully qualified cache key and the ID-only entry is - // either missing or stale - c.write(latestKey, e) - } + c.write(meta, e) return e.key, nil } // read retrieves the entry from the cache matching the provided ID if present. The second // return value indicates whether or not the key was present in the cache. -func (c *keyCache) read(id string) (cacheEntry, bool) { - e, ok := c.keys[id] +func (c *keyCache) read(meta KeyMeta) (cacheEntry, bool) { + id := cacheKey(meta.ID, meta.Created) + + if meta.IsLatest() { + if latest, ok := c.getLatestKeyMeta(meta.ID); ok { + id = cacheKey(latest.ID, latest.Created) + } + } + e, ok := c.keys.Get(id) if !ok { log.Debugf("%s miss -- id: %s\n", c, id) + } else { + log.Debugf("%s hit -- id: %s\n", c, id) } return e, ok } +// getLatestKeyMeta returns the KeyMeta for the latest key for the provided ID. +// The second return value indicates whether or not the key was present in the cache. +func (c *keyCache) getLatestKeyMeta(id string) (KeyMeta, bool) { + latest, ok := c.latest[cacheKey(id, 0)] + + return latest, ok +} + +// mapLatestKeyMeta maps the provided latest KeyMeta to the provided ID. +func (c *keyCache) mapLatestKeyMeta(id string, latest KeyMeta) { + c.latest[cacheKey(id, 0)] = latest +} + // write entry e to the cache using id as the key. -func (c *keyCache) write(id string, e cacheEntry) { - if existing, ok := c.keys[id]; ok { +func (c *keyCache) write(meta KeyMeta, e cacheEntry) { + if meta.IsLatest() { + meta = KeyMeta{ID: meta.ID, Created: e.key.Created()} + + c.mapLatestKeyMeta(meta.ID, meta) + } else if latest, ok := c.getLatestKeyMeta(meta.ID); !ok || latest.Created < e.key.Created() { + c.mapLatestKeyMeta(meta.ID, meta) + } + + id := cacheKey(meta.ID, meta.Created) + + if existing, ok := c.keys.Get(id); ok { log.Debugf("%s update -> old: %s, new: %s, id: %s\n", c, existing.key, e.key, id) } log.Debugf("%s write -> key: %s, id: %s\n", c, e.key, id) - c.keys[id] = e + c.keys.Set(id, e) } // GetOrLoadLatest returns the latest key from the cache matching the provided ID // if it's already been loaded. If the key is not present in the cache it will -// retrieve the key using the provided KeyLoader and store the key if an error is not returned. -// If the provided loader implements the optional keyReloader interface then retrieved keys -// will be inspected for validity and reloaded if necessary. -func (c *keyCache) GetOrLoadLatest(id string, loader keyLoader) (*internal.CryptoKey, error) { +// retrieve the key using the provided KeyLoader and store the key if successful. +// In the event that the cached or loaded key is invalid (see [keyCache.IsInvalid]), +// the key will be reloaded and the cache updated. +func (c *keyCache) GetOrLoadLatest(id string, loader func(KeyMeta) (*internal.CryptoKey, error)) (*cachedCryptoKey, error) { c.rw.Lock() defer c.rw.Unlock() meta := KeyMeta{ID: id} - key, ok := c.get(meta) + key, ok := c.getFresh(meta) if !ok { log.Debugf("%s.GetOrLoadLatest get miss -- id: %s\n", c, id) @@ -219,64 +313,68 @@ func (c *keyCache) GetOrLoadLatest(id string, loader keyLoader) (*internal.Crypt } } - if reloader, ok := loader.(keyReloader); ok && reloader.IsInvalid(key) { - reloaded, ok := loader.Load() + if c.IsInvalid(key.CryptoKey) { + reloaded, err := loader(meta) + if err != nil { + return nil, err + } + log.Debugf("%s.GetOrLoadLatest reload -- invalid: %s, new: %s, id: %s\n", c, key, reloaded, id) e := newCacheEntry(reloaded) - // update latest - latest := cacheKey(id, 0) - c.write(latest, e) - // ensure we've got a cache entry with a fully qualified cache key - c.write(cacheKey(id, reloaded.Created()), e) + c.write(KeyMeta{ID: id, Created: reloaded.Created()}, e) - return reloaded, ok + return tracked(e.key), nil } - return key, nil + return tracked(key), nil +} + +// IsInvalid returns true if the provided key is no longer valid. +func (c *keyCache) IsInvalid(key *internal.CryptoKey) bool { + return internal.IsKeyInvalid(key, c.policy.ExpireKeyAfter) } // Close frees all memory locked by the keys in this cache. // It MUST be called after a session is complete to avoid // running into MEMLOCK limits. func (c *keyCache) Close() error { - c.once.Do(c.close) - - return nil -} - -func (c *keyCache) close() { - c.rw.Lock() - defer c.rw.Unlock() - - for k := range c.keys { - c.keys[k].key.Close() - } + return c.keys.Close() } +// String returns a string representation of this cache. func (c *keyCache) String() string { return fmt.Sprintf("keyCache(%p)", c) } // Verify neverCache implements the cache interface. -var _ cache = (*neverCache)(nil) +var _ keyCacher = (*neverCache)(nil) -type neverCache struct { -} +type neverCache struct{} // GetOrLoad always executes the provided function to load the value. It never actually caches. -func (neverCache) GetOrLoad(id KeyMeta, loader keyLoader) (*internal.CryptoKey, error) { - return loader.Load() +func (neverCache) GetOrLoad(id KeyMeta, loader func(KeyMeta) (*internal.CryptoKey, error)) (*cachedCryptoKey, error) { + k, err := loader(id) + if err != nil { + return nil, err + } + + return &cachedCryptoKey{CryptoKey: k}, nil } // GetOrLoadLatest always executes the provided function to load the latest value. It never actually caches. -func (neverCache) GetOrLoadLatest(id string, loader keyLoader) (*internal.CryptoKey, error) { - return loader.Load() +func (neverCache) GetOrLoadLatest(id string, loader func(KeyMeta) (*internal.CryptoKey, error)) (*cachedCryptoKey, error) { + k, err := loader(KeyMeta{ID: id}) + if err != nil { + return nil, err + } + + return &cachedCryptoKey{CryptoKey: k}, nil } -// Close is a no-op function to satisfy the cache interface +// Close is a no-op function to satisfy the cache interface. func (neverCache) Close() error { return nil } diff --git a/go/appencryption/key_cache_benchmark_test.go b/go/appencryption/key_cache_benchmark_test.go index 7c4ff727d..434e32b04 100644 --- a/go/appencryption/key_cache_benchmark_test.go +++ b/go/appencryption/key_cache_benchmark_test.go @@ -1,6 +1,7 @@ package appencryption import ( + "flag" "fmt" "sync/atomic" "testing" @@ -11,28 +12,38 @@ import ( "github.com/stretchr/testify/assert" "github.com/godaddy/asherah/go/appencryption/internal" + "github.com/godaddy/asherah/go/appencryption/pkg/log" ) var ( secretFactory = new(memguard.SecretFactory) created = time.Now().Unix() + enableDebug = flag.Bool("debug", false, "enable debug logging") ) +func ConfigureLogging() { + if *enableDebug { + log.SetLogger(logger{}) + } +} + func BenchmarkKeyCache_GetOrLoad_MultipleThreadsReadExistingKey(b *testing.B) { - c := newKeyCache(NewCryptoPolicy()) + ConfigureLogging() + + c := newKeyCache(CacheTypeIntermediateKeys, NewCryptoPolicy()) - c.keys[cacheKey(testKey, created)] = cacheEntry{ - key: internal.NewCryptoKeyForTest(created, false), + c.keys.Set(cacheKey(testKey, created), cacheEntry{ + key: &cachedCryptoKey{CryptoKey: internal.NewCryptoKeyForTest(created, false)}, loadedAt: time.Now(), - } + }) b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { - key, err := c.GetOrLoad(KeyMeta{testKey, created}, keyLoaderFunc(func() (key *internal.CryptoKey, e error) { + key, err := c.GetOrLoad(KeyMeta{testKey, created}, func(_ KeyMeta) (key *internal.CryptoKey, e error) { // The passed function is irrelevant because we'll always find the value in the cache - return nil, nil - })) + return nil, errors.New("loader should not be executed") + }) assert.NoError(b, err) assert.Equal(b, created, key.Created()) @@ -41,55 +52,77 @@ func BenchmarkKeyCache_GetOrLoad_MultipleThreadsReadExistingKey(b *testing.B) { } func BenchmarkKeyCache_GetOrLoad_MultipleThreadsWriteSameKey(b *testing.B) { - c := newKeyCache(NewCryptoPolicy()) + ConfigureLogging() + + c := newKeyCache(CacheTypeIntermediateKeys, NewCryptoPolicy()) b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { - _, err := c.GetOrLoad(KeyMeta{testKey, created}, keyLoaderFunc(func() (key *internal.CryptoKey, e error) { + _, err := c.GetOrLoad(KeyMeta{testKey, created}, func(_ KeyMeta) (key *internal.CryptoKey, e error) { // Add a delay to simulate time spent in performing a metastore read time.Sleep(5 * time.Millisecond) return internal.NewCryptoKeyForTest(created, false), nil - })) + }) assert.NoError(b, err) - assert.Equal(b, created, c.keys[cacheKey(testKey, 0)].key.Created()) + + latest, _ := c.getLatestKeyMeta(testKey) + latestKey := cacheKey(latest.ID, latest.Created) + + assert.Equal(b, created, c.keys.GetOrPanic(latestKey).key.Created()) } }) } +type logger struct{} + +func (logger) Debugf(format string, v ...interface{}) { + fmt.Printf(format, v...) +} + func BenchmarkKeyCache_GetOrLoad_MultipleThreadsWriteUniqueKeys(b *testing.B) { + ConfigureLogging() + var ( - c = newKeyCache(NewCryptoPolicy()) + c = newKeyCache(CacheTypeIntermediateKeys, NewCryptoPolicy()) i int64 ) b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { - curr := atomic.AddInt64(&i, 1) - _, err := c.GetOrLoad(KeyMeta{cacheKey(testKey, curr), created}, keyLoaderFunc(func() (key *internal.CryptoKey, e error) { + curr := atomic.AddInt64(&i, 1) - 1 + + loader := func(_ KeyMeta) (key *internal.CryptoKey, e error) { // Add a delay to simulate time spent in performing a metastore read - time.Sleep(5 * time.Millisecond) return internal.NewCryptoKeyForTest(created, false), nil - })) + } + + keyID := fmt.Sprintf("%s-%d", testKey, curr) + + _, err := c.GetOrLoad(KeyMeta{keyID, created}, loader) assert.NoError(b, err) // ensure we have a "latest" entry for this key as well - latest, err := c.GetOrLoadLatest(cacheKey(testKey, curr), keyLoaderFunc(func() (*internal.CryptoKey, error) { - return nil, errors.New("loader should not be executed") - })) + latest, err := c.GetOrLoadLatest(keyID, loader) assert.NoError(b, err) assert.NotNil(b, latest) } }) assert.NotNil(b, c.keys) - assert.Equal(b, i*2, int64(len(c.keys))) + + expected := i + if expected > DefaultKeyCacheMaxSize { + expected = DefaultKeyCacheMaxSize + } + + assert.Equal(b, expected, int64(c.keys.Len())) } func BenchmarkKeyCache_GetOrLoad_MultipleThreadsReadRevokedKey(b *testing.B) { var ( - c = newKeyCache(NewCryptoPolicy()) + c = newKeyCache(CacheTypeIntermediateKeys, NewCryptoPolicy()) created = time.Now().Add(-(time.Minute * 100)).Unix() ) @@ -98,38 +131,35 @@ func BenchmarkKeyCache_GetOrLoad_MultipleThreadsReadRevokedKey(b *testing.B) { assert.NoError(b, err) cacheEntry := cacheEntry{ - key: key, + key: &cachedCryptoKey{CryptoKey: key}, loadedAt: time.Unix(created, 0), } defer c.Close() - c.keys[cacheKey(testKey, created)] = cacheEntry + c.keys.Set(cacheKey(testKey, created), cacheEntry) + c.mapLatestKeyMeta(testKey, KeyMeta{testKey, created}) b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { - _, err := c.GetOrLoad(KeyMeta{testKey, created}, keyLoaderFunc(func() (key *internal.CryptoKey, e error) { - // Add a delay to simulate time spent in performing a metastore read - time.Sleep(5 * time.Millisecond) - key, err2 := internal.NewCryptoKey(secretFactory, created, true, []byte("testing")) - if err2 != nil { - return nil, err2 - } - - return key, nil - })) + _, err := c.GetOrLoad(KeyMeta{testKey, created}, func(_ KeyMeta) (key *internal.CryptoKey, e error) { + return internal.NewCryptoKey(secretFactory, created, true, []byte("testing")) + }) assert.NoError(b, err) - assert.Equal(b, created, c.keys[cacheKey(testKey, 0)].key.Created()) - assert.True(b, c.keys[cacheKey(testKey, 0)].key.Revoked()) - assert.True(b, c.keys[cacheKey(testKey, created)].key.Revoked()) + + latest, _ := c.getLatestKeyMeta(testKey) + latestKey := cacheKey(latest.ID, latest.Created) + assert.Equal(b, created, c.keys.GetOrPanic(latestKey).key.Created()) + assert.True(b, c.keys.GetOrPanic(latestKey).key.Revoked()) + assert.True(b, c.keys.GetOrPanic(cacheKey(testKey, created)).key.Revoked()) } }) } func BenchmarkKeyCache_GetOrLoad_MultipleThreadsRead_NeedReloadKey(b *testing.B) { var ( - c = newKeyCache(NewCryptoPolicy()) + c = newKeyCache(CacheTypeIntermediateKeys, NewCryptoPolicy()) created = time.Now().Add(-(time.Minute * 100)).Unix() ) @@ -138,24 +168,22 @@ func BenchmarkKeyCache_GetOrLoad_MultipleThreadsRead_NeedReloadKey(b *testing.B) assert.NoError(b, err) cacheEntry := cacheEntry{ - key: key, + key: &cachedCryptoKey{CryptoKey: key}, loadedAt: time.Unix(created, 0), } defer c.Close() - c.keys[cacheKey(testKey, created)] = cacheEntry + + c.keys.Set(cacheKey(testKey, created), cacheEntry) + c.mapLatestKeyMeta(testKey, KeyMeta{testKey, created}) b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { - k, err := c.GetOrLoad(KeyMeta{testKey, created}, keyLoaderFunc(func() (*internal.CryptoKey, error) { + k, err := c.GetOrLoad(KeyMeta{testKey, created}, func(_ KeyMeta) (*internal.CryptoKey, error) { // Note: this function should only happen on first load (although could execute more than once currently), if it doesn't, then something is broken - - // Add a delay to simulate time spent in performing a metastore read - time.Sleep(5 * time.Millisecond) - return internal.NewCryptoKey(secretFactory, created, false, []byte("testing")) - })) + }) if err != nil { b.Error(err) @@ -168,51 +196,54 @@ func BenchmarkKeyCache_GetOrLoad_MultipleThreadsRead_NeedReloadKey(b *testing.B) } func BenchmarkKeyCache_GetOrLoad_MultipleThreadsReadUniqueKeys(b *testing.B) { - var ( - c = newKeyCache(NewCryptoPolicy()) - i int64 - ) + c := newKeyCache(CacheTypeIntermediateKeys, NewCryptoPolicy()) - for ; i < int64(b.N); i++ { - c.keys[cacheKey(fmt.Sprintf(testKey+"-%d", i), created)] = cacheEntry{ - key: internal.NewCryptoKeyForTest(created, false), + for i := 0; i < b.N && i < DefaultKeyCacheMaxSize; i++ { + keyID := fmt.Sprintf(testKey+"-%d", i) + meta := KeyMeta{ID: keyID, Created: created} + + c.mapLatestKeyMeta(meta.ID, meta) + c.keys.Set(cacheKey(meta.ID, meta.Created), cacheEntry{ + key: &cachedCryptoKey{CryptoKey: internal.NewCryptoKeyForTest(created, false), refs: 1}, loadedAt: time.Now(), - } + }) } - i = 0 + i := atomic.Int64{} b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { - curr := atomic.LoadInt64(&i) - key, err := c.GetOrLoad(KeyMeta{cacheKey(testKey, curr), created}, keyLoaderFunc(func() (key *internal.CryptoKey, e error) { + curr := i.Add(1) - 1 + curr = curr % DefaultKeyCacheMaxSize + + id := fmt.Sprintf(testKey+"-%d", curr) + key, err := c.GetOrLoad(KeyMeta{id, created}, func(_ KeyMeta) (key *internal.CryptoKey, e error) { // The passed function is irrelevant because we'll always find the value in the cache - return nil, nil - })) + return nil, errors.New(fmt.Sprintf("loader should not be executed for id=%s", id)) + }) assert.NoError(b, err) assert.Equal(b, created, key.Created()) - - atomic.AddInt64(&i, 1) } }) } func BenchmarkKeyCache_GetOrLoadLatest_MultipleThreadsReadExistingKey(b *testing.B) { - c := newKeyCache(NewCryptoPolicy()) + c := newKeyCache(CacheTypeIntermediateKeys, NewCryptoPolicy()) - c.keys[cacheKey(testKey, 0)] = cacheEntry{ - key: internal.NewCryptoKeyForTest(created, false), + c.mapLatestKeyMeta(testKey, KeyMeta{testKey, created}) + c.keys.Set(cacheKey(testKey, created), cacheEntry{ + key: &cachedCryptoKey{CryptoKey: internal.NewCryptoKeyForTest(created, false)}, loadedAt: time.Now(), - } + }) b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { - key, err := c.GetOrLoadLatest(testKey, keyLoaderFunc(func() (*internal.CryptoKey, error) { + key, err := c.GetOrLoadLatest(testKey, func(_ KeyMeta) (*internal.CryptoKey, error) { // The passed function is irrelevant because we'll always find the value in the cache return nil, nil - })) + }) assert.NoError(b, err) assert.Equal(b, created, key.Created()) } @@ -220,61 +251,62 @@ func BenchmarkKeyCache_GetOrLoadLatest_MultipleThreadsReadExistingKey(b *testing } func BenchmarkKeyCache_GetOrLoadLatest_MultipleThreadsWriteSameKey(b *testing.B) { - c := newKeyCache(NewCryptoPolicy()) + c := newKeyCache(CacheTypeIntermediateKeys, NewCryptoPolicy()) b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { - _, err := c.GetOrLoadLatest(testKey, keyLoaderFunc(func() (*internal.CryptoKey, error) { + _, err := c.GetOrLoadLatest(testKey, func(_ KeyMeta) (*internal.CryptoKey, error) { // Add a delay to simulate time spent in performing a metastore read time.Sleep(5 * time.Millisecond) return internal.NewCryptoKeyForTest(created, false), nil - })) + }) assert.NoError(b, err) - assert.Equal(b, created, c.keys[cacheKey(testKey, 0)].key.Created()) + + latest, _ := c.getLatestKeyMeta(testKey) + latestKey := cacheKey(latest.ID, latest.Created) + assert.Equal(b, created, c.keys.GetOrPanic(latestKey).key.Created()) } }) } func BenchmarkKeyCache_GetOrLoadLatest_MultipleThreadsWriteUniqueKey(b *testing.B) { var ( - c = newKeyCache(NewCryptoPolicy()) + c = newKeyCache(CacheTypeIntermediateKeys, NewCryptoPolicy()) i int64 ) b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { - curr := atomic.AddInt64(&i, 1) - _, err := c.GetOrLoadLatest(cacheKey(testKey, curr), keyLoaderFunc(func() (*internal.CryptoKey, error) { - // Add a delay to simulate time spent in performing a metastore read - time.Sleep(5 * time.Millisecond) - + curr := atomic.AddInt64(&i, 1) - 1 + _, err := c.GetOrLoadLatest(cacheKey(testKey, curr), func(_ KeyMeta) (*internal.CryptoKey, error) { return internal.NewCryptoKeyForTest(created, false), nil - })) + }) assert.NoError(b, err) - - // ensure we actually have a "latest" entry for this key in the cache - latest, err := c.GetOrLoadLatest(cacheKey(testKey, curr), keyLoaderFunc(func() (*internal.CryptoKey, error) { - return nil, errors.New("loader should not be executed") - })) - assert.NoError(b, err) - assert.NotNil(b, latest) } }) assert.NotNil(b, c.keys) - assert.Equal(b, i*2, int64(len(c.keys))) + + expected := i + if expected > DefaultKeyCacheMaxSize { + expected = DefaultKeyCacheMaxSize + } + + assert.Equal(b, expected, int64(c.keys.Len())) } -func BenchmarkKeyCache_GetOrLoadLatest_MultipleThreadsReadRevokedKey(b *testing.B) { +func BenchmarkKeyCache_GetOrLoadLatest_MultipleThreadsReadStaleRevokedKey(b *testing.B) { + ConfigureLogging() + var ( - c = newKeyCache(NewCryptoPolicy()) + c = newKeyCache(CacheTypeIntermediateKeys, NewCryptoPolicy()) created = time.Now().Add(-(time.Minute * 100)).Unix() ) key, err := internal.NewCryptoKey(secretFactory, created, false, []byte("testing")) cacheEntry := cacheEntry{ - key: key, + key: &cachedCryptoKey{CryptoKey: key, refs: 1}, loadedAt: time.Unix(created, 0), } @@ -282,53 +314,102 @@ func BenchmarkKeyCache_GetOrLoadLatest_MultipleThreadsReadRevokedKey(b *testing. defer c.Close() - c.keys[cacheKey(testKey, 0)] = cacheEntry + meta := KeyMeta{ID: testKey, Created: created} + c.mapLatestKeyMeta(testKey, meta) + c.keys.Set(cacheKey(meta.ID, meta.Created), cacheEntry) b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { - _, err := c.GetOrLoadLatest(testKey, keyLoaderFunc(func() (key *internal.CryptoKey, e error) { - // Add a delay to simulate time spent in performing a metastore read - time.Sleep(5 * time.Millisecond) - - return internal.NewCryptoKey(secretFactory, created, true, []byte("testing")) - })) + key, err := c.GetOrLoadLatest(testKey, func(_ KeyMeta) (key *internal.CryptoKey, e error) { + return internal.NewCryptoKey(secretFactory, time.Now().Unix(), true, []byte("testing")) + }) assert.NoError(b, err) - assert.Equal(b, created, c.keys[cacheKey(testKey, 0)].key.Created()) - assert.True(b, c.keys[cacheKey(testKey, 0)].key.Revoked()) + assert.True(b, key.Revoked()) + assert.Greater(b, key.Created(), created) } }) } -func BenchmarkKeyCache_GetOrLoadLatest_MultipleThreadsReadUniqueKeys(b *testing.B) { +func BenchmarkKeyCache_GetOrLoadLatest_MultipleThreadsReadRevokedKey(b *testing.B) { + ConfigureLogging() + var ( - c = newKeyCache(NewCryptoPolicy()) - i int64 + c = newKeyCache(CacheTypeIntermediateKeys, NewCryptoPolicy()) + created = time.Now().Unix() ) - for ; i < int64(b.N); i++ { - c.keys[cacheKey(fmt.Sprintf(testKey+"-%d", i), 0)] = cacheEntry{ - key: internal.NewCryptoKeyForTest(created, false), - loadedAt: time.Now(), + key, err := internal.NewCryptoKey(secretFactory, created, true, []byte("testing")) + cacheEntry := cacheEntry{ + key: &cachedCryptoKey{CryptoKey: key, refs: 1}, + loadedAt: time.Unix(created, 0), + } + + assert.NoError(b, err) + + defer c.Close() + + meta := KeyMeta{ID: testKey, Created: created} + c.mapLatestKeyMeta(testKey, meta) + c.keys.Set(cacheKey(meta.ID, meta.Created), cacheEntry) + + count := atomic.Int64{} + reloadCount := atomic.Int64{} + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + count.Add(1) + + key, err := c.GetOrLoadLatest(testKey, func(_ KeyMeta) (key *internal.CryptoKey, e error) { + reloadCount.Add(1) + + return internal.NewCryptoKey(secretFactory, time.Now().Unix(), false, []byte("testing")) + }) + + assert.NoError(b, err) + assert.False(b, key.Revoked()) } + }) +} + +func BenchmarkKeyCache_GetOrLoadLatest_MultipleThreadsReadUniqueKeys(b *testing.B) { + ConfigureLogging() + + c := newKeyCache(CacheTypeIntermediateKeys, NewCryptoPolicy()) + + for i := 0; i < b.N && i < DefaultKeyCacheMaxSize; i++ { + keyID := fmt.Sprintf(testKey+"-%d", i) + meta := KeyMeta{ID: keyID, Created: created} + c.mapLatestKeyMeta(keyID, meta) + c.keys.Set(cacheKey(meta.ID, meta.Created), cacheEntry{ + key: &cachedCryptoKey{CryptoKey: internal.NewCryptoKeyForTest(created, false)}, + loadedAt: time.Now(), + }) } - i = 0 + i := atomic.Int64{} b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { - curr := atomic.LoadInt64(&i) - key, err := c.GetOrLoadLatest(fmt.Sprintf(testKey+"-%d", curr), keyLoaderFunc(func() (key *internal.CryptoKey, e error) { + curr := i.Add(1) - 1 + curr = curr % DefaultKeyCacheMaxSize + + keyID := fmt.Sprintf(testKey+"-%d", curr) + + key, err := c.GetOrLoadLatest(keyID, func(_ KeyMeta) (key *internal.CryptoKey, e error) { // The passed function is irrelevant because we'll always find the value in the cache - return nil, nil - })) + return nil, errors.New(fmt.Sprintf("loader should not be executed for id=%s", keyID)) + }) + if err != nil { + b.Error(err) + } - assert.NoError(b, err) assert.Equal(b, created, key.Created()) - atomic.AddInt64(&i, 1) + key.Close() } }) } diff --git a/go/appencryption/key_cache_test.go b/go/appencryption/key_cache_test.go index 875801aff..2a86dcf78 100644 --- a/go/appencryption/key_cache_test.go +++ b/go/appencryption/key_cache_test.go @@ -10,7 +10,6 @@ import ( "github.com/pkg/errors" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" @@ -30,7 +29,7 @@ type CacheTestSuite struct { func (suite *CacheTestSuite) SetupTest() { suite.policy = NewCryptoPolicy() - suite.keyCache = newKeyCache(suite.policy) + suite.keyCache = newKeyCache(CacheTypeIntermediateKeys, suite.policy) suite.created = time.Now().Unix() } @@ -46,13 +45,14 @@ func (suite *CacheTestSuite) Test_CacheKey() { } func (suite *CacheTestSuite) Test_NewKeyCache() { - cache := newKeyCache(NewCryptoPolicy()) + cache := newKeyCache(CacheTypeIntermediateKeys, NewCryptoPolicy()) defer cache.Close() assert.NotNil(suite.T(), cache) assert.IsType(suite.T(), new(keyCache), cache) assert.NotNil(suite.T(), cache.keys) assert.NotNil(suite.T(), cache.policy) + assert.Equal(suite.T(), DefaultKeyCacheMaxSize, cache.keys.Capacity()) } func (suite *CacheTestSuite) Test_IsReloadRequired_WithIntervalNotElapsed() { @@ -60,7 +60,7 @@ func (suite *CacheTestSuite) Test_IsReloadRequired_WithIntervalNotElapsed() { if assert.NoError(suite.T(), err) { entry := cacheEntry{ loadedAt: time.Now(), - key: key, + key: &cachedCryptoKey{CryptoKey: key}, } defer key.Close() @@ -74,7 +74,7 @@ func (suite *CacheTestSuite) Test_IsReloadRequired_WithIntervalElapsed() { if assert.NoError(suite.T(), err) { entry := cacheEntry{ loadedAt: time.Now().Add(-2 * time.Hour), - key: key, + key: &cachedCryptoKey{CryptoKey: key}, } defer key.Close() @@ -89,7 +89,7 @@ func (suite *CacheTestSuite) Test_IsReloadRequired_WithRevoked() { entry := cacheEntry{ // Note this loadedAt would normally require reload loadedAt: time.Now().Add(-2 * time.Hour), - key: key, + key: &cachedCryptoKey{CryptoKey: key}, } defer key.Close() @@ -99,16 +99,16 @@ func (suite *CacheTestSuite) Test_IsReloadRequired_WithRevoked() { } func (suite *CacheTestSuite) TestKeyCache_GetOrLoad_WithCachedKeyNoReloadRequired() { - _, err := suite.keyCache.GetOrLoad(KeyMeta{testKey, suite.created}, keyLoaderFunc(func() (key *internal.CryptoKey, e error) { + _, err := suite.keyCache.GetOrLoad(KeyMeta{testKey, suite.created}, func(_ KeyMeta) (key *internal.CryptoKey, e error) { cryptoKey, err := internal.NewCryptoKey(secretFactory, suite.created, false, []byte("blah")) return cryptoKey, err - })) + }) assert.NoError(suite.T(), err) - key, err := suite.keyCache.GetOrLoad(KeyMeta{testKey, suite.created}, keyLoaderFunc(func() (*internal.CryptoKey, error) { + key, err := suite.keyCache.GetOrLoad(KeyMeta{testKey, suite.created}, func(_ KeyMeta) (*internal.CryptoKey, error) { return nil, errors.New("should not be called") - })) + }) assert.NoError(suite.T(), err) assert.NotNil(suite.T(), key) @@ -116,84 +116,86 @@ func (suite *CacheTestSuite) TestKeyCache_GetOrLoad_WithCachedKeyNoReloadRequire } func (suite *CacheTestSuite) TestKeyCache_GetOrLoad_WithEmptyCache() { - key, err := suite.keyCache.GetOrLoad(KeyMeta{testKey, suite.created}, keyLoaderFunc(func() (*internal.CryptoKey, error) { - cryptoKey, err := internal.NewCryptoKey(secretFactory, suite.created, false, []byte("blah")) - if err != nil { - return nil, err - } - return cryptoKey, nil - })) + meta := KeyMeta{ID: testKey, Created: suite.created} + key, err := suite.keyCache.GetOrLoad(meta, func(_ KeyMeta) (*internal.CryptoKey, error) { + return internal.NewCryptoKey(secretFactory, suite.created, false, []byte("blah")) + }) assert.NoError(suite.T(), err) assert.NotNil(suite.T(), key) assert.Equal(suite.T(), suite.created, key.Created()) - assert.Equal(suite.T(), suite.created, suite.keyCache.keys[cacheKey(testKey, 0)].key.Created()) + + latestKey, _ := suite.keyCache.getLatestKeyMeta(testKey) + assert.Equal(suite.T(), latestKey, meta) } func (suite *CacheTestSuite) TestKeyCache_GetOrLoad_DoesNotSetKeyOnError() { - key, err := suite.keyCache.GetOrLoad(KeyMeta{}, keyLoaderFunc(func() (*internal.CryptoKey, error) { + key, err := suite.keyCache.GetOrLoad(KeyMeta{}, func(_ KeyMeta) (*internal.CryptoKey, error) { return new(internal.CryptoKey), errors.New("error") - })) + }) if assert.Error(suite.T(), err) { assert.Nil(suite.T(), key) - assert.Empty(suite.T(), suite.keyCache.keys) + assert.Zero(suite.T(), suite.keyCache.keys.Len()) } } func (suite *CacheTestSuite) TestKeyCache_GetOrLoad_WithOldCachedKeyLoadNewerUpdatesLatest() { olderCreated := time.Now().Add(-(time.Hour * 24)).Unix() - _, err := suite.keyCache.GetOrLoad(KeyMeta{testKey, olderCreated}, keyLoaderFunc(func() (key *internal.CryptoKey, e error) { + _, err := suite.keyCache.GetOrLoad(KeyMeta{testKey, olderCreated}, func(_ KeyMeta) (key *internal.CryptoKey, e error) { cryptoKey, err := internal.NewCryptoKey(secretFactory, olderCreated, false, []byte("blah")) if err != nil { return nil, err } return cryptoKey, nil - })) + }) assert.NoError(suite.T(), err) - key, err := suite.keyCache.GetOrLoad(KeyMeta{testKey, suite.created}, keyLoaderFunc(func() (*internal.CryptoKey, error) { + key, err := suite.keyCache.GetOrLoad(KeyMeta{testKey, suite.created}, func(_ KeyMeta) (*internal.CryptoKey, error) { cryptoKey, err2 := internal.NewCryptoKey(secretFactory, suite.created, false, []byte("newerblah")) if err2 != nil { return nil, err2 } return cryptoKey, nil - })) + }) assert.NoError(suite.T(), err) assert.NotNil(suite.T(), key) assert.Equal(suite.T(), suite.created, key.Created()) - assert.Equal(suite.T(), suite.created, suite.keyCache.keys[cacheKey(testKey, 0)].key.Created()) - assert.Equal(suite.T(), suite.created, suite.keyCache.keys[cacheKey(testKey, suite.created)].key.Created()) - assert.Equal(suite.T(), olderCreated, suite.keyCache.keys[cacheKey(testKey, olderCreated)].key.Created()) + + latestKey, _ := suite.keyCache.getLatestKeyMeta(testKey) + assert.Equal(suite.T(), latestKey, KeyMeta{ID: testKey, Created: key.Created()}) + + assert.Equal(suite.T(), suite.created, suite.keyCache.keys.GetOrPanic(cacheKey(testKey, suite.created)).key.Created()) + assert.Equal(suite.T(), olderCreated, suite.keyCache.keys.GetOrPanic(cacheKey(testKey, olderCreated)).key.Created()) } func (suite *CacheTestSuite) TestKeyCache_GetOrLoad_WithCachedKeyReloadRequiredAndNowRevoked() { key, err := internal.NewCryptoKey(secretFactory, suite.created, false, []byte("blah")) if assert.NoError(suite.T(), err) { entry := cacheEntry{ - key: key, + key: &cachedCryptoKey{CryptoKey: key}, loadedAt: time.Now().Add(-2 * suite.policy.RevokeCheckInterval), } - suite.keyCache.keys[cacheKey(testKey, suite.created)] = entry - suite.keyCache.keys[cacheKey(testKey, 0)] = entry + suite.keyCache.keys.Set(cacheKey(testKey, suite.created), entry) + suite.keyCache.keys.Set(cacheKey(testKey, 0), entry) revokedKey, e := internal.NewCryptoKey(secretFactory, suite.created, true, []byte("blah")) if assert.NoError(suite.T(), e) { - key, err := suite.keyCache.GetOrLoad(KeyMeta{testKey, suite.created}, keyLoaderFunc(func() (*internal.CryptoKey, error) { + key, err := suite.keyCache.GetOrLoad(KeyMeta{testKey, suite.created}, func(_ KeyMeta) (*internal.CryptoKey, error) { return revokedKey, nil - })) + }) assert.NoError(suite.T(), err) assert.NotNil(suite.T(), key) assert.Equal(suite.T(), suite.created, key.Created()) assert.True(suite.T(), key.Revoked()) - assert.True(suite.T(), suite.keyCache.keys[cacheKey(testKey, 0)].key.Revoked()) + assert.True(suite.T(), suite.keyCache.keys.GetOrPanic(cacheKey(testKey, 0)).key.Revoked()) // Verify we closed the new one we loaded and kept the cached one open assert.True(suite.T(), revokedKey.IsClosed()) - assert.False(suite.T(), suite.keyCache.keys[cacheKey(testKey, suite.created)].key.IsClosed()) + assert.False(suite.T(), suite.keyCache.keys.GetOrPanic(cacheKey(testKey, suite.created)).key.IsClosed()) } } } @@ -204,44 +206,44 @@ func (suite *CacheTestSuite) TestKeyCache_GetOrLoad_WithCachedKeyReloadRequiredB if assert.NoError(suite.T(), err) { entry := cacheEntry{ - key: key, + key: &cachedCryptoKey{CryptoKey: key}, loadedAt: time.Unix(created, 0), } - suite.keyCache.keys[cacheKey(testKey, created)] = entry - suite.keyCache.keys[cacheKey(testKey, 0)] = entry + suite.keyCache.keys.Set(cacheKey(testKey, created), entry) + suite.keyCache.keys.Set(cacheKey(testKey, 0), entry) reloadedKey, e := internal.NewCryptoKey(secretFactory, created, false, []byte("blah")) assert.NoError(suite.T(), e) - key, err := suite.keyCache.GetOrLoad(KeyMeta{testKey, created}, keyLoaderFunc(func() (*internal.CryptoKey, error) { + key, err := suite.keyCache.GetOrLoad(KeyMeta{testKey, created}, func(_ KeyMeta) (*internal.CryptoKey, error) { return reloadedKey, nil - })) + }) assert.NoError(suite.T(), err) assert.NotNil(suite.T(), key) assert.Equal(suite.T(), created, key.Created()) - assert.Greater(suite.T(), suite.keyCache.keys[cacheKey(testKey, created)].loadedAt.Unix(), created) + assert.Greater(suite.T(), suite.keyCache.keys.GetOrPanic(cacheKey(testKey, created)).loadedAt.Unix(), created) // Verify we closed the new one we loaded and kept the cached one open assert.True(suite.T(), reloadedKey.IsClosed()) - assert.False(suite.T(), suite.keyCache.keys[cacheKey(testKey, created)].key.IsClosed()) + assert.False(suite.T(), suite.keyCache.keys.GetOrPanic(cacheKey(testKey, created)).key.IsClosed()) } } func (suite *CacheTestSuite) TestKeyCache_GetOrLoadLatest_WithCachedKeyNoReloadRequired() { - _, err := suite.keyCache.GetOrLoad(KeyMeta{testKey, suite.created}, keyLoaderFunc(func() (key *internal.CryptoKey, e error) { + _, err := suite.keyCache.GetOrLoad(KeyMeta{testKey, suite.created}, func(_ KeyMeta) (key *internal.CryptoKey, e error) { cryptoKey, err := internal.NewCryptoKey(secretFactory, suite.created, false, []byte("blah")) if err != nil { return nil, err } return cryptoKey, nil - })) + }) assert.NoError(suite.T(), err) - key, err := suite.keyCache.GetOrLoadLatest(testKey, keyLoaderFunc(func() (*internal.CryptoKey, error) { + key, err := suite.keyCache.GetOrLoadLatest(testKey, func(_ KeyMeta) (*internal.CryptoKey, error) { return nil, errors.New("should not be called") - })) + }) assert.NoError(suite.T(), err) assert.NotNil(suite.T(), key) @@ -249,110 +251,95 @@ func (suite *CacheTestSuite) TestKeyCache_GetOrLoadLatest_WithCachedKeyNoReloadR } func (suite *CacheTestSuite) TestKeyCache_GetOrLoadLatest_WithEmptyCache() { - key, err := suite.keyCache.GetOrLoadLatest(testKey, keyLoaderFunc(func() (*internal.CryptoKey, error) { - cryptoKey, err := internal.NewCryptoKey(secretFactory, suite.created, false, []byte("blah")) - if err != nil { - return nil, err - } - return cryptoKey, nil - })) + key, err := suite.keyCache.GetOrLoadLatest(testKey, func(_ KeyMeta) (*internal.CryptoKey, error) { + return internal.NewCryptoKey(secretFactory, suite.created, false, []byte("blah")) + }) assert.NoError(suite.T(), err) assert.NotNil(suite.T(), key) assert.Equal(suite.T(), suite.created, key.Created()) - assert.Equal(suite.T(), suite.created, suite.keyCache.keys[cacheKey(testKey, 0)].key.Created()) + + latestKey, _ := suite.keyCache.getLatestKeyMeta(testKey) + assert.Equal(suite.T(), latestKey, KeyMeta{ID: testKey, Created: suite.created}) } func (suite *CacheTestSuite) TestKeyCache_GetOrLoadLatest_DoesNotSetKeyOnError() { - key, err := suite.keyCache.GetOrLoadLatest(testKey, keyLoaderFunc(func() (*internal.CryptoKey, error) { + key, err := suite.keyCache.GetOrLoadLatest(testKey, func(_ KeyMeta) (*internal.CryptoKey, error) { return new(internal.CryptoKey), errors.New("error") - })) + }) if assert.Error(suite.T(), err) { assert.Nil(suite.T(), key) - assert.Empty(suite.T(), suite.keyCache.keys) + assert.Zero(suite.T(), suite.keyCache.keys.Len()) } } func (suite *CacheTestSuite) TestKeyCache_GetOrLoadLatest_WithCachedKeyReloadRequiredAndNowRevoked() { key, err := internal.NewCryptoKey(secretFactory, suite.created, false, []byte("blah")) - if assert.NoError(suite.T(), err) { - entry := cacheEntry{ - key: key, - loadedAt: time.Now().Add(-2 * suite.policy.RevokeCheckInterval), - } + suite.Require().NoError(err) - suite.keyCache.keys[cacheKey(testKey, suite.created)] = entry - suite.keyCache.keys[cacheKey(testKey, 0)] = entry + entry := newCacheEntry(key) + entry.loadedAt = time.Now().Add(-2 * suite.policy.RevokeCheckInterval) - revokedKey, e := internal.NewCryptoKey(secretFactory, suite.created, true, []byte("blah")) - if assert.NoError(suite.T(), e) { - key, err := suite.keyCache.GetOrLoadLatest(testKey, keyLoaderFunc(func() (*internal.CryptoKey, error) { - return revokedKey, nil - })) - - assert.NoError(suite.T(), err) - assert.NotNil(suite.T(), key) - assert.Equal(suite.T(), suite.created, key.Created()) - assert.True(suite.T(), key.Revoked()) - assert.True(suite.T(), suite.keyCache.keys[cacheKey(testKey, 0)].key.Revoked()) - // Verify we closed the new one we loaded and kept the cached one open - assert.True(suite.T(), revokedKey.IsClosed()) - assert.False(suite.T(), suite.keyCache.keys[cacheKey(testKey, suite.created)].key.IsClosed()) - } - } -} + suite.keyCache.mapLatestKeyMeta(testKey, KeyMeta{ID: testKey, Created: suite.created}) + suite.keyCache.keys.Set(cacheKey(testKey, suite.created), entry) -type mockKeyReloader struct { - mock.Mock + revokedKey, e := internal.NewCryptoKey(secretFactory, suite.created, true, []byte("blah")) + suite.Require().NoError(e) - loader keyLoaderFunc -} + first := true + calls := 0 -func (r *mockKeyReloader) Load() (*internal.CryptoKey, error) { - args := r.Called() + // Because the entry's loadedAt is older than the revoke check interval, the key should be treated as "stale" + // which should trigger the following: + // 1. A cache miss is recorded because the key is no longer "fresh" + // 2. The key is loaded via the loader function below, which returns a revoked key on the first call + // 3. The cache, having received a revoked key, increments the reloaded count + // 4. The key is reloaded via the loader function, which returns a new key on subsequent calls + latest, err := suite.keyCache.GetOrLoadLatest(testKey, func(_ KeyMeta) (*internal.CryptoKey, error) { + calls++ - if r.loader != nil { - return r.loader() - } + if first { + first = false + return revokedKey, nil + } - return args.Get(0).(*internal.CryptoKey), args.Error(1) -} + return internal.NewCryptoKey(secretFactory, suite.created, false, []byte("blah")) + }) -func (r *mockKeyReloader) IsInvalid(key *internal.CryptoKey) bool { - args := r.Called(key.Created()) - return args.Bool(0) + assert.NoError(suite.T(), err) + assert.NotNil(suite.T(), latest) + assert.Equal(suite.T(), suite.created, latest.Created()) + assert.Equal(suite.T(), 2, calls) + assert.False(suite.T(), latest.Revoked()) + // Verify we closed the new one we loaded and kept the cached one open + assert.True(suite.T(), revokedKey.IsClosed()) + assert.False(suite.T(), suite.keyCache.keys.GetOrPanic(cacheKey(testKey, suite.created)).key.IsClosed()) } -func (suite *CacheTestSuite) TestKeyCache_GetOrLoadLatest_KeyReloader_WithCachedKeyAndInvalidKey() { +func (suite *CacheTestSuite) TestKeyCache_GetOrLoadLatest_WithCachedKeyAndInvalidKey() { orig, err := internal.NewCryptoKey(secretFactory, suite.created, true, []byte("blah")) require.NoError(suite.T(), err) entry := cacheEntry{ - key: orig, + key: &cachedCryptoKey{CryptoKey: orig}, loadedAt: time.Now(), } - suite.keyCache.keys[cacheKey(testKey, suite.created)] = entry - suite.keyCache.keys[cacheKey(testKey, 0)] = entry + suite.keyCache.mapLatestKeyMeta(testKey, KeyMeta{ID: testKey, Created: suite.created}) + suite.keyCache.keys.Set(cacheKey(testKey, suite.created), entry) newerCreated := time.Now().Add(1 * time.Second).Unix() require.Greater(suite.T(), newerCreated, suite.created) - reloader := &mockKeyReloader{ - loader: keyLoaderFunc(func() (*internal.CryptoKey, error) { - reloadedKey, e := internal.NewCryptoKey(secretFactory, newerCreated, false, []byte("blah")) - assert.NoError(suite.T(), e) + loader := func(_ KeyMeta) (*internal.CryptoKey, error) { + reloadedKey, e := internal.NewCryptoKey(secretFactory, newerCreated, false, []byte("blah")) + assert.NoError(suite.T(), e) - return reloadedKey, e - }), + return reloadedKey, e } - reloader.On("IsInvalid", orig.Created()).Return(true) - reloader.On("Load").Return().Once() - - key, err := suite.keyCache.GetOrLoadLatest(testKey, reloader) - reloader.AssertExpectations(suite.T()) + key, err := suite.keyCache.GetOrLoadLatest(testKey, loader) assert.NoError(suite.T(), err) assert.NotNil(suite.T(), key) @@ -362,80 +349,95 @@ func (suite *CacheTestSuite) TestKeyCache_GetOrLoadLatest_KeyReloader_WithCached assert.False(suite.T(), key.Revoked()) // cached key is still revoked - cached := suite.keyCache.keys[cacheKey(testKey, suite.created)] + cached := suite.keyCache.keys.GetOrPanic(cacheKey(testKey, suite.created)) assert.True(suite.T(), cached.key.Revoked(), fmt.Sprintf("%+v - created: %d", cached.key, cached.key.Created())) } -func (suite *CacheTestSuite) TestKeyCache_GetOrLoadLatest_KeyReloader_WithCachedKeyAndValidKey() { +func (suite *CacheTestSuite) TestKeyCache_GetOrLoadLatest_WithCachedKeyAndValidKey() { key, err := internal.NewCryptoKey(secretFactory, suite.created, false, []byte("blah")) if assert.NoError(suite.T(), err) { entry := cacheEntry{ - key: key, + key: &cachedCryptoKey{CryptoKey: key}, loadedAt: time.Now(), } - suite.keyCache.keys[cacheKey(testKey, suite.created)] = entry - suite.keyCache.keys[cacheKey(testKey, 0)] = entry - - reloader := new(mockKeyReloader) - reloader.On("IsInvalid", key.Created()).Return(false) + suite.keyCache.keys.Set(cacheKey(testKey, suite.created), entry) - key, err := suite.keyCache.GetOrLoadLatest(testKey, reloader) + key, err := suite.keyCache.GetOrLoadLatest(testKey, func(_ KeyMeta) (*internal.CryptoKey, error) { + return key, nil + }) assert.NoError(suite.T(), err) assert.NotNil(suite.T(), key) assert.Equal(suite.T(), suite.created, key.Created()) - assert.False(suite.T(), suite.keyCache.keys[cacheKey(testKey, 0)].key.Revoked()) - - reloader.AssertNotCalled(suite.T(), "Load", mock.Anything) - reloader.AssertExpectations(suite.T()) + assert.False(suite.T(), key.Revoked()) } } func (suite *CacheTestSuite) TestKeyCache_Close() { - cache := newKeyCache(NewCryptoPolicy()) + cache := newKeyCache(CacheTypeIntermediateKeys, NewCryptoPolicy()) - key, err := cache.GetOrLoadLatest(testKey, keyLoaderFunc(func() (*internal.CryptoKey, error) { + key, err := cache.GetOrLoadLatest(testKey, func(_ KeyMeta) (*internal.CryptoKey, error) { cryptoKey, err := internal.NewCryptoKey(secretFactory, suite.created, false, []byte("blah")) if err != nil { return nil, err } + return cryptoKey, nil - })) + }) assert.NoError(suite.T(), err) + key.Close() + assert.False(suite.T(), key.IsClosed(), "key should not be closed yet, as it is still in the cache") + err = cache.Close() assert.NoError(suite.T(), err) assert.True(suite.T(), key.IsClosed()) - assert.True(suite.T(), cache.keys[cacheKey(testKey, suite.created)].key.IsClosed()) - assert.True(suite.T(), cache.keys[cacheKey(testKey, 0)].key.IsClosed()) } -func (suite *CacheTestSuite) TestKeyCache_Close_MultipleCallsNoError() { - cache := newKeyCache(NewCryptoPolicy()) +func (suite *CacheTestSuite) TestKeyCache_Close_CacheThenKey() { + cache := newKeyCache(CacheTypeIntermediateKeys, NewCryptoPolicy()) - key, err := cache.GetOrLoadLatest(testKey, keyLoaderFunc(func() (*internal.CryptoKey, error) { - cryptoKey, err := internal.NewCryptoKey(secretFactory, time.Now().Unix(), false, []byte("blah")) + key, err := cache.GetOrLoadLatest(testKey, func(_ KeyMeta) (*internal.CryptoKey, error) { + cryptoKey, err := internal.NewCryptoKey(secretFactory, suite.created, false, []byte("blah")) if err != nil { return nil, err } + return cryptoKey, nil - })) + }) assert.NoError(suite.T(), err) err = cache.Close() - assert.NoError(suite.T(), err) + assert.False(suite.T(), key.IsClosed(), "key should not be closed yet, key reference still exists") + + key.Close() assert.True(suite.T(), key.IsClosed()) +} + +func (suite *CacheTestSuite) TestKeyCache_Close_MultipleCallsNoError() { + cache := newKeyCache(CacheTypeIntermediateKeys, NewCryptoPolicy()) + + key, err := cache.GetOrLoadLatest(testKey, func(_ KeyMeta) (*internal.CryptoKey, error) { + return internal.NewCryptoKey(secretFactory, time.Now().Unix(), false, []byte("blah")) + }) + assert.NoError(suite.T(), err) + + key.Close() err = cache.Close() assert.NoError(suite.T(), err) + assert.True(suite.T(), key.IsClosed()) + + err = cache.Close() + assert.NoError(suite.T(), err) } func (suite *CacheTestSuite) TestKeyCache_String() { - cache := newKeyCache(NewCryptoPolicy()) + cache := newKeyCache(CacheTypeIntermediateKeys, NewCryptoPolicy()) defer cache.Close() assert.Contains(suite.T(), cache.String(), "keyCache(") @@ -443,13 +445,9 @@ func (suite *CacheTestSuite) TestKeyCache_String() { func (suite *CacheTestSuite) TestNeverCache_GetOrLoad() { var cache neverCache - key, err := cache.GetOrLoad(KeyMeta{testKey, created}, keyLoaderFunc(func() (key *internal.CryptoKey, e error) { - cryptoKey, err := internal.NewCryptoKey(secretFactory, created, false, []byte("blah")) - if err != nil { - return nil, err - } - return cryptoKey, nil - })) + key, err := cache.GetOrLoad(KeyMeta{testKey, created}, func(_ KeyMeta) (key *internal.CryptoKey, e error) { + return internal.NewCryptoKey(secretFactory, created, false, []byte("blah")) + }) if assert.NoError(suite.T(), err) { // neverCache can't close keys we create @@ -462,13 +460,9 @@ func (suite *CacheTestSuite) TestNeverCache_GetOrLoad() { func (suite *CacheTestSuite) TestNeverCache_GetOrLoadLatest() { var cache neverCache - key, err := cache.GetOrLoadLatest(testKey, keyLoaderFunc(func() (key *internal.CryptoKey, e error) { - cryptoKey, err := internal.NewCryptoKey(secretFactory, created, false, []byte("blah")) - if err != nil { - return nil, err - } - return cryptoKey, nil - })) + key, err := cache.GetOrLoadLatest(testKey, func(_ KeyMeta) (key *internal.CryptoKey, e error) { + return internal.NewCryptoKey(secretFactory, created, false, []byte("blah")) + }) if assert.NoError(suite.T(), err) { // neverCache can't close keys we create @@ -487,27 +481,32 @@ func (suite *CacheTestSuite) TestNeverCache_Close() { assert.NoError(suite.T(), err) } -func (suite *CacheTestSuite) TestSharedKeyCache_GetOrLoad() { +func (suite *CacheTestSuite) TestKeyCache_GetOrLoad_Concurrent_100() { if testing.Short() { suite.T().Skip("too slow for testing.Short") } var ( - cache = newKeyCache(NewCryptoPolicy()) + cache = newKeyCache(CacheTypeIntermediateKeys, NewCryptoPolicy()) i = 0 wg sync.WaitGroup counter int32 ) - loadFunc := keyLoaderFunc(func() (*internal.CryptoKey, error) { - <-time.After(time.Nanosecond * time.Duration(rand.Intn(30))) + loadFunc := func(_ KeyMeta) (*internal.CryptoKey, error) { + <-time.After(time.Millisecond * time.Duration(rand.Intn(30))) atomic.AddInt32(&counter, 1) return new(internal.CryptoKey), nil - }) + } meta := KeyMeta{ID: "testing", Created: time.Now().Unix()} + _, err := cache.GetOrLoad(meta, loadFunc) + if err != nil { + suite.T().Error(err) + } + for ; i < 100; i++ { wg.Add(1) @@ -517,24 +516,22 @@ func (suite *CacheTestSuite) TestSharedKeyCache_GetOrLoad() { key, err := cache.GetOrLoad(meta, loadFunc) if key == nil { suite.T().Error("key == nil") - suite.T().Fail() } if err != nil { suite.T().Error(err) - suite.T().Fail() } }() } wg.Wait() - // This seems to be causing intermittent issues with go2xunit parsing - //d := time.Since(startTime) - // - //fmt.Printf("Finished %d loops in: %s (%f/s)", i, d, float64(i)/d.Seconds()) - assert.Equal(suite.T(), int32(1), counter) + assert.Equal(suite.T(), 1, cache.keys.Len()) + + // metrics := cache.GetMetrics() + // assert.Equal(suite.T(), int64(1), metrics.MissCount) + // assert.Equal(suite.T(), int64(100), metrics.HitCount) } func TestCacheTestSuite(t *testing.T) { diff --git a/go/appencryption/parameterized_test.go b/go/appencryption/parameterized_test.go index 8b419bef3..3cef965d0 100644 --- a/go/appencryption/parameterized_test.go +++ b/go/appencryption/parameterized_test.go @@ -187,25 +187,25 @@ func createRevokedKey(src *internal.CryptoKey, factory securememory.SecretFactor } func createSession(crypto AEAD, metastore Metastore, kms KeyManagementService, factory securememory.SecretFactory, - policy *CryptoPolicy, partition partition, ikCache cache, skCache cache) *Session { + policy *CryptoPolicy, partition partition, ikCache keyCacher, skCache keyCacher) *Session { return &Session{ encryption: &envelopeEncryption{ - partition: partition, - Metastore: metastore, - KMS: kms, - Policy: policy, - Crypto: crypto, - SecretFactory: factory, - systemKeys: skCache, - intermediateKeys: ikCache, + partition: partition, + Metastore: metastore, + KMS: kms, + Policy: policy, + Crypto: crypto, + SecretFactory: factory, + skCache: skCache, + ikCache: ikCache, }} } func createCache(partition partition, cacheIK, cacheSK string, intermediateKey, systemKey *internal.CryptoKey, - policy *CryptoPolicy) (cache, cache) { - var ikCache, skCache cache - skCache = newKeyCache(policy) - ikCache = newKeyCache(policy) + policy *CryptoPolicy) (keyCacher, keyCacher) { + var ikCache, skCache keyCacher + skCache = newKeyCache(CacheTypeSystemKeys, policy) + ikCache = newKeyCache(CacheTypeIntermediateKeys, policy) sk := systemKey ik := intermediateKey @@ -222,9 +222,9 @@ func createCache(partition partition, cacheIK, cacheSK string, intermediateKey, } // Preload the cache with the system keys - _, _ = skCache.GetOrLoad(*meta, keyLoaderFunc(func() (*internal.CryptoKey, error) { + _, _ = skCache.GetOrLoad(*meta, func(_ KeyMeta) (*internal.CryptoKey, error) { return sk, nil - })) + }) } if cacheIK != EMPTY { @@ -239,9 +239,9 @@ func createCache(partition partition, cacheIK, cacheSK string, intermediateKey, } // Preload the cache with the intermediate keys - _, _ = ikCache.GetOrLoad(*meta, keyLoaderFunc(func() (*internal.CryptoKey, error) { + _, _ = ikCache.GetOrLoad(*meta, func(_ KeyMeta) (*internal.CryptoKey, error) { return ik, nil - })) + }) } return ikCache, skCache diff --git a/go/appencryption/pkg/cache/cache.go b/go/appencryption/pkg/cache/cache.go index 2c28f0f20..125e57e1f 100644 --- a/go/appencryption/pkg/cache/cache.go +++ b/go/appencryption/pkg/cache/cache.go @@ -39,6 +39,8 @@ const ( SLRU CachePolicy = "slru" // TinyLFU is the tiny least frequently used cache policy. TinyLFU CachePolicy = "tinylfu" + // DefaultCachePolicy is the default cache policy. + DefaultCachePolicy = LRU ) // String returns the string representation of the eviction policy. @@ -76,7 +78,7 @@ func WithPolicy[K comparable, V any](policy CachePolicy) Option[K, V] { case TinyLFU: c.policy = new(tinyLFU[K, V]) default: - panic("cache: unsupported policy " + policy.String()) + panic(fmt.Sprintf("cache: unsupported policy \"%s\"", policy.String())) } } } @@ -140,8 +142,7 @@ type cache[K comparable, V any] struct { onEvictCallback EvictFunc[K, V] } -// New returns a new cache with the given capacity, eviction policy, and -// options. +// New returns a new cache with the given capacity and options. func New[K comparable, V any](capacity int, options ...Option[K, V]) Interface[K, V] { return new(cache[K, V]).init(capacity, options...) } diff --git a/go/appencryption/policy.go b/go/appencryption/policy.go index fd39aaae9..17bd67119 100644 --- a/go/appencryption/policy.go +++ b/go/appencryption/policy.go @@ -9,6 +9,7 @@ const ( DefaultExpireAfter = time.Hour * 24 * 90 // 90 days DefaultRevokedCheckInterval = time.Minute * 60 DefaultCreateDatePrecision = time.Minute + DefaultKeyCacheMaxSize = 1000 DefaultSessionCacheMaxSize = 1000 DefaultSessionCacheDuration = time.Hour * 2 DefaultSessionCacheEngine = "default" @@ -27,8 +28,26 @@ type CryptoPolicy struct { CreateDatePrecision time.Duration // CacheIntermediateKeys determines whether Intermediate Keys will be cached. CacheIntermediateKeys bool + // IntermediateKeyCacheMaxSize controls the maximum size of the cache if intermediate key caching is enabled. + IntermediateKeyCacheMaxSize int + // IntermediateKeyCacheEvictionPolicy controls the eviction policy to use for the shared cache. + // Supported values are "lru", "lfu", "slru", and "tinylfu". Default is "lru". + IntermediateKeyCacheEvictionPolicy string + // SharedIntermediateKeyCache determines whether Intermediate Keys will use a single shared cache. If enabled, + // Intermediate Keys will share a single cache across all sessions for a given factory. + // This option is useful if you have a large number of sessions and want to reduce the memory footprint of the + // cache. + // + // This option is ignored if CacheIntermediateKeys is disabled. + SharedIntermediateKeyCache bool // CacheSystemKeys determines whether System Keys will be cached. CacheSystemKeys bool + // SystemKeyCacheMaxSize controls the maximum size of the cache if system key caching is enabled. If + // SharedKeyCache is enabled, this value will determine the maximum size of the shared cache. + SystemKeyCacheMaxSize int + // SystemKeyCacheEvictionPolicy controls the eviction policy to use for the shared cache. + // Supported values are "lru", "lfu", "slru", and "tinylfu". Default is "lru". + SystemKeyCacheEvictionPolicy string // CacheSessions determines whether sessions will be cached. CacheSessions bool // SessionCacheMaxSize controls the maximum size of the cache if session caching is enabled. @@ -42,6 +61,9 @@ type CryptoPolicy struct { // Deprecated: multiple cache implementations are no longer supported and this option will be removed // in a future release. SessionCacheEngine string + // SessionCacheEvictionPolicy controls the eviction policy to use for the shared cache. + // Supported values are "lru", "lfu", "slru", and "tinylfu". Default is "slru". + SessionCacheEvictionPolicy string } // PolicyOption is used to configure a CryptoPolicy. @@ -69,6 +91,14 @@ func WithNoCache() PolicyOption { } } +// WithSharedKeyCache enables a shared cache for both System and Intermediate Keys with the provided capacity. +func WithSharedKeyCache(capacity int) PolicyOption { + return func(policy *CryptoPolicy) { + policy.SharedIntermediateKeyCache = true + policy.SystemKeyCacheMaxSize = capacity + } +} + // WithSessionCache enables session caching. When used all sessions for a given partition will share underlying // System and Intermediate Key caches. func WithSessionCache() PolicyOption { @@ -106,15 +136,18 @@ func WithSessionCacheEngine(engine string) PolicyOption { // NewCryptoPolicy returns a new CryptoPolicy with default values. func NewCryptoPolicy(opts ...PolicyOption) *CryptoPolicy { policy := &CryptoPolicy{ - ExpireKeyAfter: DefaultExpireAfter, - RevokeCheckInterval: DefaultRevokedCheckInterval, - CreateDatePrecision: DefaultCreateDatePrecision, - CacheSystemKeys: true, - CacheIntermediateKeys: true, - CacheSessions: false, - SessionCacheMaxSize: DefaultSessionCacheMaxSize, - SessionCacheDuration: DefaultSessionCacheDuration, - SessionCacheEngine: DefaultSessionCacheEngine, + ExpireKeyAfter: DefaultExpireAfter, + RevokeCheckInterval: DefaultRevokedCheckInterval, + CreateDatePrecision: DefaultCreateDatePrecision, + CacheSystemKeys: true, + CacheIntermediateKeys: true, + IntermediateKeyCacheMaxSize: DefaultKeyCacheMaxSize, + SystemKeyCacheMaxSize: DefaultKeyCacheMaxSize, + SharedIntermediateKeyCache: false, + CacheSessions: false, + SessionCacheMaxSize: DefaultSessionCacheMaxSize, + SessionCacheDuration: DefaultSessionCacheDuration, + SessionCacheEngine: DefaultSessionCacheEngine, } for _, opt := range opts { @@ -124,12 +157,6 @@ func NewCryptoPolicy(opts ...PolicyOption) *CryptoPolicy { return policy } -// isKeyExpired checks if the key's created timestamp is older than the -// allowed number of days. -func isKeyExpired(created int64, expireAfter time.Duration) bool { - return time.Now().After(time.Unix(created, 0).Add(expireAfter)) -} - // newKeyTimestamp returns a unix timestamp in seconds truncated to the provided Duration. func newKeyTimestamp(truncate time.Duration) int64 { if truncate > 0 { diff --git a/go/appencryption/policy_test.go b/go/appencryption/policy_test.go index a3495a2e0..ef270bdc8 100644 --- a/go/appencryption/policy_test.go +++ b/go/appencryption/policy_test.go @@ -17,6 +17,9 @@ func Test_NewCryptoPolicy_WithDefaults(t *testing.T) { assert.Equal(t, DefaultCreateDatePrecision, p.CreateDatePrecision) assert.True(t, p.CacheSystemKeys) assert.True(t, p.CacheIntermediateKeys) + assert.Equal(t, DefaultKeyCacheMaxSize, p.SystemKeyCacheMaxSize) + assert.Equal(t, DefaultKeyCacheMaxSize, p.IntermediateKeyCacheMaxSize) + assert.False(t, p.SharedIntermediateKeyCache) assert.False(t, p.CacheSessions) assert.Equal(t, DefaultSessionCacheMaxSize, p.SessionCacheMaxSize) assert.Equal(t, DefaultSessionCacheDuration, p.SessionCacheDuration) @@ -50,6 +53,36 @@ func Test_NewCryptoPolicy_WithOptions(t *testing.T) { assert.Equal(t, sessionCacheEngine, policy.SessionCacheEngine) } +func Test_NewCryptoPolicy_WithOptions_SharedKeyCache(t *testing.T) { + revokeCheckInterval := time.Second * 156 + expireAfterDuration := time.Second * 100 + keyCacheMaxSize := 10 + sessionCacheMaxSize := 42 + sessionCacheDuration := time.Second * 42 + sessionCacheEngine := "deprecated" + + policy := NewCryptoPolicy( + WithRevokeCheckInterval(revokeCheckInterval), + WithExpireAfterDuration(expireAfterDuration), + WithSharedKeyCache(keyCacheMaxSize), + WithSessionCache(), + WithSessionCacheMaxSize(sessionCacheMaxSize), + WithSessionCacheDuration(sessionCacheDuration), + WithSessionCacheEngine(sessionCacheEngine), + ) + + assert.Equal(t, revokeCheckInterval, policy.RevokeCheckInterval) + assert.Equal(t, expireAfterDuration, policy.ExpireKeyAfter) + assert.True(t, policy.CacheSystemKeys) + assert.True(t, policy.CacheIntermediateKeys) + assert.True(t, policy.SharedIntermediateKeyCache) + assert.Equal(t, keyCacheMaxSize, policy.SystemKeyCacheMaxSize) + assert.True(t, policy.CacheSessions) + assert.Equal(t, sessionCacheMaxSize, policy.SessionCacheMaxSize) + assert.Equal(t, sessionCacheDuration, policy.SessionCacheDuration) + assert.Equal(t, sessionCacheEngine, policy.SessionCacheEngine) +} + func Test_IsKeyExpired(t *testing.T) { tests := []struct { Name string @@ -78,7 +111,7 @@ func Test_IsKeyExpired(t *testing.T) { key := internal.NewCryptoKeyForTest(tt.CreatedAt.Unix(), false) - verify.Equal(tt.Expect, isKeyExpired(key.Created(), time.Hour*24*time.Duration(tt.ExpireAfterDays))) + verify.Equal(tt.Expect, internal.IsKeyExpired(key.Created(), time.Hour*24*time.Duration(tt.ExpireAfterDays))) }) } } diff --git a/go/appencryption/session.go b/go/appencryption/session.go index e711f5aee..70dd83e1b 100644 --- a/go/appencryption/session.go +++ b/go/appencryption/session.go @@ -14,13 +14,14 @@ import ( // SessionFactory is used to create new encryption sessions and manage // the lifetime of the intermediate keys. type SessionFactory struct { - sessionCache sessionCache - systemKeys cache - Config *Config - Metastore Metastore - Crypto AEAD - KMS KeyManagementService - SecretFactory securememory.SecretFactory + sessionCache sessionCache + systemKeys keyCacher + intermediateKeys keyCacher // only used if shared key cache is enabled + Config *Config + Metastore Metastore + Crypto AEAD + KMS KeyManagementService + SecretFactory securememory.SecretFactory } // FactoryOption is used to configure additional options in a SessionFactory. @@ -48,21 +49,28 @@ func NewSessionFactory(config *Config, store Metastore, kms KeyManagementService config.Policy = NewCryptoPolicy() } - var skCache cache + var skCache keyCacher if config.Policy.CacheSystemKeys { - skCache = newKeyCache(config.Policy) + skCache = newKeyCache(CacheTypeSystemKeys, config.Policy) log.Debugf("new skCache: %v\n", skCache) } else { skCache = new(neverCache) } + var ikCache keyCacher + if config.Policy.SharedIntermediateKeyCache { + ikCache = newKeyCache(CacheTypeIntermediateKeys, config.Policy) + log.Debugf("new shared ikCache: %v\n", ikCache) + } + factory := &SessionFactory{ - systemKeys: skCache, - Config: config, - Metastore: store, - Crypto: crypto, - KMS: kms, - SecretFactory: new(memguard.SecretFactory), + systemKeys: skCache, + intermediateKeys: ikCache, + Config: config, + Metastore: store, + Crypto: crypto, + KMS: kms, + SecretFactory: new(memguard.SecretFactory), } if config.Policy.CacheSessions { @@ -85,6 +93,10 @@ func (f *SessionFactory) Close() error { f.sessionCache.Close() } + if f.Config.Policy.SharedIntermediateKeyCache { + f.intermediateKeys.Close() + } + return f.systemKeys.Close() } @@ -102,17 +114,29 @@ func (f *SessionFactory) GetSession(id string) (*Session, error) { } func newSession(f *SessionFactory, id string) (*Session, error) { + skCache := f.systemKeys + + var ikCache keyCacher + if f.Config.Policy.SharedIntermediateKeyCache { + ikCache = f.intermediateKeys + } else { + ikCache = f.newIKCache() + } + s := &Session{ encryption: &envelopeEncryption{ - partition: f.newPartition(id), - Metastore: f.Metastore, - KMS: f.KMS, - Policy: f.Config.Policy, - Crypto: f.Crypto, - SecretFactory: f.SecretFactory, - systemKeys: f.systemKeys, - intermediateKeys: f.newIKCache(), + partition: f.newPartition(id), + Metastore: f.Metastore, + KMS: f.KMS, + Policy: f.Config.Policy, + Crypto: f.Crypto, + SecretFactory: f.SecretFactory, + skCache: skCache, + ikCache: ikCache, }, + + ikCache: ikCache, + skCache: skCache, } log.Debugf("[newSession] for id %s. Session(%p){Encryption(%p)}", id, s, s.encryption) @@ -128,9 +152,9 @@ func (f *SessionFactory) newPartition(id string) partition { return newPartition(id, f.Config.Service, f.Config.Product) } -func (f *SessionFactory) newIKCache() cache { +func (f *SessionFactory) newIKCache() keyCacher { if f.Config.Policy.CacheIntermediateKeys { - return newKeyCache(f.Config.Policy) + return newKeyCache(CacheTypeIntermediateKeys, f.Config.Policy) } return new(neverCache) @@ -139,6 +163,9 @@ func (f *SessionFactory) newIKCache() cache { // Session is used to encrypt and decrypt data related to a specific partition ID. type Session struct { encryption Encryption + + ikCache keyCacher + skCache keyCacher } // Encrypt encrypts a provided slice of bytes and returns a DataRowRecord, which contains required diff --git a/go/appencryption/session_cache.go b/go/appencryption/session_cache.go index 443450b42..20488b4d3 100644 --- a/go/appencryption/session_cache.go +++ b/go/appencryption/session_cache.go @@ -6,6 +6,7 @@ import ( mango "github.com/goburrow/cache" + "github.com/godaddy/asherah/go/appencryption/pkg/cache" "github.com/godaddy/asherah/go/appencryption/pkg/log" ) @@ -250,9 +251,9 @@ func (s *sharedEncryption) Remove() { // sessionLoaderFunc retrieves a Session corresponding to the given partition ID. type sessionLoaderFunc func(id string) (*Session, error) -// newSessionCache returns a new SessionCache with the configured cache implementation +// newMangoSessionCache returns a new SessionCache with the configured cache implementation // using the provided SessionLoaderFunc and CryptoPolicy. -func newSessionCache(loader sessionLoaderFunc, policy *CryptoPolicy) sessionCache { +func newMangoSessionCache(loader sessionLoaderFunc, policy *CryptoPolicy) sessionCache { wrapper := func(id string) (*Session, error) { s, err := loader(id) if err != nil { @@ -285,3 +286,101 @@ func sessionInjectEncryption(s *Session, e Encryption) { s.encryption = e } + +// newSessionCacheWithCache returns a new SessionCache with the provided cache implementation +// using the provided SessionLoaderFunc and CryptoPolicy. +func newSessionCacheWithCache(loader sessionLoaderFunc, policy *CryptoPolicy, cache cache.Interface[string, *Session]) sessionCache { + return &cacheWrapper{ + loader: func(id string) (*Session, error) { + s, err := loader(id) + if err != nil { + return nil, err + } + + _, ok := s.encryption.(*sharedEncryption) + if !ok { + mu := new(sync.Mutex) + orig := s.encryption + wrapped := &sharedEncryption{ + Encryption: orig, + mu: mu, + cond: sync.NewCond(mu), + created: time.Now(), + } + + sessionInjectEncryption(s, wrapped) + } + + return s, nil + }, + policy: policy, + cache: cache, + } +} + +type cacheWrapper struct { + loader sessionLoaderFunc + policy *CryptoPolicy + cache cache.Interface[string, *Session] + + mu sync.Mutex +} + +func (c *cacheWrapper) Get(id string) (*Session, error) { + c.mu.Lock() + defer c.mu.Unlock() + + val, err := c.getOrAdd(id) + if err != nil { + return nil, err + } + + incrementSharedSessionUsage(val) + + return val, nil +} + +func (c *cacheWrapper) getOrAdd(id string) (*Session, error) { + if val, ok := c.cache.Get(id); ok { + return val, nil + } + + val, err := c.loader(id) + if err != nil { + return nil, err + } + + c.cache.Set(id, val) + + return val, nil +} + +func (c *cacheWrapper) Count() int { + return c.cache.Len() +} + +func (c *cacheWrapper) Close() { + c.cache.Close() +} + +func newSessionCache(loader sessionLoaderFunc, policy *CryptoPolicy) sessionCache { + switch policy.SessionCacheEvictionPolicy { + case "": + log.Debugf("policy.SessionCacheEvictionPolicy is empty\n") + + // TODO: remove mango cache + return newMangoSessionCache(loader, policy) + default: + log.Debugf("policy.SessionCacheEvictionPolicy is \"%s\"\n", policy.SessionCacheEvictionPolicy) + + inner := cache.New[string, *Session]( + policy.SessionCacheMaxSize, + cache.WithPolicy[string, *Session](cache.CachePolicy(policy.SessionCacheEvictionPolicy)), + cache.WithEvictFunc[string, *Session](func(k string, v *Session) { + go v.encryption.(*sharedEncryption).Remove() + }), + ) + + return newSessionCacheWithCache(loader, policy, inner) + } +} diff --git a/go/appencryption/session_cache_test.go b/go/appencryption/session_cache_test.go index 8e1bd1faa..153fc60f6 100644 --- a/go/appencryption/session_cache_test.go +++ b/go/appencryption/session_cache_test.go @@ -96,7 +96,7 @@ func TestNewSessionCache(t *testing.T) { return &Session{}, nil } - cache := newSessionCache(loader, NewCryptoPolicy()) + cache := newMangoSessionCache(loader, NewCryptoPolicy()) defer cache.Close() require.NotNil(t, cache) @@ -109,7 +109,7 @@ func TestSessionCacheGetUsesLoader(t *testing.T) { return session, nil } - cache := newSessionCache(loader, NewCryptoPolicy()) + cache := newMangoSessionCache(loader, NewCryptoPolicy()) require.NotNil(t, cache) defer cache.Close() @@ -129,7 +129,7 @@ func TestSessionCacheGetDoesNotUseLoaderOnHit(t *testing.T) { return session, nil } - cache := newSessionCache(loader, NewCryptoPolicy()) + cache := newMangoSessionCache(loader, NewCryptoPolicy()) require.NotNil(t, cache) defer cache.Close() @@ -155,7 +155,7 @@ func TestSessionCacheGetReturnLoaderError(t *testing.T) { return nil, assert.AnError } - cache := newSessionCache(loader, NewCryptoPolicy()) + cache := newMangoSessionCache(loader, NewCryptoPolicy()) require.NotNil(t, cache) defer cache.Close() @@ -169,7 +169,7 @@ func TestSessionCacheCount(t *testing.T) { totalSessions := 10 b := newSessionBucket() - cache := newSessionCache(b.load, NewCryptoPolicy()) + cache := newMangoSessionCache(b.load, NewCryptoPolicy()) require.NotNil(t, cache) defer cache.Close() @@ -189,7 +189,7 @@ func TestSessionCacheMaxCount(t *testing.T) { policy := NewCryptoPolicy() policy.SessionCacheMaxSize = maxSessions - cache := newSessionCache(b.load, policy) + cache := newMangoSessionCache(b.load, policy) require.NotNil(t, cache) defer cache.Close() @@ -235,7 +235,7 @@ func TestSessionCacheDuration(t *testing.T) { policy := NewCryptoPolicy() policy.SessionCacheDuration = ttl - cache := newSessionCache(b.load, policy) + cache := newMangoSessionCache(b.load, policy) require.NotNil(t, cache) defer cache.Close() @@ -270,7 +270,7 @@ func (t *testLogger) Debugf(f string, v ...interface{}) { func TestSessionCacheCloseWithDebugLogging(t *testing.T) { b := newSessionBucket() - cache := newSessionCache(b.load, NewCryptoPolicy()) + cache := newMangoSessionCache(b.load, NewCryptoPolicy()) require.NotNil(t, cache) l := new(testLogger) @@ -291,7 +291,7 @@ func TestSessionCacheCloseWithDebugLogging(t *testing.T) { func TestSharedSessionCloseOnCacheClose(t *testing.T) { b := newSessionBucket() - cache := newSessionCache(b.load, NewCryptoPolicy()) + cache := newMangoSessionCache(b.load, NewCryptoPolicy()) require.NotNil(t, cache) s, err := cache.Get("my-item") @@ -322,7 +322,7 @@ func TestSharedSessionCloseOnEviction(t *testing.T) { var firstBatch [max]*Session - cache := newSessionCache(b.load, policy) + cache := newMangoSessionCache(b.load, policy) require.NotNil(t, cache) defer cache.Close() @@ -364,7 +364,7 @@ func TestSharedSessionCloseOnEviction(t *testing.T) { func TestSharedSessionCloseDoesNotCloseUnderlyingSession(t *testing.T) { b := newSessionBucket() - cache := newSessionCache(b.load, NewCryptoPolicy()) + cache := newMangoSessionCache(b.load, NewCryptoPolicy()) require.NotNil(t, cache) defer cache.Close() diff --git a/go/appencryption/session_test.go b/go/appencryption/session_test.go index 344fdd676..b0c9468e5 100644 --- a/go/appencryption/session_test.go +++ b/go/appencryption/session_test.go @@ -80,7 +80,7 @@ type MockCache struct { mock.Mock } -func (c *MockCache) GetOrLoad(id KeyMeta, loader keyLoader) (*internal.CryptoKey, error) { +func (c *MockCache) GetOrLoad(id KeyMeta, loader func(KeyMeta) (*internal.CryptoKey, error)) (*cachedCryptoKey, error) { var ( ret = c.Called(id, loader) key *internal.CryptoKey @@ -90,10 +90,10 @@ func (c *MockCache) GetOrLoad(id KeyMeta, loader keyLoader) (*internal.CryptoKey key = b.(*internal.CryptoKey) } - return key, ret.Error(1) + return &cachedCryptoKey{CryptoKey: key}, ret.Error(1) } -func (c *MockCache) GetOrLoadLatest(id string, loader keyLoader) (*internal.CryptoKey, error) { +func (c *MockCache) GetOrLoadLatest(id string, loader func(KeyMeta) (*internal.CryptoKey, error)) (*cachedCryptoKey, error) { var ( ret = c.Called(id, loader) key *internal.CryptoKey @@ -103,7 +103,7 @@ func (c *MockCache) GetOrLoadLatest(id string, loader keyLoader) (*internal.Cryp key = b.(*internal.CryptoKey) } - return key, ret.Error(1) + return &cachedCryptoKey{CryptoKey: key}, ret.Error(1) } func (c *MockCache) Close() error { @@ -169,7 +169,7 @@ func TestSessionFactory_GetSession(t *testing.T) { sess, err := sessionFactory.GetSession("testing") if assert.NoError(t, err) { assert.NotNil(t, sess.encryption) - ik := sess.encryption.(*envelopeEncryption).intermediateKeys + ik := sess.encryption.(*envelopeEncryption).ikCache assert.IsType(t, new(neverCache), ik) } } @@ -184,7 +184,7 @@ func TestSessionFactory_GetSession_CanCacheIntermediateKeys(t *testing.T) { sess, err := sessionFactory.GetSession("testing") if assert.NoError(t, err) { assert.NotNil(t, sess.encryption) - ik := sess.encryption.(*envelopeEncryption).intermediateKeys + ik := sess.encryption.(*envelopeEncryption).ikCache assert.IsType(t, new(keyCache), ik) } } From fa3571bad332fcd10e0556b9aa8d9c62869c0fe6 Mon Sep 17 00:00:00 2001 From: Bo Thompson Date: Sat, 14 Oct 2023 14:20:04 -0700 Subject: [PATCH 05/20] [go] add additional eviction policies to trace report --- .../traces/out/report-session-cache.png | Bin 57231 -> 78049 bytes .../integrationtest/traces/report.go | 19 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/go/appencryption/integrationtest/traces/out/report-session-cache.png b/go/appencryption/integrationtest/traces/out/report-session-cache.png index 8111bc731f2615ff522dfbcf19fa82b5ec6d5896..41d09620be11ceea9bcbef4547e3bcb6efb6f382 100644 GIT binary patch literal 78049 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 literal 57231 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 Date: Sat, 14 Oct 2023 16:27:45 -0700 Subject: [PATCH 06/20] [go] use builder to construct cache, implement expiry (lazy) --- go/appencryption/key_cache.go | 26 +-- go/appencryption/pkg/cache/cache.go | 199 +++++++++++++----- go/appencryption/pkg/cache/cache_test.go | 51 ++++- .../pkg/cache/lfu_example_test.go | 16 +- go/appencryption/pkg/cache/lfu_test.go | 13 +- go/appencryption/pkg/cache/lru_test.go | 12 +- go/appencryption/pkg/cache/tlfu_test.go | 2 +- go/appencryption/session_cache.go | 23 +- 8 files changed, 238 insertions(+), 104 deletions(-) diff --git a/go/appencryption/key_cache.go b/go/appencryption/key_cache.go index d1fb6a1ec..f061ca80c 100644 --- a/go/appencryption/key_cache.go +++ b/go/appencryption/key_cache.go @@ -111,10 +111,15 @@ const ( CacheTypeIntermediateKeys ) -// newKeyCacheWithCacheOptions constructs a cache object that is ready to use. -func newKeyCacheWithCacheOptions(t cacheKeyType, policy *CryptoPolicy, opts ...cache.Option[string, cacheEntry]) *keyCache { +// newKeyCache constructs a cache object that is ready to use. +func newKeyCache(t cacheKeyType, policy *CryptoPolicy) (c *keyCache) { cacheMaxSize := DefaultKeyCacheMaxSize cachePolicy := "" + onEvict := func(key string, value cacheEntry) { + log.Debugf("%s eviction -- key: %s, id: %s\n", c, value.key, key) + + value.key.Close() + } switch t { case CacheTypeSystemKeys: @@ -125,11 +130,13 @@ func newKeyCacheWithCacheOptions(t cacheKeyType, policy *CryptoPolicy, opts ...c cachePolicy = policy.IntermediateKeyCacheEvictionPolicy } + cb := cache.New[string, cacheEntry](cacheMaxSize) + if cachePolicy != "" { - opts = append(opts, cache.WithPolicy[string, cacheEntry](cache.CachePolicy(cachePolicy))) + cb.WithPolicy(cache.CachePolicy(cachePolicy)) } - keys := cache.New(cacheMaxSize, opts...) + keys := cb.WithEvictFunc(onEvict).Build() return &keyCache{ policy: policy, @@ -140,17 +147,6 @@ func newKeyCacheWithCacheOptions(t cacheKeyType, policy *CryptoPolicy, opts ...c } } -// newKeyCache constructs a cache object that is ready to use. -func newKeyCache(t cacheKeyType, policy *CryptoPolicy) (c *keyCache) { - onEvict := func(key string, value cacheEntry) { - log.Debugf("%s eviction -- key: %s, id: %s\n", c, value.key, key) - - value.key.Close() - } - - return newKeyCacheWithCacheOptions(t, policy, cache.WithEvictFunc[string, cacheEntry](onEvict)) -} - // isReloadRequired returns true if the check interval has elapsed // since the timestamp provided. func isReloadRequired(entry cacheEntry, checkInterval time.Duration) bool { diff --git a/go/appencryption/pkg/cache/cache.go b/go/appencryption/pkg/cache/cache.go index 125e57e1f..d8f14c955 100644 --- a/go/appencryption/pkg/cache/cache.go +++ b/go/appencryption/pkg/cache/cache.go @@ -14,6 +14,7 @@ import ( "container/list" "fmt" "sync" + "time" ) // Interface is intended to be a generic interface for cache implementations. @@ -55,34 +56,6 @@ type EvictFunc[K comparable, V any] func(key K, value V) // NopEvict is a no-op EvictFunc. func NopEvict[K comparable, V any](K, V) {} -// Option is a functional option for the cache. -type Option[K comparable, V any] func(*cache[K, V]) - -// WithEvictFunc sets the EvictFunc for the cache. -func WithEvictFunc[K comparable, V any](fn EvictFunc[K, V]) Option[K, V] { - return func(c *cache[K, V]) { - c.onEvictCallback = fn - } -} - -// WithPolicy sets the eviction policy for the cache. -func WithPolicy[K comparable, V any](policy CachePolicy) Option[K, V] { - return func(c *cache[K, V]) { - switch policy { - case LRU: - c.policy = new(lru[K, V]) - case LFU: - c.policy = new(lfu[K, V]) - case SLRU: - c.policy = new(slru[K, V]) - case TinyLFU: - c.policy = new(tinyLFU[K, V]) - default: - panic(fmt.Sprintf("cache: unsupported policy \"%s\"", policy.String())) - } - } -} - // event is the cache event (evictItem or closeCache). type event int @@ -94,9 +67,12 @@ const ( ) type cacheItem[K comparable, V any] struct { - key K - value V + key K + value V + parent *list.Element // Pointer to the frequencyParent + + expiration time.Time // Expiration time } // cacheEvent is the event sent on the events channel. @@ -124,6 +100,115 @@ type policy[K comparable, V any] interface { remove(item *cacheItem[K, V]) } +// Clock is an interface for getting the current time. +type Clock interface { + Now() time.Time +} + +// realClock is the default Clock implementation. +type realClock struct{} + +// Now returns the current time. +func (c *realClock) Now() time.Time { + return time.Now() +} + +type builder[K comparable, V any] struct { + capacity int + policy policy[K, V] + evictFunc EvictFunc[K, V] + clock Clock + expiry time.Duration +} + +// New returns a new cache builder with the given capacity. Use the builder to +// set the eviction policy, eviction callback, and other options. Call Build() +// to create the cache. +func New[K comparable, V any](capacity int) *builder[K, V] { + return &builder[K, V]{ + capacity: capacity, + policy: new(lru[K, V]), + evictFunc: NopEvict[K, V], + clock: new(realClock), + } +} + +// WithEvictFunc sets the EvictFunc for the cache. +func (b *builder[K, V]) WithEvictFunc(fn EvictFunc[K, V]) *builder[K, V] { + b.evictFunc = fn + return b +} + +// WithPolicy sets the eviction policy for the cache. The default policy is LRU. +func (b *builder[K, V]) WithPolicy(policy CachePolicy) *builder[K, V] { + switch policy { + case LRU: + b.policy = new(lru[K, V]) + case LFU: + b.policy = new(lfu[K, V]) + case SLRU: + b.policy = new(slru[K, V]) + case TinyLFU: + b.policy = new(tinyLFU[K, V]) + default: + panic(fmt.Sprintf("cache: unsupported policy \"%s\"", policy.String())) + } + + return b +} + +// LRU sets the cache eviction policy to LRU (least recently used). +func (b *builder[K, V]) LRU() *builder[K, V] { + return b.WithPolicy(LRU) +} + +// LFU sets the cache eviction policy to LFU (least frequently used). +func (b *builder[K, V]) LFU() *builder[K, V] { + return b.WithPolicy(LFU) +} + +// SLRU sets the cache eviction policy to SLRU (segmented least recently used). +func (b *builder[K, V]) SLRU() *builder[K, V] { + return b.WithPolicy(SLRU) +} + +// TinyLFU sets the cache eviction policy to TinyLFU (tiny least frequently used). +func (b *builder[K, V]) TinyLFU() *builder[K, V] { + return b.WithPolicy(TinyLFU) +} + +// WithClock sets the Clock for the cache. +func (b *builder[K, V]) WithClock(clock Clock) *builder[K, V] { + b.clock = clock + return b +} + +// WithExpiry sets the expiry for the cache. +func (b *builder[K, V]) WithExpiry(expiry time.Duration) *builder[K, V] { + b.expiry = expiry + return b +} + +// Build creates the cache. +func (b *builder[K, V]) Build() Interface[K, V] { + c := &cache[K, V]{ + byKey: make(map[K]*cacheItem[K, V]), + events: make(chan cacheEvent[K, V], 100), + + policy: b.policy, + clock: b.clock, + expiry: b.expiry, + onEvictCallback: b.evictFunc, + } + + c.policy.init(b.capacity) + + c.closeWG.Add(1) + go c.processEvents() + + return c +} + // cache is the generic cache type. type cache[K comparable, V any] struct { byKey map[K]*cacheItem[K, V] // Hashmap containing *CacheItems for O(1) access @@ -140,34 +225,14 @@ type cache[K comparable, V any] struct { // and frequency of the evicted item are passed to the function. Set to // a custom function to handle evicted items. The default is a no-op. onEvictCallback EvictFunc[K, V] -} - -// New returns a new cache with the given capacity and options. -func New[K comparable, V any](capacity int, options ...Option[K, V]) Interface[K, V] { - return new(cache[K, V]).init(capacity, options...) -} -// init initializes the cache with the given capacity and options. It must be -// called before the cache can be used. -func (c *cache[K, V]) init(capacity int, opts ...Option[K, V]) *cache[K, V] { - c.byKey = make(map[K]*cacheItem[K, V]) - c.events = make(chan cacheEvent[K, V], 100) - c.onEvictCallback = NopEvict[K, V] + // clock is used to get the current time. Set to a custom Clock to use a + // custom clock. The default is the real time clock. + clock Clock - for _, opt := range opts { - opt(c) - } - - if c.policy == nil { - c.policy = new(lru[K, V]) - } - - c.policy.init(capacity) - - c.closeWG.Add(1) - go c.processEvents() - - return c + // expiry is the duration after which an item is considered expired. Set to + // a custom duration to use a custom expiry. The default is no expiry. + expiry time.Duration } // processEvents processes events in a separate goroutine. @@ -244,6 +309,10 @@ func (c *cache[K, V]) Set(key K, value V) { if item, ok := c.byKey[key]; ok { item.value = value + if c.expiry > 0 { + item.expiration = c.clock.Now().Add(c.expiry) + } + c.policy.access(item) return @@ -254,7 +323,14 @@ func (c *cache[K, V]) Set(key K, value V) { c.evict() } - item := &cacheItem[K, V]{key: key, value: value} + item := &cacheItem[K, V]{ + key: key, + value: value, + } + + if c.expiry > 0 { + item.expiration = c.clock.Now().Add(c.expiry) + } c.byKey[key] = item @@ -278,6 +354,11 @@ func (c *cache[K, V]) Get(key K) (V, bool) { return c.zeroValue(), false } + if c.expiry > 0 && item.expiration.Before(c.clock.Now()) { + c.evictItem(item) + return c.zeroValue(), false + } + c.policy.access(item) return item.value, true @@ -326,7 +407,11 @@ func (c *cache[K, V]) zeroValue() V { // evict removes an item from the cache and sends an evict event. func (c *cache[K, V]) evict() { item := c.policy.victim() + c.evictItem(item) +} +// evictItem removes the given item from the cache and sends an evict event. +func (c *cache[K, V]) evictItem(item *cacheItem[K, V]) { delete(c.byKey, item.key) c.size-- diff --git a/go/appencryption/pkg/cache/cache_test.go b/go/appencryption/pkg/cache/cache_test.go index 50e95ad51..d59796b5a 100644 --- a/go/appencryption/pkg/cache/cache_test.go +++ b/go/appencryption/pkg/cache/cache_test.go @@ -2,6 +2,7 @@ package cache_test import ( "testing" + "time" "github.com/stretchr/testify/suite" @@ -10,15 +11,38 @@ import ( type CacheSuite struct { suite.Suite - cache cache.Interface[int, string] + cache cache.Interface[int, string] + clock *fakeClock + expiry time.Duration } func TestCacheSuite(t *testing.T) { suite.Run(t, new(CacheSuite)) } +// fakeClock is a fake clock that returns a static time. +type fakeClock struct { + now time.Time +} + +// Now returns the current time. +func (c *fakeClock) Now() time.Time { + return c.now +} + +// SetNow sets the current time. +func (c *fakeClock) SetNow(now time.Time) { + c.now = now +} + func (suite *CacheSuite) SetupTest() { - suite.cache = cache.New[int, string](2) + suite.clock = &fakeClock{ + now: time.Now(), + } + + suite.expiry = time.Hour + + suite.cache = cache.New[int, string](2).WithClock(suite.clock).WithExpiry(suite.expiry).Build() } func (suite *CacheSuite) TestNew() { @@ -43,3 +67,26 @@ func (suite *CacheSuite) TestClosing() { // closing again does nothing suite.Assert().NoError(suite.cache.Close()) } + +func (suite *CacheSuite) TestExpiry() { + suite.cache.Set(1, "one") + suite.cache.Set(2, "two") + + one, ok := suite.cache.Get(1) + suite.Assert().Equal("one", one) + suite.Assert().True(ok) + + two, ok := suite.cache.Get(2) + suite.Assert().Equal("two", two) + suite.Assert().True(ok) + + // advance clock + suite.clock.SetNow(suite.clock.Now().Add(suite.expiry + time.Second)) + + // get should return false + _, ok = suite.cache.Get(1) + suite.Assert().False(ok) + + _, ok = suite.cache.Get(2) + suite.Assert().False(ok) +} diff --git a/go/appencryption/pkg/cache/lfu_example_test.go b/go/appencryption/pkg/cache/lfu_example_test.go index 7ca28fc26..69a6e423c 100644 --- a/go/appencryption/pkg/cache/lfu_example_test.go +++ b/go/appencryption/pkg/cache/lfu_example_test.go @@ -9,14 +9,16 @@ import ( func ExampleNew() { evictionMsg := make(chan string) - // Create a new LFU cache with a capacity of 3 items and an eviction callback. - cache := cache.New[int, string](3, cache.WithPolicy[int, string](cache.LFU), cache.WithEvictFunc(func(key int, value string) { - // This callback is executed via a background goroutine whenever an - // item is evicted from the cache. We use a channel to synchronize - // the goroutine with this example function so we can verify the - // item that was evicted. + // This callback is executed via a background goroutine whenever an + // item is evicted from the cache. We use a channel to synchronize + // the goroutine with this example function so we can verify the + // item that was evicted. + evict := func(key int, value string) { evictionMsg <- fmt.Sprintln("evicted:", key, value) - })) + } + + // Create a new LFU cache with a capacity of 3 items and an eviction callback. + cache := cache.New[int, string](3).LFU().WithEvictFunc(evict).Build() // Add some items to the cache. cache.Set(1, "foo") diff --git a/go/appencryption/pkg/cache/lfu_test.go b/go/appencryption/pkg/cache/lfu_test.go index a7aad1492..53807a253 100644 --- a/go/appencryption/pkg/cache/lfu_test.go +++ b/go/appencryption/pkg/cache/lfu_test.go @@ -20,7 +20,7 @@ func TestLFUSuite(t *testing.T) { } func (suite *LFUSuite) SetupTest() { - suite.cache = cache.New[int, string](2, cache.WithPolicy[int, string](cache.LFU)) + suite.cache = cache.New[int, string](2).LFU().Build() } func (suite *LFUSuite) TestNewLFU() { @@ -170,13 +170,12 @@ func (suite *LFUSuite) TestClose() { func (suite *LFUSuite) TestWithEvictFunc() { evicted := map[int]int{} - suite.cache = cache.New[int, string]( - 100, - cache.WithPolicy[int, string](cache.LFU), - cache.WithEvictFunc(func(key int, _ string) { + suite.cache = cache.New[int, string](100). + WithEvictFunc(func(key int, _ string) { evicted[key] = 1 - }), - ) + }). + LFU(). + Build() // overfill the cache for i := 0; i < 105; i++ { diff --git a/go/appencryption/pkg/cache/lru_test.go b/go/appencryption/pkg/cache/lru_test.go index c126d11fb..654af8197 100644 --- a/go/appencryption/pkg/cache/lru_test.go +++ b/go/appencryption/pkg/cache/lru_test.go @@ -19,7 +19,7 @@ func TestLRUSuite(t *testing.T) { } func (suite *LRUSuite) SetupTest() { - suite.cache = cache.New[int, string](10) + suite.cache = cache.New[int, string](10).Build() } func (suite *LRUSuite) TestNewLRU() { @@ -117,14 +117,14 @@ func (suite *LRUSuite) TestWithEvictFunc() { done := make(chan struct{}) evicted := false - cache := cache.New[int, string](1, cache.WithEvictFunc(func(key int, value string) { + cache := cache.New[int, string](1).WithEvictFunc(func(key int, value string) { evicted = true suite.Assert().Equal(1, key) suite.Assert().Equal("one", value) close(done) - })) + }).Build() cache.Set(1, "one") cache.Set(2, "two") @@ -145,7 +145,7 @@ func TestSLRUSuite(t *testing.T) { } func (suite *SLRUSuite) SetupTest() { - suite.cache = cache.New[int, string](10, cache.WithPolicy[int, string](cache.SLRU)) + suite.cache = cache.New[int, string](10).SLRU().Build() } func (suite *SLRUSuite) TestNewSLRU() { @@ -257,14 +257,14 @@ func (suite *SLRUSuite) TestWithEvictFunc() { done := make(chan struct{}) evicted := false - cache := cache.New[int, string](10, cache.WithPolicy[int, string](cache.SLRU), cache.WithEvictFunc(func(key int, value string) { + cache := cache.New[int, string](10).SLRU().WithEvictFunc(func(key int, value string) { evicted = true suite.Assert().Equal(1, key) suite.Assert().Equal("#1", value) close(done) - })) + }).Build() // fill the cache to capacity for i := 0; i < cache.Capacity(); i++ { diff --git a/go/appencryption/pkg/cache/tlfu_test.go b/go/appencryption/pkg/cache/tlfu_test.go index d1ae5a3b5..ec95bc04d 100644 --- a/go/appencryption/pkg/cache/tlfu_test.go +++ b/go/appencryption/pkg/cache/tlfu_test.go @@ -20,7 +20,7 @@ func TestTinyLFUSuite(t *testing.T) { } func (suite *TinyLFUSuite) SetupTest() { - suite.cache = cache.New[int, string](100, cache.WithPolicy[int, string](cache.TinyLFU)) + suite.cache = cache.New[int, string](100).TinyLFU().Build() } func (suite *TinyLFUSuite) TestNewTinyLFU() { diff --git a/go/appencryption/session_cache.go b/go/appencryption/session_cache.go index 20488b4d3..0bef41555 100644 --- a/go/appencryption/session_cache.go +++ b/go/appencryption/session_cache.go @@ -373,14 +373,19 @@ func newSessionCache(loader sessionLoaderFunc, policy *CryptoPolicy) sessionCach default: log.Debugf("policy.SessionCacheEvictionPolicy is \"%s\"\n", policy.SessionCacheEvictionPolicy) - inner := cache.New[string, *Session]( - policy.SessionCacheMaxSize, - cache.WithPolicy[string, *Session](cache.CachePolicy(policy.SessionCacheEvictionPolicy)), - cache.WithEvictFunc[string, *Session](func(k string, v *Session) { - go v.encryption.(*sharedEncryption).Remove() - }), - ) - - return newSessionCacheWithCache(loader, policy, inner) + cb := cache.New[string, *Session](policy.SessionCacheMaxSize) + cb.WithEvictFunc(func(k string, v *Session) { + go v.encryption.(*sharedEncryption).Remove() + }) + + if policy.SessionCacheDuration > 0 { + cb.WithExpiry(policy.SessionCacheDuration) + } + + if policy.SessionCacheEvictionPolicy != "" { + cb.WithPolicy(cache.CachePolicy(policy.SessionCacheEvictionPolicy)) + } + + return newSessionCacheWithCache(loader, policy, cb.Build()) } } From e1e428138e8b56be958471c92c1295f234e25ffe Mon Sep 17 00:00:00 2001 From: Bo Thompson Date: Mon, 16 Oct 2023 11:43:18 -0700 Subject: [PATCH 07/20] exclude traces from integration test runs --- build/go/integration_test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/go/integration_test.sh b/build/go/integration_test.sh index 5ad51afc1..d4f61898a 100755 --- a/build/go/integration_test.sh +++ b/build/go/integration_test.sh @@ -4,4 +4,4 @@ # Note the use of `-p 1` is required to prevent multiple test packages from running in # parallel (default), ensuring access to any shared resource (e.g., dynamodb-local) # is serialized. -gotestsum -f testname -- -p 1 -race -coverprofile coverage.out -v ./integrationtest/... +gotestsum -f testname -- -p 1 -race -coverprofile coverage.out -v `go list ./integrationtest/... | grep -v traces` From 7145e9dee5351b866b49a6f5b08abdb626f1c885 Mon Sep 17 00:00:00 2001 From: Bo Thompson Date: Mon, 16 Oct 2023 11:47:53 -0700 Subject: [PATCH 08/20] fix race condition in test --- go/appencryption/pkg/cache/lfu_test.go | 54 ++++---------------------- 1 file changed, 7 insertions(+), 47 deletions(-) diff --git a/go/appencryption/pkg/cache/lfu_test.go b/go/appencryption/pkg/cache/lfu_test.go index 53807a253..5869697fa 100644 --- a/go/appencryption/pkg/cache/lfu_test.go +++ b/go/appencryption/pkg/cache/lfu_test.go @@ -2,6 +2,7 @@ package cache_test import ( "fmt" + "sync" "testing" "time" @@ -90,53 +91,6 @@ func (suite *LFUSuite) TestDelete() { suite.Assert().Equal(0, suite.cache.Len()) } -// func (suite *LFUSuite) TestEach() { -// suite.cache.Set(1, "one") -// suite.cache.Set(1, "one") // increment frequency -// suite.cache.Set(2, "two") -// suite.cache.Set(3, "three") // evict 2 - -// var ( -// keys []int -// values []string -// ) - -// suite.cache.Each(func(key int, value string) bool { -// keys = append(keys, key) -// values = append(values, value) - -// return true -// }) - -// // Each() iterates in order of least frequently used -// suite.Assert().Equal([]int{3, 1}, keys) -// suite.Assert().Equal([]string{"three", "one"}, values) -// } - -// func (suite *LFUSuite) TestEachWithEarlyExit() { -// suite.cache.Set(1, "one") -// suite.cache.Set(1, "one") // increment frequency -// suite.cache.Set(2, "two") -// suite.cache.Set(3, "three") // evict 2 - -// var ( -// keys []int -// values []string -// ) - -// suite.cache.Each(func(key int, value string) bool { -// keys = append(keys, key) -// values = append(values, value) - -// // early exit -// return false -// }) - -// // Each() iterates in order of least frequently used -// suite.Assert().Equal([]int{3}, keys) -// suite.Assert().Equal([]string{"three"}, values) -// } - func (suite *LFUSuite) TestEviction() { suite.cache.Set(1, "one") suite.cache.Set(2, "two") @@ -168,11 +122,14 @@ func (suite *LFUSuite) TestClose() { } func (suite *LFUSuite) TestWithEvictFunc() { + mux := sync.Mutex{} evicted := map[int]int{} suite.cache = cache.New[int, string](100). WithEvictFunc(func(key int, _ string) { + mux.Lock() evicted[key] = 1 + mux.Unlock() }). LFU(). Build() @@ -184,6 +141,9 @@ func (suite *LFUSuite) TestWithEvictFunc() { // wait for the background goroutine to evict items suite.Assert().Eventually(func() bool { + mux.Lock() + defer mux.Unlock() + return len(evicted) == 5 }, 100*time.Millisecond, 10*time.Millisecond, "eviction callback was not called") From 87f08e33e3c169b3896f1a122122df5b5ef5f8fd Mon Sep 17 00:00:00 2001 From: Bo Thompson Date: Mon, 16 Oct 2023 11:51:03 -0700 Subject: [PATCH 09/20] remove mango session cache implementation --- go/appencryption/session_cache.go | 257 ++----------------------- go/appencryption/session_cache_test.go | 100 +++------- go/appencryption/session_test.go | 6 +- 3 files changed, 51 insertions(+), 312 deletions(-) diff --git a/go/appencryption/session_cache.go b/go/appencryption/session_cache.go index 0bef41555..cfb08cec4 100644 --- a/go/appencryption/session_cache.go +++ b/go/appencryption/session_cache.go @@ -4,8 +4,6 @@ import ( "sync" "time" - mango "github.com/goburrow/cache" - "github.com/godaddy/asherah/go/appencryption/pkg/cache" "github.com/godaddy/asherah/go/appencryption/pkg/log" ) @@ -16,198 +14,10 @@ type sessionCache interface { Close() } -// cacheStash is a temporary staging ground for the session cache. -type cacheStash struct { - tmp map[string]*Session - mux sync.RWMutex - events chan stashEvent -} - -type event uint8 - -const ( - stashClose event = iota - stashRemove -) - -type stashEvent struct { - id string - event event -} - -func (c *cacheStash) process() { - for e := range c.events { - switch e.event { - case stashRemove: - c.mux.Lock() - delete(c.tmp, e.id) - c.mux.Unlock() - case stashClose: - close(c.events) - - return - } - } -} - -func (c *cacheStash) add(id string, s *Session) { - c.mux.Lock() - c.tmp[id] = s - c.mux.Unlock() -} - -func (c *cacheStash) get(id string) (s *Session, ok bool) { - c.mux.RLock() - s, ok = c.tmp[id] - c.mux.RUnlock() - - return s, ok -} - -func (c *cacheStash) remove(id string) { - c.events <- stashEvent{ - id: id, - event: stashRemove, - } -} - -func (c *cacheStash) close() { - c.events <- stashEvent{ - event: stashClose, - } -} - -func (c *cacheStash) len() int { - c.mux.RLock() - defer c.mux.RUnlock() - - return len(c.tmp) -} - -func newCacheStash() *cacheStash { - return &cacheStash{ - tmp: make(map[string]*Session), - events: make(chan stashEvent), - } -} - -// mangoCache is a sessionCache implementation based on goburrow's -// Mango cache (https://github.com/goburrow/cache). -type mangoCache struct { - inner mango.LoadingCache - loader sessionLoaderFunc - - // mu protects the inner queue - mu sync.Mutex - - stash *cacheStash -} - -func (m *mangoCache) Get(id string) (*Session, error) { - m.mu.Lock() - defer m.mu.Unlock() - - sess, err := m.getOrAdd(id) - if err != nil { - return nil, err - } - - incrementSharedSessionUsage(sess) - - return sess, nil -} - -func (m *mangoCache) getOrAdd(id string) (*Session, error) { - // (fast path) if it's cached return it immediately - if val, ok := m.inner.GetIfPresent(id); ok { - sess := sessionOrPanic(val) - - m.stash.remove(id) - - return sess, nil - } - - // check the stash first to prevent mango from reloading a value currently in queue to be cached. - if sess, ok := m.stash.get(id); ok { - return sess, nil - } - - // m.inner.Get will add a new item via the loader on cache miss. However, newly loaded keys are added to - // the cache asynchronously, so we'll need to add it to the stash down below. - val, err := m.inner.Get(id) - if err != nil { - return nil, err - } - - sess := sessionOrPanic(val) - - // if we're here then mango has loaded a new cache value (session), so we'll add it to the tmp cache for now to - // allow mango an opportunity to actually cache the value. - m.stash.add(id, sess) - - return sess, nil -} - -func sessionOrPanic(val mango.Value) *Session { - sess, ok := val.(*Session) - if !ok { - panic("unexpected value") - } - - return sess -} - func incrementSharedSessionUsage(s *Session) { s.encryption.(*sharedEncryption).incrementUsage() } -func (m *mangoCache) Count() int { - s := &mango.Stats{} - m.inner.Stats(s) - - return int(s.LoadSuccessCount - s.EvictionCount) -} - -func (m *mangoCache) Close() { - if log.DebugEnabled() { - s := &mango.Stats{} - m.inner.Stats(s) - log.Debugf("session cache stash len = %d\n", m.stash.len()) - log.Debugf("%v\n", s) - } - - m.inner.Close() - m.stash.close() -} - -func mangoRemovalListener(m *mangoCache, k mango.Key, v mango.Value) { - m.stash.remove(k.(string)) - - go v.(*Session).encryption.(*sharedEncryption).Remove() -} - -func newMangoCache(sessionLoader sessionLoaderFunc, policy *CryptoPolicy) *mangoCache { - cache := &mangoCache{ - loader: sessionLoader, - stash: newCacheStash(), - } - - cache.inner = mango.NewLoadingCache( - func(k mango.Key) (mango.Value, error) { - return sessionLoader(k.(string)) - }, - mango.WithMaximumSize(policy.SessionCacheMaxSize), - mango.WithExpireAfterAccess(policy.SessionCacheDuration), - mango.WithRemovalListener(func(k mango.Key, v mango.Value) { - mangoRemovalListener(cache, k, v) - }), - ) - - go cache.stash.process() - - return cache -} - // sharedEncryption is used to track the number of concurrent users to ensure sessions remain // cached while in use. type sharedEncryption struct { @@ -251,35 +61,6 @@ func (s *sharedEncryption) Remove() { // sessionLoaderFunc retrieves a Session corresponding to the given partition ID. type sessionLoaderFunc func(id string) (*Session, error) -// newMangoSessionCache returns a new SessionCache with the configured cache implementation -// using the provided SessionLoaderFunc and CryptoPolicy. -func newMangoSessionCache(loader sessionLoaderFunc, policy *CryptoPolicy) sessionCache { - wrapper := func(id string) (*Session, error) { - s, err := loader(id) - if err != nil { - return nil, err - } - - _, ok := s.encryption.(*sharedEncryption) - if !ok { - mu := new(sync.Mutex) - orig := s.encryption - wrapped := &sharedEncryption{ - Encryption: orig, - mu: mu, - cond: sync.NewCond(mu), - created: time.Now(), - } - - sessionInjectEncryption(s, wrapped) - } - - return s, nil - } - - return newMangoCache(wrapper, policy) -} - // sessionInjectEncryption is used to inject e into s and is primarily used for testing. func sessionInjectEncryption(s *Session, e Encryption) { log.Debugf("injecting Encryption(%p) into Session(%p)", e, s) @@ -292,6 +73,8 @@ func sessionInjectEncryption(s *Session, e Encryption) { func newSessionCacheWithCache(loader sessionLoaderFunc, policy *CryptoPolicy, cache cache.Interface[string, *Session]) sessionCache { return &cacheWrapper{ loader: func(id string) (*Session, error) { + log.Debugf("loading session for id: %s", id) + s, err := loader(id) if err != nil { return nil, err @@ -318,6 +101,8 @@ func newSessionCacheWithCache(loader sessionLoaderFunc, policy *CryptoPolicy, ca } } +// cacheWrapper is a wrapper around a cache.Interface[string, *Session] that implements the +// sessionCache interface. type cacheWrapper struct { loader sessionLoaderFunc policy *CryptoPolicy @@ -360,32 +145,24 @@ func (c *cacheWrapper) Count() int { } func (c *cacheWrapper) Close() { + log.Debugf("closing session cache") + c.cache.Close() } func newSessionCache(loader sessionLoaderFunc, policy *CryptoPolicy) sessionCache { - switch policy.SessionCacheEvictionPolicy { - case "": - log.Debugf("policy.SessionCacheEvictionPolicy is empty\n") + cb := cache.New[string, *Session](policy.SessionCacheMaxSize) + cb.WithEvictFunc(func(k string, v *Session) { + go v.encryption.(*sharedEncryption).Remove() + }) - // TODO: remove mango cache - return newMangoSessionCache(loader, policy) - default: - log.Debugf("policy.SessionCacheEvictionPolicy is \"%s\"\n", policy.SessionCacheEvictionPolicy) - - cb := cache.New[string, *Session](policy.SessionCacheMaxSize) - cb.WithEvictFunc(func(k string, v *Session) { - go v.encryption.(*sharedEncryption).Remove() - }) - - if policy.SessionCacheDuration > 0 { - cb.WithExpiry(policy.SessionCacheDuration) - } - - if policy.SessionCacheEvictionPolicy != "" { - cb.WithPolicy(cache.CachePolicy(policy.SessionCacheEvictionPolicy)) - } + if policy.SessionCacheDuration > 0 { + cb.WithExpiry(policy.SessionCacheDuration) + } - return newSessionCacheWithCache(loader, policy, cb.Build()) + if policy.SessionCacheEvictionPolicy != "" { + cb.WithPolicy(cache.CachePolicy(policy.SessionCacheEvictionPolicy)) } + + return newSessionCacheWithCache(loader, policy, cb.Build()) } diff --git a/go/appencryption/session_cache_test.go b/go/appencryption/session_cache_test.go index 153fc60f6..5d7f17a70 100644 --- a/go/appencryption/session_cache_test.go +++ b/go/appencryption/session_cache_test.go @@ -96,7 +96,7 @@ func TestNewSessionCache(t *testing.T) { return &Session{}, nil } - cache := newMangoSessionCache(loader, NewCryptoPolicy()) + cache := newSessionCache(loader, NewCryptoPolicy()) defer cache.Close() require.NotNil(t, cache) @@ -109,7 +109,7 @@ func TestSessionCacheGetUsesLoader(t *testing.T) { return session, nil } - cache := newMangoSessionCache(loader, NewCryptoPolicy()) + cache := newSessionCache(loader, NewCryptoPolicy()) require.NotNil(t, cache) defer cache.Close() @@ -129,7 +129,7 @@ func TestSessionCacheGetDoesNotUseLoaderOnHit(t *testing.T) { return session, nil } - cache := newMangoSessionCache(loader, NewCryptoPolicy()) + cache := newSessionCache(loader, NewCryptoPolicy()) require.NotNil(t, cache) defer cache.Close() @@ -155,7 +155,7 @@ func TestSessionCacheGetReturnLoaderError(t *testing.T) { return nil, assert.AnError } - cache := newMangoSessionCache(loader, NewCryptoPolicy()) + cache := newSessionCache(loader, NewCryptoPolicy()) require.NotNil(t, cache) defer cache.Close() @@ -169,7 +169,7 @@ func TestSessionCacheCount(t *testing.T) { totalSessions := 10 b := newSessionBucket() - cache := newMangoSessionCache(b.load, NewCryptoPolicy()) + cache := newSessionCache(b.load, NewCryptoPolicy()) require.NotNil(t, cache) defer cache.Close() @@ -189,7 +189,7 @@ func TestSessionCacheMaxCount(t *testing.T) { policy := NewCryptoPolicy() policy.SessionCacheMaxSize = maxSessions - cache := newMangoSessionCache(b.load, policy) + cache := newSessionCache(b.load, policy) require.NotNil(t, cache) defer cache.Close() @@ -227,15 +227,20 @@ func TestSessionCacheMaxCount(t *testing.T) { func TestSessionCacheDuration(t *testing.T) { ttl := time.Millisecond * 100 - // can't use more than 16 sessions here as that is the max drain - // for the mango cache implementation totalSessions := 16 b := newSessionBucket() policy := NewCryptoPolicy() policy.SessionCacheDuration = ttl - cache := newMangoSessionCache(b.load, policy) + loaded := 0 + + loader := func(id string) (*Session, error) { + loaded++ + return b.load(id) + } + + cache := newSessionCache(loader, policy) require.NotNil(t, cache) defer cache.Close() @@ -244,18 +249,23 @@ func TestSessionCacheDuration(t *testing.T) { cache.Get(strconv.Itoa(i)) } + expectedCount := totalSessions + + // assert we have a load for each session + require.Equal(t, expectedCount, loaded) + // ensure the ttl has elapsed time.Sleep(ttl + time.Millisecond*50) - expectedCount := 0 + assert.Eventually(t, func() bool { + for i := 0; i < totalSessions; i++ { + cache.Get(strconv.Itoa(i)) + } - // mango cache implementation only reaps expired entries following a write, so we'll write a new - // cache entry and ensure it's the only one left - _, _ = cache.Get("99") // IDs 0-15 were created above - expectedCount = 1 + // now that the ttl has elapsed, we should have loaded the sessions again + // and the total loaded should be greater than the expected count + return loaded > expectedCount - assert.Eventually(t, func() bool { - return cache.Count() == expectedCount }, time.Second*10, time.Millisecond*10) } @@ -270,7 +280,7 @@ func (t *testLogger) Debugf(f string, v ...interface{}) { func TestSessionCacheCloseWithDebugLogging(t *testing.T) { b := newSessionBucket() - cache := newMangoSessionCache(b.load, NewCryptoPolicy()) + cache := newSessionCache(b.load, NewCryptoPolicy()) require.NotNil(t, cache) l := new(testLogger) @@ -283,7 +293,7 @@ func TestSessionCacheCloseWithDebugLogging(t *testing.T) { // assert additional debug info was written to log assert.NotEqual(t, 0, l.Len()) - assert.Contains(t, l.String(), "session cache stash len = 0") + assert.Contains(t, l.String(), "closing session cache") log.SetLogger(nil) } @@ -291,7 +301,7 @@ func TestSessionCacheCloseWithDebugLogging(t *testing.T) { func TestSharedSessionCloseOnCacheClose(t *testing.T) { b := newSessionBucket() - cache := newMangoSessionCache(b.load, NewCryptoPolicy()) + cache := newSessionCache(b.load, NewCryptoPolicy()) require.NotNil(t, cache) s, err := cache.Get("my-item") @@ -322,7 +332,7 @@ func TestSharedSessionCloseOnEviction(t *testing.T) { var firstBatch [max]*Session - cache := newMangoSessionCache(b.load, policy) + cache := newSessionCache(b.load, policy) require.NotNil(t, cache) defer cache.Close() @@ -364,7 +374,7 @@ func TestSharedSessionCloseOnEviction(t *testing.T) { func TestSharedSessionCloseDoesNotCloseUnderlyingSession(t *testing.T) { b := newSessionBucket() - cache := newMangoSessionCache(b.load, NewCryptoPolicy()) + cache := newSessionCache(b.load, NewCryptoPolicy()) require.NotNil(t, cache) defer cache.Close() @@ -385,51 +395,3 @@ func TestSharedSessionCloseDoesNotCloseUnderlyingSession(t *testing.T) { // shared sessions aren't actually closed until evicted from the cache assert.False(t, b.IsClosed(s1)) } - -func TestCacheStash(t *testing.T) { - id := "stashed item" - stash := newCacheStash() - - complete := make(chan bool) - - go func() { - stash.process() - - complete <- true - }() - - // stash is empty - s, ok := stash.get(id) - assert.Nil(t, s) - assert.False(t, ok) - assert.Equal(t, 0, stash.len()) - - // create a new session and stash it - sess := new(Session) - stash.add(id, sess) - - // stash now contains the session we just added - s, ok = stash.get(id) - assert.Equal(t, sess, s) - assert.True(t, ok) - assert.Equal(t, 1, stash.len()) - - // now remove the stashed session - stash.remove(id) - - // remove events are queued asynchronously - assert.Eventually(t, func() bool { - _, ok := stash.get(id) - - return !ok - }, 500*time.Millisecond, 10*time.Millisecond) - - // and verify it's gone - s, ok = stash.get(id) - assert.Nil(t, s) - assert.False(t, ok) - assert.Equal(t, 0, stash.len()) - - stash.close() - assert.True(t, <-complete) -} diff --git a/go/appencryption/session_test.go b/go/appencryption/session_test.go index b0c9468e5..b0d6147f8 100644 --- a/go/appencryption/session_test.go +++ b/go/appencryption/session_test.go @@ -121,9 +121,9 @@ func TestNewSessionFactory(t *testing.T) { } func TestNewSessionFactory_WithSessionCache(t *testing.T) { - policy := &CryptoPolicy{ - CacheSessions: true, - } + policy := NewCryptoPolicy() + policy.CacheSessions = true + factory := NewSessionFactory(&Config{ Policy: policy, }, nil, nil, nil) From b7081282eaded576a35c0b4521e3609261ae354d Mon Sep 17 00:00:00 2001 From: Bo Thompson Date: Mon, 16 Oct 2023 11:58:01 -0700 Subject: [PATCH 10/20] remove deprecated SessionCacheEngine option --- go/appencryption/policy.go | 18 ------------------ go/appencryption/policy_test.go | 7 ------- 2 files changed, 25 deletions(-) diff --git a/go/appencryption/policy.go b/go/appencryption/policy.go index 17bd67119..ffe7e963a 100644 --- a/go/appencryption/policy.go +++ b/go/appencryption/policy.go @@ -55,12 +55,6 @@ type CryptoPolicy struct { // SessionCacheDuration controls the amount of time a session will remain cached without being accessed // if session caching is enabled. SessionCacheDuration time.Duration - // WithSessionCacheEngine determines the underlying cache implemenataion in use by the session cache - // if session caching is enabled. - // - // Deprecated: multiple cache implementations are no longer supported and this option will be removed - // in a future release. - SessionCacheEngine string // SessionCacheEvictionPolicy controls the eviction policy to use for the shared cache. // Supported values are "lru", "lfu", "slru", and "tinylfu". Default is "slru". SessionCacheEvictionPolicy string @@ -122,17 +116,6 @@ func WithSessionCacheDuration(d time.Duration) PolicyOption { } } -// WithSessionCacheEngine determines the underlying cache implemenataion in use by the session cache -// if session caching is enabled. -// -// Deprecated: multiple cache implementations are no longer supported and this option will be removed -// in a future release. -func WithSessionCacheEngine(engine string) PolicyOption { - return func(policy *CryptoPolicy) { - policy.SessionCacheEngine = engine - } -} - // NewCryptoPolicy returns a new CryptoPolicy with default values. func NewCryptoPolicy(opts ...PolicyOption) *CryptoPolicy { policy := &CryptoPolicy{ @@ -147,7 +130,6 @@ func NewCryptoPolicy(opts ...PolicyOption) *CryptoPolicy { CacheSessions: false, SessionCacheMaxSize: DefaultSessionCacheMaxSize, SessionCacheDuration: DefaultSessionCacheDuration, - SessionCacheEngine: DefaultSessionCacheEngine, } for _, opt := range opts { diff --git a/go/appencryption/policy_test.go b/go/appencryption/policy_test.go index ef270bdc8..f269b9a21 100644 --- a/go/appencryption/policy_test.go +++ b/go/appencryption/policy_test.go @@ -23,7 +23,6 @@ func Test_NewCryptoPolicy_WithDefaults(t *testing.T) { assert.False(t, p.CacheSessions) assert.Equal(t, DefaultSessionCacheMaxSize, p.SessionCacheMaxSize) assert.Equal(t, DefaultSessionCacheDuration, p.SessionCacheDuration) - assert.Equal(t, DefaultSessionCacheEngine, p.SessionCacheEngine) } func Test_NewCryptoPolicy_WithOptions(t *testing.T) { @@ -31,7 +30,6 @@ func Test_NewCryptoPolicy_WithOptions(t *testing.T) { expireAfterDuration := time.Second * 100 sessionCacheMaxSize := 42 sessionCacheDuration := time.Second * 42 - sessionCacheEngine := "deprecated" policy := NewCryptoPolicy( WithRevokeCheckInterval(revokeCheckInterval), @@ -40,7 +38,6 @@ func Test_NewCryptoPolicy_WithOptions(t *testing.T) { WithSessionCache(), WithSessionCacheMaxSize(sessionCacheMaxSize), WithSessionCacheDuration(sessionCacheDuration), - WithSessionCacheEngine(sessionCacheEngine), ) assert.Equal(t, revokeCheckInterval, policy.RevokeCheckInterval) @@ -50,7 +47,6 @@ func Test_NewCryptoPolicy_WithOptions(t *testing.T) { assert.True(t, policy.CacheSessions) assert.Equal(t, sessionCacheMaxSize, policy.SessionCacheMaxSize) assert.Equal(t, sessionCacheDuration, policy.SessionCacheDuration) - assert.Equal(t, sessionCacheEngine, policy.SessionCacheEngine) } func Test_NewCryptoPolicy_WithOptions_SharedKeyCache(t *testing.T) { @@ -59,7 +55,6 @@ func Test_NewCryptoPolicy_WithOptions_SharedKeyCache(t *testing.T) { keyCacheMaxSize := 10 sessionCacheMaxSize := 42 sessionCacheDuration := time.Second * 42 - sessionCacheEngine := "deprecated" policy := NewCryptoPolicy( WithRevokeCheckInterval(revokeCheckInterval), @@ -68,7 +63,6 @@ func Test_NewCryptoPolicy_WithOptions_SharedKeyCache(t *testing.T) { WithSessionCache(), WithSessionCacheMaxSize(sessionCacheMaxSize), WithSessionCacheDuration(sessionCacheDuration), - WithSessionCacheEngine(sessionCacheEngine), ) assert.Equal(t, revokeCheckInterval, policy.RevokeCheckInterval) @@ -80,7 +74,6 @@ func Test_NewCryptoPolicy_WithOptions_SharedKeyCache(t *testing.T) { assert.True(t, policy.CacheSessions) assert.Equal(t, sessionCacheMaxSize, policy.SessionCacheMaxSize) assert.Equal(t, sessionCacheDuration, policy.SessionCacheDuration) - assert.Equal(t, sessionCacheEngine, policy.SessionCacheEngine) } func Test_IsKeyExpired(t *testing.T) { From 40555f2c23524f19830ad2f4a7c20df4d1b85c6c Mon Sep 17 00:00:00 2001 From: Bo Thompson Date: Mon, 16 Oct 2023 12:01:16 -0700 Subject: [PATCH 11/20] rename pkg/cache/internal/(LICENSE -> NOTICE) --- go/appencryption/integrationtest/traces/README.md | 2 +- go/appencryption/pkg/cache/internal/{LICENSE => NOTICE} | 0 go/appencryption/pkg/cache/internal/doc.go | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename go/appencryption/pkg/cache/internal/{LICENSE => NOTICE} (100%) diff --git a/go/appencryption/integrationtest/traces/README.md b/go/appencryption/integrationtest/traces/README.md index c3ed5ce52..f86b8903d 100644 --- a/go/appencryption/integrationtest/traces/README.md +++ b/go/appencryption/integrationtest/traces/README.md @@ -6,7 +6,7 @@ different cache configurations. The source code for this package is derived from the package of the same name in the [Mango Cache](https://github.com/goburrow/cache) project. See -[LICENSE](../../pkg/cache/internal/LICENSE) for copyright and +[NOTICE](../../pkg/cache/internal/NOTICE) for copyright and licensing information. ## Traces diff --git a/go/appencryption/pkg/cache/internal/LICENSE b/go/appencryption/pkg/cache/internal/NOTICE similarity index 100% rename from go/appencryption/pkg/cache/internal/LICENSE rename to go/appencryption/pkg/cache/internal/NOTICE diff --git a/go/appencryption/pkg/cache/internal/doc.go b/go/appencryption/pkg/cache/internal/doc.go index 2615d20ba..00bd2f3a7 100644 --- a/go/appencryption/pkg/cache/internal/doc.go +++ b/go/appencryption/pkg/cache/internal/doc.go @@ -1,7 +1,7 @@ // Package internal contains data structures used by cache implementations. // // These data structures are derived from the [Mango Cache] source code. -// See LICENSE for important copyright and licensing information. +// See NOTICE for important copyright and licensing information. // // [Mango Cache]: https://github.com/goburrow/cache package internal From 1d9f2462f21718758c6fbca7c99d7d76bcb95a9c Mon Sep 17 00:00:00 2001 From: Bo Thompson Date: Mon, 16 Oct 2023 12:13:08 -0700 Subject: [PATCH 12/20] default session cache eviction policy to slru (as documented) --- go/appencryption/session_cache.go | 6 ++++-- go/appencryption/session_cache_test.go | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/go/appencryption/session_cache.go b/go/appencryption/session_cache.go index cfb08cec4..ae7e285eb 100644 --- a/go/appencryption/session_cache.go +++ b/go/appencryption/session_cache.go @@ -160,9 +160,11 @@ func newSessionCache(loader sessionLoaderFunc, policy *CryptoPolicy) sessionCach cb.WithExpiry(policy.SessionCacheDuration) } - if policy.SessionCacheEvictionPolicy != "" { - cb.WithPolicy(cache.CachePolicy(policy.SessionCacheEvictionPolicy)) + if policy.SessionCacheEvictionPolicy == "" { + policy.SessionCacheEvictionPolicy = "slru" } + cb.WithPolicy(cache.CachePolicy(policy.SessionCacheEvictionPolicy)) + return newSessionCacheWithCache(loader, policy, cb.Build()) } diff --git a/go/appencryption/session_cache_test.go b/go/appencryption/session_cache_test.go index 5d7f17a70..758155b8f 100644 --- a/go/appencryption/session_cache_test.go +++ b/go/appencryption/session_cache_test.go @@ -100,6 +100,8 @@ func TestNewSessionCache(t *testing.T) { defer cache.Close() require.NotNil(t, cache) + + assert.Equal(t, cache.(*cacheWrapper).policy.SessionCacheEvictionPolicy, "slru") } func TestSessionCacheGetUsesLoader(t *testing.T) { From 6bf4d095a4248756391b9be252118c4d830ccbd1 Mon Sep 17 00:00:00 2001 From: Bo Thompson Date: Mon, 16 Oct 2023 12:15:29 -0700 Subject: [PATCH 13/20] go mod tidy --- go/appencryption/go.mod | 1 - go/appencryption/go.sum | 6 ------ 2 files changed, 7 deletions(-) diff --git a/go/appencryption/go.mod b/go/appencryption/go.mod index e257de196..12566a83c 100644 --- a/go/appencryption/go.mod +++ b/go/appencryption/go.mod @@ -4,7 +4,6 @@ go 1.19 require ( github.com/aws/aws-sdk-go v1.46.7 - github.com/goburrow/cache v0.1.4 github.com/godaddy/asherah/go/securememory v0.1.5 github.com/google/uuid v1.4.0 github.com/pkg/errors v0.9.1 diff --git a/go/appencryption/go.sum b/go/appencryption/go.sum index ff7b57929..918b9dca6 100644 --- a/go/appencryption/go.sum +++ b/go/appencryption/go.sum @@ -2,10 +2,6 @@ github.com/awnumar/memcall v0.1.2 h1:7gOfDTL+BJ6nnbtAp9+HQzUFjtP1hEseRQq8eP055QY github.com/awnumar/memcall v0.1.2/go.mod h1:S911igBPR9CThzd/hYQQmTc9SWNu3ZHIlCGaWsWsoJo= github.com/awnumar/memguard v0.22.3 h1:b4sgUXtbUjhrGELPbuC62wU+BsPQy+8lkWed9Z+pj0Y= github.com/awnumar/memguard v0.22.3/go.mod h1:mmGunnffnLHlxE5rRgQc3j+uwPZ27eYb61ccr8Clz2Y= -github.com/aws/aws-sdk-go v1.44.190 h1:QC+Pf/Ooj7Waf2obOPZbIQOqr00hy4h54j3ZK9mvHcc= -github.com/aws/aws-sdk-go v1.44.190/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= -github.com/aws/aws-sdk-go v1.44.250 h1:IuGUO2Hafv/b0yYKI5UPLQShYDx50BCIQhab/H1sX2M= -github.com/aws/aws-sdk-go v1.44.250/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= github.com/aws/aws-sdk-go v1.44.265 h1:rlBuD8OYjM5Vfcf7jDa264oVHqlPqY7y7o+JmrjNFUc= github.com/aws/aws-sdk-go v1.44.265/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= github.com/aws/aws-sdk-go v1.46.7 h1:IjvAWeiJZlbETOemOwvheN5L17CvKvKW0T1xOC6d3Sc= @@ -14,8 +10,6 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/goburrow/cache v0.1.4 h1:As4KzO3hgmzPlnaMniZU9+VmoNYseUhuELbxy9mRBfw= -github.com/goburrow/cache v0.1.4/go.mod h1:cDFesZDnIlrHoNlMYqqMpCRawuXulgx+y7mXU8HZ+/c= github.com/godaddy/asherah/go/securememory v0.1.4 h1:1UlEPE5Q2wK1fbGwjIBtlGO02teLFBFk7dNIvdWOzNQ= github.com/godaddy/asherah/go/securememory v0.1.4/go.mod h1:grCFdMhT5CY8h+E+Qb1Abhd6uBDIxrwVh6Tulsc9gj4= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= From d5ba1a7da59a79e97ee80c422f75d16444926a32 Mon Sep 17 00:00:00 2001 From: Bo Thompson Date: Mon, 23 Oct 2023 10:40:10 -0700 Subject: [PATCH 14/20] Add support for synchronous evictions --- go/appencryption/key_cache.go | 41 ++++++---- go/appencryption/pkg/cache/cache.go | 85 ++++++++++++++++++--- go/appencryption/pkg/cache/cache_test.go | 95 +++++++++++++++++++----- 3 files changed, 178 insertions(+), 43 deletions(-) diff --git a/go/appencryption/key_cache.go b/go/appencryption/key_cache.go index f061ca80c..0a65184b2 100644 --- a/go/appencryption/key_cache.go +++ b/go/appencryption/key_cache.go @@ -30,6 +30,7 @@ func (c *cachedCryptoKey) Close() { return } + log.Debugf("closing cached key: %s, refs=%d", c.CryptoKey, c.refs) c.CryptoKey.Close() } @@ -115,11 +116,6 @@ const ( func newKeyCache(t cacheKeyType, policy *CryptoPolicy) (c *keyCache) { cacheMaxSize := DefaultKeyCacheMaxSize cachePolicy := "" - onEvict := func(key string, value cacheEntry) { - log.Debugf("%s eviction -- key: %s, id: %s\n", c, value.key, key) - - value.key.Close() - } switch t { case CacheTypeSystemKeys: @@ -130,21 +126,36 @@ func newKeyCache(t cacheKeyType, policy *CryptoPolicy) (c *keyCache) { cachePolicy = policy.IntermediateKeyCacheEvictionPolicy } + c = &keyCache{ + policy: policy, + latest: make(map[string]KeyMeta), + + cacheType: t, + } + + onEvict := func(key string, value cacheEntry) { + log.Debugf("[onEvict] closing key -- id: %s\n", key) + + value.key.Close() + } + cb := cache.New[string, cacheEntry](cacheMaxSize) if cachePolicy != "" { + log.Debugf("setting cache policy to %s", cachePolicy) + cb.WithPolicy(cache.CachePolicy(cachePolicy)) } - keys := cb.WithEvictFunc(onEvict).Build() + if cacheMaxSize < 100 { + log.Debugf("cache size is less than 100, setting synchronous eviction policy") - return &keyCache{ - policy: policy, - keys: keys, - latest: make(map[string]KeyMeta), - - cacheType: t, + cb.Synchronous() } + + c.keys = cb.WithEvictFunc(onEvict).Build() + + return c } // isReloadRequired returns true if the check interval has elapsed @@ -246,8 +257,6 @@ func (c *keyCache) read(meta KeyMeta) (cacheEntry, bool) { e, ok := c.keys.Get(id) if !ok { log.Debugf("%s miss -- id: %s\n", c, id) - } else { - log.Debugf("%s hit -- id: %s\n", c, id) } return e, ok @@ -337,12 +346,14 @@ func (c *keyCache) IsInvalid(key *internal.CryptoKey) bool { // It MUST be called after a session is complete to avoid // running into MEMLOCK limits. func (c *keyCache) Close() error { + log.Debugf("%s closing\n", c) + return c.keys.Close() } // String returns a string representation of this cache. func (c *keyCache) String() string { - return fmt.Sprintf("keyCache(%p)", c) + return fmt.Sprintf("keyCache(%p){type=%s,size=%d,cap=%d}", c, c.cacheType, c.keys.Len(), c.keys.Capacity()) } // Verify neverCache implements the cache interface. diff --git a/go/appencryption/pkg/cache/cache.go b/go/appencryption/pkg/cache/cache.go index d8f14c955..603d4381f 100644 --- a/go/appencryption/pkg/cache/cache.go +++ b/go/appencryption/pkg/cache/cache.go @@ -15,6 +15,8 @@ import ( "fmt" "sync" "time" + + "github.com/godaddy/asherah/go/appencryption/pkg/log" ) // Interface is intended to be a generic interface for cache implementations. @@ -119,6 +121,7 @@ type builder[K comparable, V any] struct { evictFunc EvictFunc[K, V] clock Clock expiry time.Duration + isSync bool } // New returns a new cache builder with the given capacity. Use the builder to @@ -136,6 +139,7 @@ func New[K comparable, V any](capacity int) *builder[K, V] { // WithEvictFunc sets the EvictFunc for the cache. func (b *builder[K, V]) WithEvictFunc(fn EvictFunc[K, V]) *builder[K, V] { b.evictFunc = fn + return b } @@ -180,31 +184,43 @@ func (b *builder[K, V]) TinyLFU() *builder[K, V] { // WithClock sets the Clock for the cache. func (b *builder[K, V]) WithClock(clock Clock) *builder[K, V] { b.clock = clock + return b } // WithExpiry sets the expiry for the cache. func (b *builder[K, V]) WithExpiry(expiry time.Duration) *builder[K, V] { b.expiry = expiry + + return b +} + +// Synchronous sets the cache to use a synchronous eviction process. By +// default, the cache uses a concurrent eviction process which executes the +// eviction callback in a separate goroutine. +// Use this option to ensure eviction is processed inline, prior to adding +// a new item to the cache. +func (b *builder[K, V]) Synchronous() *builder[K, V] { + b.isSync = true + return b } // Build creates the cache. func (b *builder[K, V]) Build() Interface[K, V] { c := &cache[K, V]{ - byKey: make(map[K]*cacheItem[K, V]), - events: make(chan cacheEvent[K, V], 100), + byKey: make(map[K]*cacheItem[K, V]), policy: b.policy, clock: b.clock, expiry: b.expiry, onEvictCallback: b.evictFunc, + isSync: b.isSync, } c.policy.init(b.capacity) - c.closeWG.Add(1) - go c.processEvents() + c.startup() return c } @@ -233,6 +249,10 @@ type cache[K comparable, V any] struct { // expiry is the duration after which an item is considered expired. Set to // a custom duration to use a custom expiry. The default is no expiry. expiry time.Duration + + // isSync is true if the cache uses a synchronized eviction process. The default + // is false, which uses a concurrent eviction process. + isSync bool } // processEvents processes events in a separate goroutine. @@ -242,8 +262,11 @@ func (c *cache[K, V]) processEvents() { for event := range c.events { switch event.event { case evictItem: + log.Debugf("%s executing evict callback for item: %v", c, event.item.key) c.onEvictCallback(event.item.key, event.item.value) case closeCache: + log.Debugf("%s closed, exiting event loop", c) + return } } @@ -266,18 +289,43 @@ func (c *cache[K, V]) Close() error { c.evict() } + c.shutdown() + + c.byKey = nil + + c.policy.close() + + return nil +} + +// startup starts the cache event processing goroutine. +func (c *cache[K, V]) startup() { + if c.isSync { + // no need to start the event processing goroutine + return + } + + c.events = make(chan cacheEvent[K, V]) + + c.closeWG.Add(1) + + go c.processEvents() +} + +// shutdown closes the events channel and waits for the event processing +// goroutine to exit. +func (c *cache[K, V]) shutdown() { + if c.isSync { + return + } + c.events <- cacheEvent[K, V]{event: closeCache} c.closeWG.Wait() close(c.events) - c.byKey = nil c.events = nil - - c.policy.close() - - return nil } // Len returns the number of items in the cache. @@ -318,7 +366,7 @@ func (c *cache[K, V]) Set(key K, value V) { return } - // if the cache is full, evict the least recently used item + // if the cache is full, evict an item if c.size == c.policy.capacity() { c.evict() } @@ -404,7 +452,8 @@ func (c *cache[K, V]) zeroValue() V { return v } -// evict removes an item from the cache and sends an evict event. +// evict removes an item from the cache and sends an evict event or, if the +// cache uses a synchronized eviction process, calls the evict callback. func (c *cache[K, V]) evict() { item := c.policy.victim() c.evictItem(item) @@ -418,5 +467,19 @@ func (c *cache[K, V]) evictItem(item *cacheItem[K, V]) { c.policy.remove(item) + if c.isSync { + log.Debugf("%s executing evict callback for item (synchronous): %v", c, item.key) + + c.onEvictCallback(item.key, item.value) + + return + } + + log.Debugf("%s sending evict event for item: %v", c, item.key) c.events <- cacheEvent[K, V]{event: evictItem, item: item} } + +// String returns a string representation of this cache. +func (c *cache[K, V]) String() string { + return fmt.Sprintf("cache[%T, %T](%p)", *new(K), *new(V), c) +} diff --git a/go/appencryption/pkg/cache/cache_test.go b/go/appencryption/pkg/cache/cache_test.go index d59796b5a..bc7702600 100644 --- a/go/appencryption/pkg/cache/cache_test.go +++ b/go/appencryption/pkg/cache/cache_test.go @@ -11,7 +11,6 @@ import ( type CacheSuite struct { suite.Suite - cache cache.Interface[int, string] clock *fakeClock expiry time.Duration } @@ -41,42 +40,52 @@ func (suite *CacheSuite) SetupTest() { } suite.expiry = time.Hour +} + +func (suite *CacheSuite) newCache() cache.Interface[int, string] { + cb := cache.New[int, string](2).WithClock(suite.clock).WithExpiry(suite.expiry) - suite.cache = cache.New[int, string](2).WithClock(suite.clock).WithExpiry(suite.expiry).Build() + return cb.Build() } -func (suite *CacheSuite) TestNew() { - suite.Assert().Equal(0, suite.cache.Len()) - suite.Assert().Equal(2, suite.cache.Capacity()) +func (suite *CacheSuite) TestBuild() { + c := suite.newCache() + + suite.Assert().Equal(0, c.Len()) + suite.Assert().Equal(2, c.Capacity()) } func (suite *CacheSuite) TestClosing() { - suite.Assert().NoError(suite.cache.Close()) + c := suite.newCache() + + suite.Assert().NoError(c.Close()) // set/get do nothing after closing - suite.cache.Set(1, "one") - suite.Assert().Equal(0, suite.cache.Len()) + c.Set(1, "one") + suite.Assert().Equal(0, c.Len()) // getting a value does nothing, returns false - _, ok := suite.cache.Get(1) + _, ok := c.Get(1) suite.Assert().False(ok) // delete does nothing - suite.Assert().False(suite.cache.Delete(1)) + suite.Assert().False(c.Delete(1)) // closing again does nothing - suite.Assert().NoError(suite.cache.Close()) + suite.Assert().NoError(c.Close()) } func (suite *CacheSuite) TestExpiry() { - suite.cache.Set(1, "one") - suite.cache.Set(2, "two") + c := suite.newCache() - one, ok := suite.cache.Get(1) + c.Set(1, "one") + c.Set(2, "two") + + one, ok := c.Get(1) suite.Assert().Equal("one", one) suite.Assert().True(ok) - two, ok := suite.cache.Get(2) + two, ok := c.Get(2) suite.Assert().Equal("two", two) suite.Assert().True(ok) @@ -84,9 +93,61 @@ func (suite *CacheSuite) TestExpiry() { suite.clock.SetNow(suite.clock.Now().Add(suite.expiry + time.Second)) // get should return false - _, ok = suite.cache.Get(1) + _, ok = c.Get(1) + suite.Assert().False(ok) + + _, ok = c.Get(2) + suite.Assert().False(ok) +} + +func (suite *CacheSuite) TestSynchronousEviction() { + evicted := false + + cb := cache.New[int, string](2).Synchronous() + cb.WithEvictFunc(func(key int, value string) { + suite.Assert().Equal(1, key) + suite.Assert().Equal("one", value) + + evicted = true + }) + + c := cb.Build() + + c.Set(1, "one") + c.Set(2, "two") + c.Set(3, "three") + + suite.Assert().True(evicted) + + // 1 should be evicted + _, ok := c.Get(1) suite.Assert().False(ok) - _, ok = suite.cache.Get(2) + _, ok = c.Get(2) + suite.Assert().True(ok) + + // 3 should still be there + three, ok := c.Get(3) + suite.Assert().Equal("three", three) + suite.Assert().True(ok) +} + +func (suite *CacheSuite) TestSynchronousClosing() { + c := cache.New[int, string](2).Synchronous().Build() + + suite.Assert().NoError(c.Close()) + + // set/get do nothing after closing + c.Set(1, "one") + suite.Assert().Equal(0, c.Len()) + + // getting a value does nothing, returns false + _, ok := c.Get(1) suite.Assert().False(ok) + + // delete does nothing + suite.Assert().False(c.Delete(1)) + + // closing again does nothing + suite.Assert().NoError(c.Close()) } From ec3aa9608d624a0cff4be00dc856a283de288bde Mon Sep 17 00:00:00 2001 From: Bo Thompson Date: Mon, 23 Oct 2023 10:41:52 -0700 Subject: [PATCH 15/20] additional debug logging --- go/appencryption/envelope.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/go/appencryption/envelope.go b/go/appencryption/envelope.go index f5835abbd..8b01a4569 100644 --- a/go/appencryption/envelope.go +++ b/go/appencryption/envelope.go @@ -10,6 +10,7 @@ import ( "github.com/rcrowley/go-metrics" "github.com/godaddy/asherah/go/appencryption/internal" + "github.com/godaddy/asherah/go/appencryption/pkg/log" ) // MetricsPrefix prefixes all metrics names @@ -377,12 +378,14 @@ func (e *envelopeEncryption) EncryptPayload(ctx context.Context, data []byte) (* defer encryptTimer.UpdateSince(time.Now()) loader := func(meta KeyMeta) (*internal.CryptoKey, error) { + log.Debugf("[EncryptPayload] loadLatestOrCreateIntermediateKey: %s", meta.ID) return e.loadLatestOrCreateIntermediateKey(ctx, meta.ID) } // Try to get latest from cache. ik, err := e.ikCache.GetOrLoadLatest(e.partition.IntermediateKeyID(), loader) if err != nil { + log.Debugf("[EncryptPayload] GetOrLoadLatest failed: %s", err.Error()) return nil, err } @@ -392,6 +395,7 @@ func (e *envelopeEncryption) EncryptPayload(ctx context.Context, data []byte) (* // to prevent excessive IK/SK creation (we always create new DRK on each write, so not a concern there) drk, err := internal.GenerateKey(e.SecretFactory, time.Now().Unix(), AES256KeySize) if err != nil { + log.Debugf("[EncryptPayload] GenerateKey failed: %s", err.Error()) return nil, err } @@ -401,6 +405,7 @@ func (e *envelopeEncryption) EncryptPayload(ctx context.Context, data []byte) (* return e.Crypto.Encrypt(data, bytes) }) if err != nil { + log.Debugf("[EncryptPayload] WithKeyFunc failed to encrypt data using DRK: %s", err.Error()) return nil, err } @@ -410,6 +415,7 @@ func (e *envelopeEncryption) EncryptPayload(ctx context.Context, data []byte) (* }) }) if err != nil { + log.Debugf("[EncryptPayload] WithKeyFunc failed to encrypt DRK using IK: %s", err.Error()) return nil, err } @@ -444,11 +450,15 @@ func (e *envelopeEncryption) DecryptDataRowRecord(ctx context.Context, drr DataR } loader := func(meta KeyMeta) (*internal.CryptoKey, error) { + log.Debugf("[DecryptDataRowRecord] loadIntermediateKey: %s", meta.ID) + return e.loadIntermediateKey(ctx, meta) } ik, err := e.ikCache.GetOrLoad(*drr.Key.ParentKeyMeta, loader) if err != nil { + log.Debugf("[DecryptDataRowRecord] GetOrLoad IK failed: %s", err.Error()) + return nil, err } From 0aa395f5a6ecbe738c87be1d27cbc6de5d73c363 Mon Sep 17 00:00:00 2001 From: Bo Thompson Date: Mon, 23 Oct 2023 10:48:30 -0700 Subject: [PATCH 16/20] support additional cache configurations --- go/appencryption/cmd/example/main.go | 83 ++++++++++++++++++++++++---- go/appencryption/go.work.sum | 40 -------------- go/appencryption/policy.go | 7 ++- go/appencryption/policy_test.go | 6 +- 4 files changed, 79 insertions(+), 57 deletions(-) diff --git a/go/appencryption/cmd/example/main.go b/go/appencryption/cmd/example/main.go index 075dc99d5..026f3eeec 100644 --- a/go/appencryption/cmd/example/main.go +++ b/go/appencryption/cmd/example/main.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "log" + "math/rand" "net/http" _ "net/http/pprof" "os" @@ -63,6 +64,13 @@ type Options struct { CheckInterval time.Duration `long:"check" description:"Interval to check for expired keys"` ConnectionString string `short:"C" long:"conn" description:"MySQL Connection String"` NoExit bool `short:"x" long:"no-exit" description:"Prevent app from closing once tests are completed. Especially useful for profiling."` + + SessionCacheSize int `long:"session-cache-size" description:"Number of sessions to cache in the shared session cache."` + SessionCacheExpiry time.Duration `long:"session-cache-expiry" description:"Duration after which a session is evicted from the shared session cache."` + EnableSharedIKCache bool `long:"enable-shared-ik-cache" description:"Enables the shared IK cache."` + IKCacheSize int `long:"ik-cache-size" description:"Number of IKs to cache in the IK cache."` + SKCacheSize int `long:"sk-cache-size" description:"Number of SKs to cache in the SK cache."` + RandomizePartition bool `long:"randomize-partition" description:"Randomize the partition ID for each session using a Zipfian distribution."` } var ( @@ -232,17 +240,37 @@ func main() { return func(*appencryption.CryptoPolicy) { /* noop */ } } + policy := appencryption.NewCryptoPolicy( + appencryption.WithExpireAfterDuration(expireAfter), + appencryption.WithRevokeCheckInterval(checkInterval), + withCacheOption(), + withSessionCacheOption(), + ) + + if opts.SessionCacheSize > 0 { + policy.SessionCacheMaxSize = opts.SessionCacheSize + } + + if opts.SessionCacheExpiry > 0 { + policy.SessionCacheDuration = opts.SessionCacheExpiry + } + + if opts.IKCacheSize > 0 { + policy.IntermediateKeyCacheMaxSize = opts.IKCacheSize + } + + if opts.SKCacheSize > 0 { + policy.SystemKeyCacheMaxSize = opts.SKCacheSize + } + + policy.SharedIntermediateKeyCache = opts.EnableSharedIKCache + keyManager := CreateKMS() conf := &appencryption.Config{ Service: "exampleService", Product: "productId", - Policy: appencryption.NewCryptoPolicy( - appencryption.WithExpireAfterDuration(expireAfter), - appencryption.WithRevokeCheckInterval(checkInterval), - withCacheOption(), - withSessionCacheOption(), - ), + Policy: policy, } secrets := new(memguard.SecretFactory) @@ -293,9 +321,26 @@ func main() { }() } + var partitioner func() int + if opts.RandomizePartition { + r := rand.New(rand.NewSource(1)) + zipf := rand.NewZipf(r, 1.01, 1.0, 1<<16-1) + + partitioner = func() int { + return int(zipf.Uint64()) + } + } + for i := 0; i < opts.Iterations; i++ { - log.Println("Run iteration:", i) - RunSessionIteration(time.Now(), factory) + if opts.Verbose { + log.Printf( + "[run iteration %d] secrets: allocs=%d, inuse=%d\n", + i, + securememory.AllocCounter.Count(), + securememory.InUseCounter.Count()) + } + + RunSessionIteration(time.Now(), factory, partitioner) } done <- true @@ -364,14 +409,30 @@ func main() { } } -func RunSessionIteration(start time.Time, factory *appencryption.SessionFactory) { +func RunSessionIteration(start time.Time, factory *appencryption.SessionFactory, partitioner func() int) { var wg sync.WaitGroup for i := 0; i < opts.Sessions; i++ { wg.Add(1) + partitionID := i + + if partitioner != nil { + partitionID = partitioner() + } + go func(i int) { - defer wg.Done() + defer func() { + if r := recover(); r != nil { + log.Printf( + "[panic] secrets: allocs=%d, inuse=%d\n", + securememory.AllocCounter.Count(), + securememory.InUseCounter.Count()) + panic(r) + } + + wg.Done() + }() runFunc := func(shopper string) { session, err := factory.GetSession(shopper) @@ -407,7 +468,7 @@ func RunSessionIteration(start time.Time, factory *appencryption.SessionFactory) runFunc(shopper) } - }(i) + }(partitionID) } wg.Wait() diff --git a/go/appencryption/go.work.sum b/go/appencryption/go.work.sum index 7d4baf243..560dcbb5c 100644 --- a/go/appencryption/go.work.sum +++ b/go/appencryption/go.work.sum @@ -384,47 +384,7 @@ golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDA golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 h1:nt+Q6cXKz4MosCSpnbMtqiQ8Oz0pxTef2B4Vca2lvfk= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.4.0 h1:O7UWfv5+A2qiuulQk30kVinPoMtoIPeVaKLEgLpVkvg= -golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= -golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= -google.golang.org/api v0.22.0 h1:J1Pl9P2lnmYFSJvgs70DKELqHNh8CNWXPbud4njEE2s= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8 h1:Cpp2P6TPjujNoC5M2KHY6g7wfyLYfIWRZaSdIKfDasA= -gopkg.in/airbrake/gobrake.v2 v2.0.9 h1:7z2uVWwn7oVeeugY1DtlPAy5H+KYgB1KeKTnqjNatLo= -gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/cheggaaa/pb.v1 v1.0.25 h1:Ev7yu1/f6+d+b3pi5vPdRPc6nNtP1umSfcWiEfRqv6I= -gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8= -gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= -gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 h1:OAj3g0cR6Dx/R07QgQe8wkA9RNjB2u4i700xBkIT4e0= -gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= -gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= -gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI= -gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gotest.tools/gotestsum v1.8.2 h1:szU3TaSz8wMx/uG+w/A2+4JUPwH903YYaMI9yOOYAyI= -honnef.co/go/tools v0.0.1-2020.1.3 h1:sXmLre5bzIR6ypkjXCDI3jHPssRhc8KD/Ome589sc3U= -k8s.io/api v0.22.5 h1:xk7C+rMjF/EGELiD560jdmwzrB788mfcHiNbMQLIVI8= -k8s.io/apimachinery v0.22.5 h1:cIPwldOYm1Slq9VLBRPtEYpyhjIm1C6aAMAoENuvN9s= -k8s.io/apiserver v0.22.5 h1:71krQxCUz218ecb+nPhfDsNB6QgP1/4EMvi1a2uYBlg= -k8s.io/client-go v0.22.5 h1:I8Zn/UqIdi2r02aZmhaJ1hqMxcpfJ3t5VqvHtctHYFo= -k8s.io/code-generator v0.19.7 h1:kM/68Y26Z/u//TFc1ggVVcg62te8A2yQh57jBfD0FWQ= -k8s.io/component-base v0.22.5 h1:U0eHqZm7mAFE42hFwYhY6ze/MmVaW00JpMrzVsQmzYE= -k8s.io/cri-api v0.25.0 h1:INwdXsCDSA/0hGNdPxdE2dQD6ft/5K1EaKXZixvSQxg= -k8s.io/gengo v0.0.0-20201113003025-83324d819ded h1:JApXBKYyB7l9xx+DK7/+mFjC7A9Bt5A93FPvFD0HIFE= -k8s.io/klog/v2 v2.30.0 h1:bUO6drIvCIsvZ/XFgfxoGFQU/a4Qkh0iAlvUR7vlHJw= -k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd h1:sOHNzJIkytDF6qadMNKhhDRpc6ODik8lVC6nOur7B2c= -k8s.io/kubernetes v1.13.0 h1:qTfB+u5M92k2fCCCVP2iuhgwwSOv1EkAkvQY1tQODD8= -k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b h1:wxEMGetGMur3J1xuGLQY7GEQYg9bZxKn3tKo5k/eYcs= -lukechampine.com/frand v1.4.2 h1:RzFIpOvkMXuPMBb9maa4ND4wjBn71E1Jpf8BzJHMaVw= -rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE= -rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY= -rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4= -sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.15 h1:4uqm9Mv+w2MmBYD+F4qf/v6tDFUdPOk29C095RbU5mY= -sigs.k8s.io/structured-merge-diff/v4 v4.1.2 h1:Hr/htKFmJEbtMgS/UD0N+gtgctAqz81t3nu+sPzynno= -sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= diff --git a/go/appencryption/policy.go b/go/appencryption/policy.go index ffe7e963a..420c27430 100644 --- a/go/appencryption/policy.go +++ b/go/appencryption/policy.go @@ -85,11 +85,12 @@ func WithNoCache() PolicyOption { } } -// WithSharedKeyCache enables a shared cache for both System and Intermediate Keys with the provided capacity. -func WithSharedKeyCache(capacity int) PolicyOption { +// WithSharedIntermediateKeyCache enables a shared cache for Intermediate Keys with the provided capacity. The shared +// cache will be used by all sessions for a given factory. +func WithSharedIntermediateKeyCache(capacity int) PolicyOption { return func(policy *CryptoPolicy) { policy.SharedIntermediateKeyCache = true - policy.SystemKeyCacheMaxSize = capacity + policy.IntermediateKeyCacheMaxSize = capacity } } diff --git a/go/appencryption/policy_test.go b/go/appencryption/policy_test.go index f269b9a21..3c788c316 100644 --- a/go/appencryption/policy_test.go +++ b/go/appencryption/policy_test.go @@ -49,7 +49,7 @@ func Test_NewCryptoPolicy_WithOptions(t *testing.T) { assert.Equal(t, sessionCacheDuration, policy.SessionCacheDuration) } -func Test_NewCryptoPolicy_WithOptions_SharedKeyCache(t *testing.T) { +func Test_NewCryptoPolicy_WithOptions_SharedIntermediateKeyCache(t *testing.T) { revokeCheckInterval := time.Second * 156 expireAfterDuration := time.Second * 100 keyCacheMaxSize := 10 @@ -59,7 +59,7 @@ func Test_NewCryptoPolicy_WithOptions_SharedKeyCache(t *testing.T) { policy := NewCryptoPolicy( WithRevokeCheckInterval(revokeCheckInterval), WithExpireAfterDuration(expireAfterDuration), - WithSharedKeyCache(keyCacheMaxSize), + WithSharedIntermediateKeyCache(keyCacheMaxSize), WithSessionCache(), WithSessionCacheMaxSize(sessionCacheMaxSize), WithSessionCacheDuration(sessionCacheDuration), @@ -70,7 +70,7 @@ func Test_NewCryptoPolicy_WithOptions_SharedKeyCache(t *testing.T) { assert.True(t, policy.CacheSystemKeys) assert.True(t, policy.CacheIntermediateKeys) assert.True(t, policy.SharedIntermediateKeyCache) - assert.Equal(t, keyCacheMaxSize, policy.SystemKeyCacheMaxSize) + assert.Equal(t, keyCacheMaxSize, policy.IntermediateKeyCacheMaxSize) assert.True(t, policy.CacheSessions) assert.Equal(t, sessionCacheMaxSize, policy.SessionCacheMaxSize) assert.Equal(t, sessionCacheDuration, policy.SessionCacheDuration) From b5876ce0cb878bd3977095b7590f02538e5072e0 Mon Sep 17 00:00:00 2001 From: Bo Thompson Date: Wed, 25 Oct 2023 11:18:54 -0700 Subject: [PATCH 17/20] bump version --- go/appencryption/.versionfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go/appencryption/.versionfile b/go/appencryption/.versionfile index 9e11b32fc..1d0ba9ea1 100644 --- a/go/appencryption/.versionfile +++ b/go/appencryption/.versionfile @@ -1 +1 @@ -0.3.1 +0.4.0 From 9bbc3c1b72fdcbc8db5f1006c95b43c87ddff5d1 Mon Sep 17 00:00:00 2001 From: Bo Thompson Date: Wed, 25 Oct 2023 11:31:47 -0700 Subject: [PATCH 18/20] fix typo --- go/appencryption/pkg/cache/cache.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go/appencryption/pkg/cache/cache.go b/go/appencryption/pkg/cache/cache.go index 603d4381f..825b06442 100644 --- a/go/appencryption/pkg/cache/cache.go +++ b/go/appencryption/pkg/cache/cache.go @@ -92,7 +92,7 @@ type policy[K comparable, V any] interface { // close removes all items from the cache, sends a close event to the event // processing goroutine, and waits for it to exit. close() - // admit is called when an item is admit to the cache. + // admit is called when an item is admitted to the cache. admit(item *cacheItem[K, V]) // access is called when an item is access. access(item *cacheItem[K, V]) From 181e267d0bda340213da883cbcda9308bc5cde55 Mon Sep 17 00:00:00 2001 From: Bo Thompson Date: Wed, 25 Oct 2023 11:41:45 -0700 Subject: [PATCH 19/20] fix typos --- go/appencryption/pkg/cache/cache.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/go/appencryption/pkg/cache/cache.go b/go/appencryption/pkg/cache/cache.go index 825b06442..77724406a 100644 --- a/go/appencryption/pkg/cache/cache.go +++ b/go/appencryption/pkg/cache/cache.go @@ -94,11 +94,11 @@ type policy[K comparable, V any] interface { close() // admit is called when an item is admitted to the cache. admit(item *cacheItem[K, V]) - // access is called when an item is access. + // access is called when an item is accessed. access(item *cacheItem[K, V]) // victim returns the victim item to be evicted. victim() *cacheItem[K, V] - // remove is called when an item is remove from the cache. + // remove is called when an item is removed from the cache. remove(item *cacheItem[K, V]) } From cad74e6140dbf2e605135ff5127a3e5bf3ab0320 Mon Sep 17 00:00:00 2001 From: Bo Thompson Date: Tue, 31 Oct 2023 10:05:16 -0700 Subject: [PATCH 20/20] removed unused constant --- go/appencryption/pkg/cache/cache.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/go/appencryption/pkg/cache/cache.go b/go/appencryption/pkg/cache/cache.go index 77724406a..0de3f158e 100644 --- a/go/appencryption/pkg/cache/cache.go +++ b/go/appencryption/pkg/cache/cache.go @@ -42,8 +42,6 @@ const ( SLRU CachePolicy = "slru" // TinyLFU is the tiny least frequently used cache policy. TinyLFU CachePolicy = "tinylfu" - // DefaultCachePolicy is the default cache policy. - DefaultCachePolicy = LRU ) // String returns the string representation of the eviction policy.