diff --git a/.forgejo/workflows/test.yml b/.forgejo/workflows/test.yml new file mode 100644 index 0000000..696628c --- /dev/null +++ b/.forgejo/workflows/test.yml @@ -0,0 +1,26 @@ +name: test +on: + pull_request: + push: + branches: + - main + +jobs: + test: + runs-on: docker-bookworm + container: + image: 'code.forgejo.org/oci/node:20-bookworm' + services: + memcached: + image: memcached:1.6-alpine + steps: + - uses: https://code.forgejo.org/actions/checkout@v4 + - uses: https://code.forgejo.org/actions/setup-go@v5 + with: + go-version-file: "go.mod" + - name: golangci-lint + uses: https://github.com/golangci/golangci-lint-action@v6 + with: + version: v1.60.3 # renovate: datasource=go depName=golangci-lint packageName=github.com/golangci/golangci-lint/cmd/golangci-lint + - name: test + run: MEMCACHE_CONN=memcached:11211 go test -v ./... diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..25287ce --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,27 @@ +linters: + enable-all: false + disable-all: true + fast: false + enable: + - bidichk + - dupl + - errcheck + - forbidigo + - gocritic + - gofmt + - gofumpt + - gosimple + - govet + - ineffassign + - nakedret + - nolintlint + - revive + - staticcheck + - stylecheck + - tenv + - testifylint + - typecheck + - unconvert + - unused + - unparam + - wastedassign diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8405e89 --- /dev/null +++ b/LICENSE @@ -0,0 +1,191 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, "control" means (i) the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising +permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +"Object" form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +"submitted" means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of +this License; and +You must cause any modified files to carry prominent notices stating that You +changed the files; and +You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +If the Work includes a "NOTICE" text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets "[]" replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same "printed page" as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..201f691 --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +# cache + +cache is a middleware that aim to have a transparent interface for a lot of cache implementations. + +Use use many cache adapters, including memory and Memcache. + +### Installation + + go get code.forgejo.org/go-chi/cache + +## Getting Help + +```go +// Cache is the interface that operates the cache data. +type Cache interface { + // Put puts value into cache with key and expire time. + Put(key string, val any, timeout int64) error + // Get gets cached value by given key. + Get(key string) any + // Delete deletes cached value by given key. + Delete(key string) error + // Incr increases cached int-type value by given key as a counter. + Incr(key string) error + // Decr decreases cached int-type value by given key as a counter. + Decr(key string) error + // IsExist returns true if cached value exists. + IsExist(key string) bool + // Flush deletes all cached data. + Flush() error + // StartAndGC starts GC routine based on config string settings. + StartAndGC(opt Options) error + // Ping tests if the cache is alive + Ping() error +} +``` + +## Credits + +This package is a modified version of [go-chi/cache](https://gitea.com/go-chi/cache) which is a modified version of [go-macaron/cache](https://github.com/go-macaron/cache). + +## License + +This project is under the Apache License, Version 2.0. See the [LICENSE](LICENSE) file for the full license text. diff --git a/cache.go b/cache.go new file mode 100644 index 0000000..5a1d622 --- /dev/null +++ b/cache.go @@ -0,0 +1,118 @@ +// Copyright 2013 Beego Authors +// Copyright 2014 The Macaron Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +// Package cache is a middleware that aim to have a transparent interface for a lot of cache implementations +package cache + +import ( + "fmt" +) + +// Cache is the interface that operates the cache data. +type Cache interface { + // Put puts value into cache with key and expire time. + Put(key string, val any, timeout int64) error + // Get gets cached value by given key. + Get(key string) any + // Delete deletes cached value by given key. + Delete(key string) error + // Incr increases cached int-type value by given key as a counter. + Incr(key string) error + // Decr decreases cached int-type value by given key as a counter. + Decr(key string) error + // IsExist returns true if cached value exists. + IsExist(key string) bool + // Flush deletes all cached data. + Flush() error + // StartAndGC starts GC routine based on config string settings. + StartAndGC(opt Options) error + // Ping tests if the cache is alive + Ping() error +} + +// Options represents a struct for specifying configuration options for the cache middleware. +type Options struct { + // Name of adapter. Default is "memory". + Adapter string + // Adapter configuration, it's corresponding to adapter. + AdapterConfig string + // GC interval time in seconds. Default is 60. + Interval int + // Occupy entire database. Default is false. + OccupyMode bool + // Configuration section name. Default is "cache". + Section string +} + +func prepareOptions(opt Options) Options { + if len(opt.Section) == 0 { + opt.Section = "cache" + } + if len(opt.Adapter) == 0 { + opt.Adapter = "memory" + } + if opt.Interval == 0 { + opt.Interval = 60 + } + if len(opt.AdapterConfig) == 0 { + opt.AdapterConfig = "data/caches" + } + + return opt +} + +// NewCacher creates and returns a new cacher by given adapter name and configuration. +// It panics when given adapter isn't registered and starts GC automatically. +func NewCacher(opt Options) (Cache, error) { + opt = prepareOptions(opt) + adapter, ok := adapters[opt.Adapter] + if !ok { + return nil, fmt.Errorf("cache: unknown adapter '%s'(forgot to import?)", opt.Adapter) + } + return adapter, adapter.StartAndGC(opt) +} + +var adapters = make(map[string]Cache) + +// Register registers a adapter. +func Register(name string, adapter Cache) { + if adapter == nil { + panic("cache: cannot register adapter with nil value") + } + if _, dup := adapters[name]; dup { + panic(fmt.Errorf("cache: cannot register adapter '%s' twice", name)) + } + adapters[name] = adapter +} + +const ( + pingTestKey = "__chi_cache_test" + pingTestVal = "test-value" +) + +// GenericPing test a cache by storing and retrieving a value +func GenericPing(c Cache) error { + if err := c.Put(pingTestKey, pingTestVal, 10); err != nil { + return err + } + val := c.Get(pingTestKey) + if valStr, ok := val.(string); !ok || valStr != pingTestVal { + return fmt.Errorf("ping doesn't seem to work correctly, set test value '%v' but get '%v'", + pingTestVal, + val, + ) + } + return c.Delete(pingTestKey) +} diff --git a/cache_test.go b/cache_test.go new file mode 100644 index 0000000..27d623a --- /dev/null +++ b/cache_test.go @@ -0,0 +1,131 @@ +// Copyright 2014 The Macaron Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package cache + +import ( + "encoding/gob" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_Cacher(t *testing.T) { + t.Run("Register invalid adapter", func(t *testing.T) { + t.Run("Adatper not exists", func(t *testing.T) { + opt := Options{ + Adapter: "fake", + } + _, err := NewCacher(opt) + assert.Error(t, err) + }) + + t.Run("Provider value is nil", func(t *testing.T) { + defer func() { + assert.NotNil(t, recover()) + }() + + Register("fake", nil) + }) + + t.Run("Register twice", func(t *testing.T) { + defer func() { + assert.NotNil(t, recover()) + }() + + Register("memory", &MemoryCacher{}) + }) + }) +} + +func GenerateAdapterTest(opt Options) func(t *testing.T) { + return func(t *testing.T) { + t.Run("Basic operations", func(t *testing.T) { + c, err := NewCacher(opt) + require.NoError(t, err) + + assert.NoError(t, c.Put("uname", "some-user-name", 1)) + assert.NoError(t, c.Put("uname2", "unknwon2", 1)) + assert.True(t, c.IsExist("uname")) + + assert.Nil(t, c.Get("404")) + assert.EqualValues(t, "some-user-name", c.Get("uname")) + + time.Sleep(1 * time.Second) + assert.False(t, c.IsExist("uname")) + assert.Nil(t, c.Get("uname")) + time.Sleep(1 * time.Second) + assert.Nil(t, c.Get("uname2")) + + require.NoError(t, c.Put("uname", "some-user-name", 0)) + require.NoError(t, c.Delete("uname")) + assert.Nil(t, c.Get("uname")) + + require.NoError(t, c.Put("uname", "some-user-name", 0)) + require.NoError(t, c.Flush()) + assert.Nil(t, c.Get("uname")) + + require.NoError(t, c.Delete("uname")) + + gob.Register(opt) + require.NoError(t, c.Put("struct", opt, 0)) + }) + + t.Run("Increase and decrease operations", func(t *testing.T) { + c, err := NewCacher(opt) + require.NoError(t, err) + + require.Error(t, c.Incr("404")) + require.Error(t, c.Decr("404")) + + require.NoError(t, c.Put("int", 0, 0)) + require.NoError(t, c.Put("int32", int32(0), 0)) + require.NoError(t, c.Put("int64", int64(0), 0)) + require.NoError(t, c.Put("uint", uint(0), 0)) + require.NoError(t, c.Put("uint32", uint32(0), 0)) + require.NoError(t, c.Put("uint64", uint64(0), 0)) + require.NoError(t, c.Put("string", "hi", 0)) + + require.Error(t, c.Decr("uint")) + require.Error(t, c.Decr("uint32")) + require.Error(t, c.Decr("uint64")) + + require.NoError(t, c.Incr("int")) + require.NoError(t, c.Incr("int32")) + require.NoError(t, c.Incr("int64")) + require.NoError(t, c.Incr("uint")) + require.NoError(t, c.Incr("uint32")) + require.NoError(t, c.Incr("uint64")) + + require.NoError(t, c.Decr("int")) + require.NoError(t, c.Decr("int32")) + require.NoError(t, c.Decr("int64")) + require.NoError(t, c.Decr("uint")) + require.NoError(t, c.Decr("uint32")) + require.NoError(t, c.Decr("uint64")) + + require.Error(t, c.Incr("string")) + require.Error(t, c.Decr("string")) + + assert.EqualValues(t, 0, c.Get("int")) + assert.EqualValues(t, 0, c.Get("int32")) + assert.EqualValues(t, 0, c.Get("int64")) + assert.EqualValues(t, 0, c.Get("uint")) + assert.EqualValues(t, 0, c.Get("uint32")) + assert.EqualValues(t, 0, c.Get("uint64")) + }) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..95bd4fb --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module code.forgejo.org/go-chi/cache + +go 1.23 + +require ( + github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 + github.com/stretchr/testify v1.9.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..65fe46b --- /dev/null +++ b/go.sum @@ -0,0 +1,12 @@ +github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 h1:N7oVaKyGp8bttX0bfZGmcGkjz7DLQXhAn3DNd3T0ous= +github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c= +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/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/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/memcache/memcache.go b/memcache/memcache.go new file mode 100644 index 0000000..7587bba --- /dev/null +++ b/memcache/memcache.go @@ -0,0 +1,112 @@ +// Copyright 2013 Beego Authors +// Copyright 2014 The Macaron Authors +// Copyright 2024 The Forgejo Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package memcache + +import ( + "fmt" + "strings" + + "github.com/bradfitz/gomemcache/memcache" + + "code.forgejo.org/go-chi/cache" +) + +// Cacher represents a memcache cache adapter implementation. +type Cacher struct { + c *memcache.Client +} + +func NewItem(key string, data []byte, expire int32) *memcache.Item { + return &memcache.Item{ + Key: key, + Value: data, + Expiration: expire, + } +} + +// Put puts value into cache with key and expire time. +// If expired is 0, it lives forever. +func (c *Cacher) Put(key string, val any, expire int64) error { + var v []byte + switch val := val.(type) { + case string: + v = []byte(val) + case []byte: + v = val + default: + // Best effort. + v = []byte(fmt.Sprint(val)) + } + + return c.c.Set(NewItem(key, v, int32(expire))) +} + +// Get gets cached value by given key. +func (c *Cacher) Get(key string) any { + item, err := c.c.Get(key) + if err != nil { + return nil + } + return string(item.Value) +} + +// Delete deletes cached value by given key. +func (c *Cacher) Delete(key string) error { + if err := c.c.Delete(key); err != nil && err != memcache.ErrCacheMiss { + return err + } + return nil +} + +// Incr increases cached int-type value by given key as a counter. +func (c *Cacher) Incr(key string) error { + _, err := c.c.Increment(key, 1) + return err +} + +// Decr decreases cached int-type value by given key as a counter. +func (c *Cacher) Decr(key string) error { + _, err := c.c.Decrement(key, 1) + return err +} + +// IsExist returns true if cached value exists. +func (c *Cacher) IsExist(key string) bool { + _, err := c.c.Get(key) + return err == nil +} + +// Flush deletes all cached data. +func (c *Cacher) Flush() error { + return c.c.FlushAll() +} + +// StartAndGC starts GC routine based on config string settings. +// AdapterConfig: 127.0.0.1:9090;127.0.0.1:9091 +func (c *Cacher) StartAndGC(opt cache.Options) error { + c.c = memcache.New(strings.Split(opt.AdapterConfig, ";")...) + return nil +} + +// Ping tests if the cache is alive. +func (c *Cacher) Ping() error { + return cache.GenericPing(c) +} + +func init() { + cache.Register("memcache", &Cacher{}) +} diff --git a/memcache/memcache_test.go b/memcache/memcache_test.go new file mode 100644 index 0000000..2881ef0 --- /dev/null +++ b/memcache/memcache_test.go @@ -0,0 +1,116 @@ +// Copyright 2014 The Macaron Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package memcache + +import ( + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "code.forgejo.org/go-chi/cache" +) + +func Test_MemcacheCacher(t *testing.T) { + conn := os.Getenv("MEMCACHE_CONN") + if conn == "" { + conn = "127.0.0.1:9090" + } + + opt := cache.Options{ + Adapter: "memcache", + AdapterConfig: conn, + } + + t.Run("Basic operations", func(t *testing.T) { + c, err := cache.NewCacher(opt) + require.NoError(t, err) + assert.NotNil(t, c) + + assert.NoError(t, c.Put("uname", "some-user-name", 1)) + assert.NoError(t, c.Put("uname2", "unknwon2", 1)) + assert.True(t, c.IsExist("uname")) + + assert.Nil(t, c.Get("404")) + assert.EqualValues(t, "some-user-name", c.Get("uname")) + + time.Sleep(1 * time.Second) + assert.Nil(t, c.Get("uname")) + time.Sleep(1 * time.Second) + assert.Nil(t, c.Get("uname2")) + + require.NoError(t, c.Put("uname", "some-user-name", 0)) + require.NoError(t, c.Delete("uname")) + assert.Nil(t, c.Get("uname")) + + require.NoError(t, c.Put("uname", "some-user-name", 0)) + require.NoError(t, c.Flush()) + assert.Nil(t, c.Get("uname")) + + require.NoError(t, c.Delete("uname")) + }) + + t.Run("Increase and decrease operations", func(t *testing.T) { + c, err := cache.NewCacher(opt) + require.NoError(t, err) + assert.NotNil(t, c) + + require.Error(t, c.Incr("404")) + require.Error(t, c.Decr("404")) + + require.NoError(t, c.Put("int", 0, 0)) + require.NoError(t, c.Put("int32", int32(0), 0)) + require.NoError(t, c.Put("int64", int64(0), 0)) + require.NoError(t, c.Put("uint", uint(0), 0)) + require.NoError(t, c.Put("uint32", uint32(0), 0)) + require.NoError(t, c.Put("uint64", uint64(0), 0)) + require.NoError(t, c.Put("string", "hi", 0)) + + require.NoError(t, c.Incr("int")) + require.NoError(t, c.Incr("int32")) + require.NoError(t, c.Incr("int64")) + require.NoError(t, c.Incr("uint")) + require.NoError(t, c.Incr("uint32")) + require.NoError(t, c.Incr("uint64")) + + assert.EqualValues(t, "1", c.Get("int")) + assert.EqualValues(t, "1", c.Get("int32")) + assert.EqualValues(t, "1", c.Get("int64")) + assert.EqualValues(t, "1", c.Get("uint")) + assert.EqualValues(t, "1", c.Get("uint32")) + assert.EqualValues(t, "1", c.Get("uint64")) + + require.NoError(t, c.Decr("int")) + require.NoError(t, c.Decr("int32")) + require.NoError(t, c.Decr("int64")) + require.NoError(t, c.Decr("uint")) + require.NoError(t, c.Decr("uint32")) + require.NoError(t, c.Decr("uint64")) + + require.Error(t, c.Incr("string")) + require.Error(t, c.Decr("string")) + + assert.EqualValues(t, "0", c.Get("int")) + assert.EqualValues(t, "0", c.Get("int32")) + assert.EqualValues(t, "0", c.Get("int64")) + assert.EqualValues(t, "0", c.Get("uint")) + assert.EqualValues(t, "0", c.Get("uint32")) + assert.EqualValues(t, "0", c.Get("uint64")) + + require.NoError(t, c.Flush()) + }) +} diff --git a/memory.go b/memory.go new file mode 100644 index 0000000..4e2c1d5 --- /dev/null +++ b/memory.go @@ -0,0 +1,183 @@ +// Copyright 2013 Beego Authors +// Copyright 2014 The Macaron Authors +// Copyright 2024 The Forgejo Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package cache + +import ( + "errors" + "sync" + "time" +) + +// MemoryItem represents a memory cache item. +type MemoryItem struct { + val any + created int64 + expire int64 +} + +func (item *MemoryItem) hasExpired() bool { + return item.expire > 0 && + (time.Now().Unix()-item.created) >= item.expire +} + +// MemoryCacher represents a memory cache adapter implementation. +type MemoryCacher struct { + lock sync.RWMutex + items map[string]*MemoryItem + interval int // GC interval. +} + +// Put puts value into cache with key and expire time. +// If expired is 0, it will be deleted by next GC operation. +func (c *MemoryCacher) Put(key string, val any, expire int64) error { + c.lock.Lock() + defer c.lock.Unlock() + + c.items[key] = &MemoryItem{ + val: val, + created: time.Now().Unix(), + expire: expire, + } + return nil +} + +// Get gets cached value by given key. +func (c *MemoryCacher) Get(key string) any { + c.lock.RLock() + defer c.lock.RUnlock() + + item, ok := c.items[key] + if !ok { + return nil + } + if item.hasExpired() { + go func() { + _ = c.Delete(key) + }() + return nil + } + return item.val +} + +// Delete deletes cached value by given key. +func (c *MemoryCacher) Delete(key string) error { + c.lock.Lock() + defer c.lock.Unlock() + + delete(c.items, key) + return nil +} + +// Incr increases cached int-type value by given key as a counter. +func (c *MemoryCacher) Incr(key string) (err error) { + c.lock.RLock() + defer c.lock.RUnlock() + + item, ok := c.items[key] + if !ok { + return errors.New("key not exist") + } + item.val, err = Incr(item.val) + return err +} + +// Decr decreases cached int-type value by given key as a counter. +func (c *MemoryCacher) Decr(key string) (err error) { + c.lock.RLock() + defer c.lock.RUnlock() + + item, ok := c.items[key] + if !ok { + return errors.New("key not exist") + } + + item.val, err = Decr(item.val) + return err +} + +// IsExist returns true if cached value exists. +func (c *MemoryCacher) IsExist(key string) bool { + c.lock.RLock() + defer c.lock.RUnlock() + + if item, ok := c.items[key]; ok { + return !item.hasExpired() + } + return false +} + +// Flush deletes all cached data. +func (c *MemoryCacher) Flush() error { + c.lock.Lock() + defer c.lock.Unlock() + + clear(c.items) + return nil +} + +func (c *MemoryCacher) checkRawExpiration(key string) { + item, ok := c.items[key] + if !ok { + return + } + + if item.hasExpired() { + delete(c.items, key) + } +} + +func (c *MemoryCacher) startGC() { + c.lock.RLock() + if c.interval < 1 { + c.lock.RUnlock() + return + } + + ticker := time.NewTicker(time.Duration(c.interval) * time.Second) + c.lock.RUnlock() + + for { + <-ticker.C + + c.lock.Lock() + if c.items != nil { + for key := range c.items { + c.checkRawExpiration(key) + } + } + c.lock.Unlock() + } +} + +// StartAndGC starts GC routine based on config string settings. +func (c *MemoryCacher) StartAndGC(opt Options) error { + c.lock.Lock() + c.interval = opt.Interval + c.lock.Unlock() + + go c.startGC() + return nil +} + +func init() { + Register("memory", &MemoryCacher{items: make(map[string]*MemoryItem)}) +} + +// Ping tests if the cache is alive. +func (c *MemoryCacher) Ping() error { + return GenericPing(c) +} diff --git a/memory_test.go b/memory_test.go new file mode 100644 index 0000000..3573477 --- /dev/null +++ b/memory_test.go @@ -0,0 +1,35 @@ +// Copyright 2014 The Macaron Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package cache + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_MemoryCacher(t *testing.T) { + GenerateAdapterTest(Options{ + Interval: 2, + })(t) +} + +func Test_MemoryPing(t *testing.T) { + c, err := NewCacher(Options{Adapter: "memory"}) + require.NoError(t, err) + assert.NotNil(t, c) + assert.NoError(t, c.Ping()) +} diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..3146f63 --- /dev/null +++ b/renovate.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["go-chi/renovate-config"] +} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..0637c9a --- /dev/null +++ b/utils.go @@ -0,0 +1,71 @@ +// Copyright 2014 The Macaron Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"): you may +// not use this file except in compliance with the License. You may obtain +// a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations +// under the License. + +package cache + +import ( + "errors" +) + +func Incr(val any) (any, error) { + switch valTyp := val.(type) { + case int: + val = valTyp + 1 + case int32: + val = valTyp + 1 + case int64: + val = valTyp + 1 + case uint: + val = valTyp + 1 + case uint32: + val = valTyp + 1 + case uint64: + val = valTyp + 1 + default: + return val, errors.New("item value is not int-type") + } + return val, nil +} + +func Decr(val any) (any, error) { + switch valTyp := val.(type) { + case int: + val = valTyp - 1 + case int32: + val = valTyp - 1 + case int64: + val = valTyp - 1 + case uint: + if valTyp > 0 { + val = valTyp - 1 + } else { + return val, errors.New("item value is less than 0") + } + case uint32: + if valTyp > 0 { + val = valTyp - 1 + } else { + return val, errors.New("item value is less than 0") + } + case uint64: + if valTyp > 0 { + val = valTyp - 1 + } else { + return val, errors.New("item value is less than 0") + } + default: + return val, errors.New("item value is not int-type") + } + return val, nil +}