Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
eb4731b
Add Cache-Control header
toshinari123 Jan 2, 2024
8bdb787
Fix set Cache-Control header position
toshinari123 Jan 9, 2024
bb7860b
Remove pageship in .gitignore
toshinari123 Jan 9, 2024
2a0ce74
Add default Cache-Control header
kiootic Jan 9, 2024
8826885
model type ContentCache
toshinari123 Jan 10, 2024
1a7f50a
model type ContentCache
toshinari123 Jan 10, 2024
c2adf48
model type ContentCache (real)
toshinari123 Jan 10, 2024
7ca8d3e
corrected type ContentCache
toshinari123 Jan 10, 2024
28f1f86
write func NewContentCache
toshinari123 Jan 10, 2024
2222d24
write func getContent
toshinari123 Jan 10, 2024
8b415ef
Merge branch 'main' of github.com:oursky/pageship into serve-cache-co…
toshinari123 Jan 23, 2024
c04f09c
Add OnExit in Ristretto to remove mutex entry
toshinari123 Jan 23, 2024
738b81c
Use io.Reader in contentcache GetContent
toshinari123 Jan 23, 2024
b945f21
Add ContentCache to serveFile
toshinari123 Jan 23, 2024
514cd2b
Refactor duplicated logic
toshinari123 Jan 24, 2024
1f501a5
Fix contentcache implementation
toshinari123 Jan 24, 2024
47ff960
Create basic contentcache test
toshinari123 Jan 24, 2024
b2c4013
Merge PRs
toshinari123 Jan 29, 2024
78133f8
Fix basic test
toshinari123 Jan 29, 2024
fe49d41
Switch Read to ReadAll
toshinari123 Jan 29, 2024
9581e8a
Test data race and Use sync.Map
toshinari123 Jan 29, 2024
845e9cc
Upgrade contentCache implementation
toshinari123 Feb 5, 2024
85e9e7a
Add keyToString as Ristretto doesnt support complex keys
toshinari123 Feb 5, 2024
9c1086c
Test contentcache
toshinari123 Feb 5, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
*.test
*.out
go.work

*.local
.env
dist/
/book
/book
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,16 @@ require (
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.15.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.21.1 // indirect
github.com/aws/smithy-go v1.14.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/chzyer/readline v1.5.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgraph-io/ristretto v0.1.1 // indirect
github.com/foxcpp/go-mockdns v1.0.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang/glog v1.1.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-cmp v0.5.9 // indirect
Expand Down Expand Up @@ -111,6 +114,7 @@ require (
github.com/miekg/dns v1.1.50 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.2.0 // indirect
Expand Down
9 changes: 9 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -256,9 +256,12 @@ github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA
github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/certifi/gocertifi v0.0.0-20191021191039-0944d244cd40/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054/go.mod h1:sGbDF6GwGcLpkNXPUTkMRoywsNa/ol15pxFe6ERfguA=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw=
github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M=
github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E=
Expand Down Expand Up @@ -429,8 +432,11 @@ 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/denisenkom/go-mssqldb v0.10.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0=
github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8=
github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA=
github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/dhui/dktest v0.3.10 h1:0frpeeoM9pHouHjhLeZDuDTJ0PqjDTrycaHaMmkJAo8=
github.com/dhui/dktest v0.3.10/go.mod h1:h5Enh0nG3Qbo9WjNFRrwmKUaePEBhXMOygbz3Ww7Sz0=
Expand Down Expand Up @@ -612,6 +618,8 @@ github.com/golang-migrate/migrate/v4 v4.15.2/go.mod h1:f2toGLkYqD3JH+Todi4aZ2Zdb
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE=
github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ=
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
Expand Down Expand Up @@ -1697,6 +1705,7 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
Expand Down
158 changes: 158 additions & 0 deletions internal/cache/contentcache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package cache

import (
"fmt"
"sync"

"github.com/dgraph-io/ristretto"
)

type ContentCache[K any, V any, R any] struct {
m *mm
size int64
cache *ristretto.Cache
load func(r R) (V, int64, error)
}

func NewContentCache[K any, V any, R any](contentCacheSize int64, metrics bool, load func(r R) (V, int64, error)) (*ContentCache[K, V, R], error) {
m := New()
size := contentCacheSize
nc := size / 1000
if (nc < 100) { //mainly for testing
nc = 100
}
cache, err := ristretto.NewCache(&ristretto.Config{
//NumCounters is 10 times estimated max number of items in cache, as suggested in https://pkg.go.dev/github.com/dgraph-io/ristretto@v0.1.1#Config
NumCounters: nc, //limit / 10 KB small files * 10
MaxCost: size,
BufferItems: 64,
Metrics: metrics,
IgnoreInternalCost: true,
})
if err != nil {
return nil, err
}

return &ContentCache[K, V, R]{m: m, size: size, cache: cache, load: load}, nil
}

func (c *ContentCache[K, V, R]) keyToString(key K) string {
return fmt.Sprintf("%v", key)
}

func (c *ContentCache[K, V, R]) GetContent(key K) (V, bool) {
l := c.m.RLock(key)
defer l.RUnlock()

v, b := c.cache.Get(c.keyToString(key))
if v == nil {
var nv V
return nv, b
}
return v.(V), b
}

func (c *ContentCache[K, V, R]) SetContent(key K, r R) (V, error) {
l := c.m.Lock(key)
defer l.Unlock()

b, n, err := c.load(r)
if err != nil {
return b, err
}

c.cache.Set(c.keyToString(key), b, n)
return b, nil
}

//modified from https://stackoverflow.com/questions/40931373/how-to-gc-a-map-of-mutexes-in-go
type mm struct {
ml sync.Mutex
ma map[interface{}]*mentry
}

type mentry struct {
m *mm
el sync.RWMutex
cnt int
key interface{}
}

type Unlocker interface {
Unlock()
}

type RUnlocker interface {
RUnlock()
}

func New() *mm {
return &mm{ma: make(map[interface{}]*mentry)}
}

func (m *mm) Lock(key interface{}) Unlocker {
m.ml.Lock()
e, ok := m.ma[key]
if !ok {
e = &mentry{m: m, key: key}
m.ma[key] = e
}
e.cnt++
m.ml.Unlock()

e.el.Lock()

return e
}

func (me *mentry) Unlock() {
m := me.m

m.ml.Lock()
e, ok := m.ma[me.key]
if !ok {
m.ml.Unlock()
panic(fmt.Errorf("Unlock requested for key=%v but no entry found", me.key))
}
e.cnt--
if e.cnt < 1 {
delete(m.ma, me.key)
}
m.ml.Unlock()

e.el.Unlock()
}


func (m *mm) RLock(key interface{}) RUnlocker {
m.ml.Lock()
e, ok := m.ma[key]
if !ok {
e = &mentry{m: m, key: key}
m.ma[key] = e
}
e.cnt++
m.ml.Unlock()

e.el.RLock()

return e
}

func (me *mentry) RUnlock() {
m := me.m

m.ml.Lock()
e, ok := m.ma[me.key]
if !ok {
m.ml.Unlock()
panic(fmt.Errorf("Unlock requested for key=%v but no entry found", me.key))
}
e.cnt--
if e.cnt < 1 {
delete(m.ma, me.key)
}
m.ml.Unlock()

e.el.RUnlock()
}
153 changes: 153 additions & 0 deletions internal/cache/contentcache_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package cache //white box testing

import (
"bytes"
"testing"
"sync"
"time"
"fmt"
"io"

"github.com/stretchr/testify/assert"
)

type CacheKeyMock string

func TestGetContent(t *testing.T) {
fmt.Println("testing GetContent...")
load := func(r io.ReadSeeker) (*bytes.Buffer, int64, error) {
b, err := io.ReadAll(r)
nb := bytes.NewBuffer(b)
if err != nil {
return nb, 0, err
}
return nb, int64(nb.Len()), nil
}
cc, err := NewContentCache[CacheKeyMock](8, true, load)
assert.Empty(t, err)

k1 := CacheKeyMock("id1")
k2 := CacheKeyMock("id2")
k3 := CacheKeyMock("id3")

ccc, found := cc.GetContent(k1)
assert.Equal(t, false, found)
assert.Equal(t, "hit: 0 miss: 1 keys-added: 0 keys-updated: 0 keys-evicted: 0 cost-added: 0 cost-evicted: 0 sets-dropped: 0 sets-rejected: 0 gets-dropped: 0 gets-kept: 0 gets-total: 1 hit-ratio: 0.00", cc.cache.Metrics.String())

ccc, err = cc.SetContent(k1, bytes.NewReader([]byte("a")))
time.Sleep(100 * time.Millisecond) //https://github.com/dgraph-io/ristretto/issues/161
assert.Empty(t, err)
assert.Equal(t, bytes.NewBuffer([]byte("a")), ccc)
assert.Equal(t, "hit: 0 miss: 1 keys-added: 1 keys-updated: 0 keys-evicted: 0 cost-added: 1 cost-evicted: 0 sets-dropped: 0 sets-rejected: 0 gets-dropped: 0 gets-kept: 0 gets-total: 1 hit-ratio: 0.00", cc.cache.Metrics.String())

ccc, found = cc.GetContent(k1)
assert.Equal(t, true, found)
assert.Equal(t, bytes.NewBuffer([]byte("a")), ccc)
assert.Equal(t, "hit: 1 miss: 1 keys-added: 1 keys-updated: 0 keys-evicted: 0 cost-added: 1 cost-evicted: 0 sets-dropped: 0 sets-rejected: 0 gets-dropped: 0 gets-kept: 0 gets-total: 2 hit-ratio: 0.50", cc.cache.Metrics.String())

ccc, err = cc.SetContent(k1, bytes.NewReader([]byte("test")))
time.Sleep(100 * time.Millisecond)
assert.Empty(t, err)
assert.Equal(t, bytes.NewBuffer([]byte("test")), ccc)
assert.Equal(t, "hit: 1 miss: 1 keys-added: 1 keys-updated: 1 keys-evicted: 0 cost-added: 4 cost-evicted: 0 sets-dropped: 0 sets-rejected: 0 gets-dropped: 0 gets-kept: 0 gets-total: 2 hit-ratio: 0.50", cc.cache.Metrics.String())

ccc, found = cc.GetContent(k1)
assert.Equal(t, true, found)
assert.Equal(t, bytes.NewBuffer([]byte("test")), ccc)
assert.Equal(t, "hit: 2 miss: 1 keys-added: 1 keys-updated: 1 keys-evicted: 0 cost-added: 4 cost-evicted: 0 sets-dropped: 0 sets-rejected: 0 gets-dropped: 0 gets-kept: 0 gets-total: 3 hit-ratio: 0.67", cc.cache.Metrics.String())

ccc, err = cc.SetContent(k2, bytes.NewReader([]byte("overflow")))
time.Sleep(100 * time.Millisecond)
assert.Empty(t, err)
assert.Equal(t, bytes.NewBuffer([]byte("overflow")), ccc)
assert.Equal(t, "hit: 2 miss: 1 keys-added: 2 keys-updated: 1 keys-evicted: 1 cost-added: 12 cost-evicted: 4 sets-dropped: 0 sets-rejected: 0 gets-dropped: 0 gets-kept: 0 gets-total: 3 hit-ratio: 0.67", cc.cache.Metrics.String())

ccc, found = cc.GetContent(k2)
assert.Equal(t, true, found)
assert.Equal(t, bytes.NewBuffer([]byte("overflow")), ccc)
assert.Equal(t, "hit: 3 miss: 1 keys-added: 2 keys-updated: 1 keys-evicted: 1 cost-added: 12 cost-evicted: 4 sets-dropped: 0 sets-rejected: 0 gets-dropped: 0 gets-kept: 0 gets-total: 4 hit-ratio: 0.75", cc.cache.Metrics.String())

ccc, err = cc.SetContent(k3, bytes.NewReader([]byte("content too big")))
time.Sleep(100 * time.Millisecond)
assert.Empty(t, err)
assert.Equal(t, bytes.NewBuffer([]byte("content too big")), ccc)
assert.Equal(t, "hit: 3 miss: 1 keys-added: 2 keys-updated: 1 keys-evicted: 1 cost-added: 12 cost-evicted: 4 sets-dropped: 0 sets-rejected: 0 gets-dropped: 0 gets-kept: 0 gets-total: 4 hit-ratio: 0.75", cc.cache.Metrics.String())

ccc, found = cc.GetContent(k3)
assert.Equal(t, false, found)
assert.Equal(t, "hit: 3 miss: 2 keys-added: 2 keys-updated: 1 keys-evicted: 1 cost-added: 12 cost-evicted: 4 sets-dropped: 0 sets-rejected: 0 gets-dropped: 0 gets-kept: 0 gets-total: 5 hit-ratio: 0.60", cc.cache.Metrics.String())
}

func TestDataRace(t *testing.T) {
fmt.Println("testing data race...")
var wg sync.WaitGroup
load := func(r io.ReadSeeker) (*bytes.Buffer, int64, error) {
b, err := io.ReadAll(r)
nb := bytes.NewBuffer(b)
if err != nil {
return nb, 0, err
}
return nb, int64(nb.Len()), nil
}
cc, err := NewContentCache[CacheKeyMock](16, true, load)
assert.Empty(t, err)
k1 := CacheKeyMock("id1")
k2 := CacheKeyMock("id2")
wg.Add(1)
go func() {
defer wg.Done()
_, err := cc.SetContent(k1, bytes.NewReader([]byte("data")))
time.Sleep(100 * time.Millisecond)
assert.Empty(t, err)
_, err = cc.SetContent(k2, bytes.NewReader([]byte("race")))
time.Sleep(100 * time.Millisecond)
assert.Empty(t, err)
_, found := cc.GetContent(k1)
assert.Equal(t, true, found)
_, found = cc.GetContent(k2)
assert.Equal(t, true, found)
}()
wg.Add(1)
go func() {
defer wg.Done()
_, err := cc.SetContent(k1, bytes.NewReader([]byte("race")))
time.Sleep(100 * time.Millisecond)
assert.Empty(t, err)
_, err = cc.SetContent(k2, bytes.NewReader([]byte("data")))
time.Sleep(100 * time.Millisecond)
assert.Empty(t, err)
_, found := cc.GetContent(k1)
assert.Equal(t, true, found)
_, found = cc.GetContent(k2)
assert.Equal(t, true, found)
}()
wg.Add(1)
go func() {
defer wg.Done()
_, err := cc.SetContent(k2, bytes.NewReader([]byte("data")))
time.Sleep(100 * time.Millisecond)
assert.Empty(t, err)
_, err = cc.SetContent(k1, bytes.NewReader([]byte("race")))
time.Sleep(100 * time.Millisecond)
assert.Empty(t, err)
_, found := cc.GetContent(k1)
assert.Equal(t, true, found)
_, found = cc.GetContent(k2)
assert.Equal(t, true, found)
}()
wg.Add(1)
go func() {
defer wg.Done()
_, err := cc.SetContent(k2, bytes.NewReader([]byte("race")))
time.Sleep(100 * time.Millisecond)
assert.Empty(t, err)
_, err = cc.SetContent(k1, bytes.NewReader([]byte("data")))
time.Sleep(100 * time.Millisecond)
assert.Empty(t, err)
_, found := cc.GetContent(k1)
assert.Equal(t, true, found)
_, found = cc.GetContent(k2)
assert.Equal(t, true, found)
}()
wg.Wait()
}
Loading