diff --git a/go.mod b/go.mod index 5cac566..46f4c76 100644 --- a/go.mod +++ b/go.mod @@ -2,16 +2,18 @@ module code.d7z.net/d7z-project/gitea-pages go 1.23.2 -require code.gitea.io/sdk/gitea v0.19.0 +require ( + code.gitea.io/sdk/gitea v0.19.0 + github.com/stretchr/testify v1.8.1 +) require ( + github.com/davecgh/go-spew v1.1.1 // indirect github.com/davidmz/go-pageant v1.0.2 // indirect github.com/go-fed/httpsig v1.1.0 // indirect github.com/hashicorp/go-version v1.6.0 // indirect - github.com/ncruces/go-sqlite3 v0.21.3 // indirect - github.com/ncruces/julianday v1.0.0 // indirect - github.com/stretchr/testify v1.8.1 // indirect - github.com/tetratelabs/wazero v1.8.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/crypto v0.31.0 // indirect golang.org/x/sys v0.28.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index c48fa48..d41f108 100644 --- a/go.sum +++ b/go.sum @@ -9,10 +9,6 @@ github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI= github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM= github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/ncruces/go-sqlite3 v0.21.3 h1:hHkfNQLcbnxPJZhC/RGw9SwP3bfkv/Y0xUHWsr1CdMQ= -github.com/ncruces/go-sqlite3 v0.21.3/go.mod h1:zxMOaSG5kFYVFK4xQa0pdwIszqxqJ0W0BxBgwdrNjuA= -github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M= -github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -22,30 +18,25 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/tetratelabs/wazero v1.8.2 h1:yIgLR/b2bN31bjxwXHD8a3d+BogigR952csSDdLYEv4= -github.com/tetratelabs/wazero v1.8.2/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= -golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= -golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/pkg/services/cache.go b/pkg/services/cache.go index cb82be4..1c5311f 100644 --- a/pkg/services/cache.go +++ b/pkg/services/cache.go @@ -1,10 +1,12 @@ package services import ( + "bytes" "errors" "io" + "os" + "strings" "sync" - "time" ) type Cache interface { @@ -17,11 +19,10 @@ type Cache interface { var ErrCacheOutOfMemory = errors.New("内容无法被缓存,超过最大限定值") type CacheMemory struct { - l sync.RWMutex - data map[string][]byte - maxAge time.Duration - sizeGl int - sizeOne int + l sync.RWMutex + data map[string][]byte + sizeGlobal int + sizeItem int current int cache []byte @@ -30,10 +31,10 @@ type CacheMemory struct { func NewCacheMemory(maxUsage, maxGlobalUsage int) *CacheMemory { return &CacheMemory{ - data: make(map[string][]byte), - l: sync.RWMutex{}, - sizeGl: maxGlobalUsage, - sizeOne: maxUsage, + data: make(map[string][]byte), + l: sync.RWMutex{}, + sizeGlobal: maxGlobalUsage, + sizeItem: maxUsage, cache: make([]byte, maxUsage+1), ordered: make([]string, 0), @@ -43,32 +44,32 @@ func NewCacheMemory(maxUsage, maxGlobalUsage int) *CacheMemory { func (c *CacheMemory) Put(key string, reader io.Reader) error { c.l.Lock() defer c.l.Unlock() - size, err := io.ReadAtLeast(reader, c.cache, 0) + size, err := io.ReadAtLeast(reader, c.cache, 1) if err != nil { return err } if size == len(c.cache) { return ErrCacheOutOfMemory } - needed := c.sizeGl - c.current + size - if needed < 0 { + currentItemSize := len(c.data[key]) + available := c.sizeGlobal + currentItemSize - (c.current + size) + if available < 0 { // 清理旧的内容 count := 0 for i, k := range c.ordered { - needed += len(c.data[k]) - if needed > 0 { + available += len(c.data[k]) + if available > 0 { break } count = i + 1 } - if needed < 0 { + if available < 0 { // 清理全部内容也无法留出空间 return ErrCacheOutOfMemory } for _, s := range c.ordered[:count] { delete(c.data, s) - c.current -= len(c.data) } c.ordered = c.ordered[count:] } @@ -76,22 +77,59 @@ func (c *CacheMemory) Put(key string, reader io.Reader) error { dest := make([]byte, size) copy(dest, c.cache[:size]) c.data[key] = dest - c.ordered = append(c.ordered, key) + + c.current -= currentItemSize c.current += len(dest) + + nextOrdered := make([]string, 0, len(c.ordered)) + for _, s := range c.ordered { + if s != key { + nextOrdered = append(nextOrdered, s) + } + } + c.ordered = append(nextOrdered, key) return nil } func (c *CacheMemory) Get(key string) (io.ReadSeekCloser, error) { - //TODO implement me - panic("implement me") + c.l.RLock() + defer c.l.RUnlock() + if i, ok := c.data[key]; ok { + return nopCloser{ + bytes.NewReader(i), + }, nil + } + return nil, os.ErrNotExist } -func (c *CacheMemory) Delete(key string) error { - //TODO implement me - panic("implement me") +func (c *CacheMemory) Delete(pattern string) error { + c.l.Lock() + defer c.l.Unlock() + nextOrder := make([]string, 0, len(c.ordered)) + for _, key := range c.ordered { + if strings.HasPrefix(key, pattern) { + c.current -= len(c.data[key]) + delete(c.data, key) + } else { + nextOrder = append(nextOrder, key) + } + } + clear(c.ordered) + c.ordered = nextOrder + return nil } func (c *CacheMemory) Close() error { - //TODO implement me - panic("implement me") + c.l.Lock() + defer c.l.Unlock() + clear(c.ordered) + clear(c.data) + c.current = 0 + return nil } + +type nopCloser struct { + io.ReadSeeker +} + +func (nopCloser) Close() error { return nil } diff --git a/pkg/services/cache_test.go b/pkg/services/cache_test.go new file mode 100644 index 0000000..c45797b --- /dev/null +++ b/pkg/services/cache_test.go @@ -0,0 +1,69 @@ +package services + +import ( + "fmt" + "github.com/stretchr/testify/require" + "io" + "os" + "strings" + "testing" +) + +func TestCacheGetPutDelete(t *testing.T) { + memory := NewCacheMemory(1024, 10240) + + require.NoError(t, memory.Put("hello", strings.NewReader("world"))) + + value, err := memory.Get("hello") + require.NoError(t, err) + all, err := io.ReadAll(value) + require.NoError(t, err) + require.Equal(t, "world", string(all)) + require.Equal(t, 5, memory.current) + + require.NoError(t, memory.Put("hello", strings.NewReader("kotlin"))) + + value, err = memory.Get("hello") + require.NoError(t, err) + all, err = io.ReadAll(value) + require.NoError(t, err) + require.Equal(t, "kotlin", string(all)) + require.Equal(t, 6, memory.current) + require.Equal(t, 1, len(memory.data)) + + require.NoError(t, memory.Put("data", strings.NewReader("kotlin"))) + require.Equal(t, 12, memory.current) + require.Equal(t, 2, len(memory.data)) + require.Equal(t, 2, len(memory.ordered)) + + require.NoError(t, memory.Delete("hello")) + value, err = memory.Get("hello") + require.Error(t, err) + require.Equal(t, 1, len(memory.data)) + require.Equal(t, 1, len(memory.ordered)) + +} + +func TestCacheLimit(t *testing.T) { + memory := NewCacheMemory(5, 5*5) + require.NoError(t, memory.Put("hello", strings.NewReader("world"))) + require.Equal(t, 5, memory.current) + require.ErrorIs(t, memory.Put("hello", strings.NewReader("world1")), ErrCacheOutOfMemory) + require.Equal(t, 5, memory.current) + for i := 0; i < 4; i++ { + require.NoError(t, memory.Put(fmt.Sprintf("hello-%d", i), strings.NewReader("govet"))) + } + value, err := memory.Get("hello") + require.NoError(t, err) + all, err := io.ReadAll(value) + require.NoError(t, err) + require.Equal(t, "world", string(all)) + + require.NoError(t, memory.Put("test", strings.NewReader("govet"))) + + value, err = memory.Get("hello") + require.ErrorIs(t, err, os.ErrNotExist) + + require.Equal(t, 5, len(memory.data)) + require.Equal(t, 5, len(memory.ordered)) +} diff --git a/pkg/services/config.go b/pkg/services/config.go index c96db81..407322d 100644 --- a/pkg/services/config.go +++ b/pkg/services/config.go @@ -15,44 +15,33 @@ type Config interface { func NewConfigMemory() Config { return &ConfigMemory{ - data: make(map[string]string), - lock: sync.RWMutex{}, + data: sync.Map{}, } } // ConfigMemory 一个简单的内存配置归档,仅用于测试 type ConfigMemory struct { - data map[string]string - lock sync.RWMutex + data sync.Map } func (m *ConfigMemory) Put(key string, value string) error { - m.lock.Lock() - defer m.lock.Unlock() - m.data[key] = value + m.data.Store(key, value) return nil } func (m *ConfigMemory) Get(key string) (string, error) { - m.lock.RLock() - defer m.lock.RUnlock() - v, ok := m.data[key] - if !ok { - return "", os.ErrNotExist + if value, ok := m.data.Load(key); ok { + return value.(string), nil } - return v, nil + return "", os.ErrNotExist } func (m *ConfigMemory) Delete(key string) error { - m.lock.Lock() - defer m.lock.Unlock() - delete(m.data, key) + m.data.Delete(key) return nil } func (m *ConfigMemory) Close() error { - m.lock.Lock() - defer m.lock.Unlock() - clear(m.data) + m.data.Clear() return nil }