-
Notifications
You must be signed in to change notification settings - Fork 90
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
075bc8f
commit 5698daa
Showing
19 changed files
with
315 additions
and
57 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
package error_page | ||
|
||
import ( | ||
"bytes" | ||
"crypto/md5" //nolint:gosec | ||
"encoding/gob" | ||
"sync" | ||
"time" | ||
|
||
"gh.tarampamp.am/error-pages/internal/template" | ||
) | ||
|
||
type ( | ||
// RenderedCache is a cache for rendered error pages. It's safe for concurrent use. | ||
// It uses a hash of the template and props as a key. | ||
// | ||
// To remove expired items, call ClearExpired method periodically (a bit more often than the ttl). | ||
RenderedCache struct { | ||
ttl time.Duration | ||
|
||
mu sync.RWMutex | ||
items map[[32]byte]cacheItem // map[template_hash[0:15];props_hash[16:32]]cache_item | ||
} | ||
|
||
cacheItem struct { | ||
content []byte | ||
addedAtNano int64 | ||
} | ||
) | ||
|
||
// NewRenderedCache creates a new RenderedCache with the specified ttl. | ||
func NewRenderedCache(ttl time.Duration) *RenderedCache { | ||
return &RenderedCache{ttl: ttl, items: make(map[[32]byte]cacheItem)} | ||
} | ||
|
||
// genKey generates a key for the cache item by hashing the template and props. | ||
func (rc *RenderedCache) genKey(template string, props template.Props) [32]byte { | ||
var ( | ||
key [32]byte | ||
th, ph = hash(template), hash(props) // template hash, props hash | ||
) | ||
|
||
copy(key[:16], th[:]) // first 16 bytes for the template hash | ||
copy(key[16:], ph[:]) // last 16 bytes for the props hash | ||
|
||
return key | ||
} | ||
|
||
// Has checks if the cache has an item with the specified template and props. | ||
func (rc *RenderedCache) Has(template string, props template.Props) bool { | ||
var key = rc.genKey(template, props) | ||
|
||
rc.mu.RLock() | ||
_, ok := rc.items[key] | ||
rc.mu.RUnlock() | ||
|
||
return ok | ||
} | ||
|
||
// Put adds a new item to the cache with the specified template, props, and content. | ||
func (rc *RenderedCache) Put(template string, props template.Props, content []byte) { | ||
var key = rc.genKey(template, props) | ||
|
||
rc.mu.Lock() | ||
rc.items[key] = cacheItem{content: content, addedAtNano: time.Now().UnixNano()} | ||
rc.mu.Unlock() | ||
} | ||
|
||
// Get returns the content of the item with the specified template and props. | ||
func (rc *RenderedCache) Get(template string, props template.Props) ([]byte, bool) { | ||
var key = rc.genKey(template, props) | ||
|
||
rc.mu.RLock() | ||
item, ok := rc.items[key] | ||
rc.mu.RUnlock() | ||
|
||
return item.content, ok | ||
} | ||
|
||
// ClearExpired removes all expired items from the cache. | ||
func (rc *RenderedCache) ClearExpired() { | ||
rc.mu.Lock() | ||
|
||
var now = time.Now().UnixNano() | ||
|
||
for key, item := range rc.items { | ||
if now-item.addedAtNano > rc.ttl.Nanoseconds() { | ||
delete(rc.items, key) | ||
} | ||
} | ||
|
||
rc.mu.Unlock() | ||
} | ||
|
||
// Clear removes all items from the cache. | ||
func (rc *RenderedCache) Clear() { | ||
rc.mu.Lock() | ||
clear(rc.items) | ||
rc.mu.Unlock() | ||
} | ||
|
||
// hash returns an MD5 hash of the provided value (it may be any built-in type). | ||
func hash(in any) [16]byte { | ||
var b bytes.Buffer | ||
|
||
if err := gob.NewEncoder(&b).Encode(in); err != nil { | ||
return [16]byte{} // never happens because we encode only built-in types | ||
} | ||
|
||
return md5.Sum(b.Bytes()) //nolint:gosec | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
package error_page_test | ||
|
||
import ( | ||
"strconv" | ||
"sync" | ||
"testing" | ||
"time" | ||
|
||
"github.com/stretchr/testify/assert" | ||
|
||
"gh.tarampamp.am/error-pages/internal/http/handlers/error_page" | ||
"gh.tarampamp.am/error-pages/internal/template" | ||
) | ||
|
||
func TestRenderedCache_CRUD(t *testing.T) { | ||
t.Parallel() | ||
|
||
var cache = error_page.NewRenderedCache(time.Millisecond) | ||
|
||
t.Run("has", func(t *testing.T) { | ||
assert.False(t, cache.Has("template", template.Props{})) | ||
cache.Put("template", template.Props{}, []byte("content")) | ||
assert.True(t, cache.Has("template", template.Props{})) | ||
|
||
assert.False(t, cache.Has("template", template.Props{Code: 1})) | ||
assert.False(t, cache.Has("foo", template.Props{Code: 1})) | ||
}) | ||
|
||
t.Run("exists", func(t *testing.T) { | ||
var got, ok = cache.Get("template", template.Props{}) | ||
|
||
assert.True(t, ok) | ||
assert.Equal(t, []byte("content"), got) | ||
|
||
cache.Clear() | ||
|
||
assert.False(t, cache.Has("template", template.Props{})) | ||
}) | ||
|
||
t.Run("not exists", func(t *testing.T) { | ||
var got, ok = cache.Get("template", template.Props{Code: 2}) | ||
|
||
assert.False(t, ok) | ||
assert.Nil(t, got) | ||
}) | ||
|
||
t.Run("race condition provocation", func(t *testing.T) { | ||
var wg sync.WaitGroup | ||
|
||
for i := 0; i < 100; i++ { | ||
wg.Add(2) | ||
|
||
go func(i int) { | ||
defer wg.Done() | ||
|
||
cache.Get("template", template.Props{}) | ||
cache.Put("template"+strconv.Itoa(i), template.Props{}, []byte("content")) | ||
cache.Has("template", template.Props{}) | ||
}(i) | ||
|
||
go func() { | ||
defer wg.Done() | ||
|
||
cache.ClearExpired() | ||
}() | ||
} | ||
|
||
wg.Wait() | ||
}) | ||
} | ||
|
||
func TestRenderedCache_Expiring(t *testing.T) { | ||
t.Parallel() | ||
|
||
var cache = error_page.NewRenderedCache(10 * time.Millisecond) | ||
|
||
cache.Put("template", template.Props{}, []byte("content")) | ||
cache.ClearExpired() | ||
assert.True(t, cache.Has("template", template.Props{})) | ||
|
||
<-time.After(10 * time.Millisecond) | ||
|
||
assert.True(t, cache.Has("template", template.Props{})) // expired, but not cleared yet | ||
cache.ClearExpired() | ||
assert.False(t, cache.Has("template", template.Props{})) // cleared | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.