diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 0000000..f30c5c6 --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,12 @@ +coverage: + status: + project: + default: + target: 0 + threshold: null + base: auto + patch: + default: + target: 0 + threshold: null + base: auto diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..3012e3a --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @yuseferi diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..f8812b0 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +# Configure version updates for both dependencies defined in manifests and vendored dependencies + +version: 2 +updates: + - package-ecosystem: gomod + directory: / + schedule: + interval: "weekly" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..de941ff --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,24 @@ +# This workflow will build a golang project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go +name: Build +on: + workflow_run: + workflows: [Quality check] + types: [completed] + + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Set up Go + uses: actions/setup-go@v4 + - name: Build + run: go mod download; go build -v ./... diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8138d32 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,47 @@ +# This workflow will build a golang project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go +name: Quality check +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + lint: + name: Linter + timeout-minutes: 10 + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Linter + uses: golangci/golangci-lint-action@v3 + with: + version: v1.53.2 + fail_ci_if_error: true + tests: + name: Tests + needs: lint + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Set up Go + uses: actions/setup-go@v4 + - name: Run tests + run: go mod download; go test -cover -coverprofile=./unit-cover.txt -race ./... + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v3 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + with: + files: ./unit-cover.txt + flags: unittests + name: codecov-umbrella + fail_ci_if_error: true + verbose: true \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3b735ec..f50e6e1 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,5 @@ # Go workspace file go.work +.vs/ +.idea/ diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..2f15b55 --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,47 @@ +run: + deadline: 5m + issues-exit-code: 1 + skip-dirs: + - docs + - mocks + - scripts + +output: + format: colored-line-number + print-issued-lines: true + print-linter-name: true + +linters-settings: + dupl: + threshold: 250 + lll: + line-length: 160 + goconst: + min-len: 2 + min-occurrences: 3 + errcheck: + exclude-functions: + - (io.Closer).Close + cyclop: + max-complexity: 10 + funlen: + lines: 50 + +linters: + disable-all: true + enable: + - dupl + - errcheck + - goconst + - gosec + - gosimple + - govet + - ineffassign + - staticcheck + - typecheck + - unused + - funlen + - cyclop + - lll + - forbidigo + diff --git a/README.md b/README.md new file mode 100644 index 0000000..14d5869 --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# gocache (zap with context) +[![codecov](https://codecov.io/github/yuseferi/gocache/branch/codecov-integration/graph/badge.svg?token=64IHXT3ROF)](https://codecov.io/github/yuseferi/gocache) +[![CodeQL](https://github.com/yuseferi/gocache/actions/workflows/github-code-scanning/codeql/badge.svg)](https://github.com/yuseferi/gocache/actions/workflows/github-code-scanning/codeql) +[![Check & Build](https://github.com/yuseferi/gocache/actions/workflows/ci.yml/badge.svg)](https://github.com/yuseferi/gocache/actions/workflows/ci.yml) +[![License: AGPL v3](https://img.shields.io/badge/License-AGPL_v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0) +![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/yuseferi/gocache) + +gocache provides a data race-free cache implementation in Go. + + +### Installation + +```shell + go get -u github.com/yuseferi/gocache +``` + +### Usage: + + + +```Go +cache := gocache.NewCache(time.Minute * 2) // with 2 minutes interval cleaning expired items +cache.Set("key", "value", time.Minute) // set cache +value, found := cache.Get("key") // retrive cache data +cache.Delete("key") // delete specific key manually +cache.Clear() // clear all cache items ( purge) +size := cache.Size() // get cache size +``` + + +### Contributing +We strongly believe in open-source ❤️😊. Please feel free to contribute by raising issues and submitting pull requests to make gocache even better! + + +Released under the [GNU GENERAL PUBLIC LICENSE](LICENSE). + + + + diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..554d296 --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module github.com/yuseferi/gocache + +go 1.20 + +require github.com/stretchr/testify v1.8.4 + +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..fa4b6e6 --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +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.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +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/gocache.go b/gocache.go new file mode 100644 index 0000000..0ef4000 --- /dev/null +++ b/gocache.go @@ -0,0 +1,114 @@ +// Package gocache provides a data race-free cache implementation in Go. +// +// Usage: +// +// cache := gocache.NewCache(time.Minute * 2) // with 2 minutes interval cleaning +// cache.Set("key", "value", time.Minute) +// value, found := cache.Get("key") +// cache.Delete("key") +// cache.Clear() +// size := cache.Size() +package gocache + +import ( + "sync" + "time" +) + +// Cache represents a data race-free cache. +type Cache struct { + items map[string]cacheItem + mutex sync.RWMutex + cleanupExpiredPeriod time.Duration +} + +type cacheItem struct { + value interface{} + expiration time.Time +} + +// NewCache creates a new Cache instance. +func NewCache(cleanupExpiredPeriod time.Duration) *Cache { + cache := &Cache{ + items: make(map[string]cacheItem), + } + cache.cleanupExpiredPeriod = cleanupExpiredPeriod + // Start a goroutine to periodically check for expired items and remove them + go cache.deleteExpiredItems() + + return cache +} + +// Get retrieves the value associated with the specified key from the cache. +// It returns the value and a boolean indicating whether the key was found or not. +// If the key is found but the associated item has expired, the value will be nil +// and the boolean will be false. +func (c *Cache) Get(key string) (interface{}, bool) { + c.mutex.RLock() + defer c.mutex.RUnlock() + + item, found := c.items[key] + if !found { + return nil, false + } + + if item.expiration.Before(time.Now()) { + return nil, false + } + + return item.value, true +} + +// Set adds or updates a key-value pair in the cache with the specified expiration duration. +// If the key already exists, its value and expiration are updated. +func (c *Cache) Set(key string, value interface{}, expiration time.Duration) { + c.mutex.Lock() + defer c.mutex.Unlock() + + expirationTime := time.Now().Add(expiration) + c.items[key] = cacheItem{ + value: value, + expiration: expirationTime, + } +} + +// Delete removes the specified key and its associated value from the cache. +// If the key does not exist in the cache, the function does nothing. +func (c *Cache) Delete(key string) { + c.mutex.Lock() + defer c.mutex.Unlock() + + delete(c.items, key) +} + +// Clear removes all items from the cache, making it empty. +func (c *Cache) Clear() { + c.mutex.Lock() + defer c.mutex.Unlock() + + c.items = make(map[string]cacheItem) +} + +// Size returns the number of items currently stored in the cache. +func (c *Cache) Size() int { + c.mutex.RLock() + defer c.mutex.RUnlock() + + return len(c.items) +} + +// deleteExpiredItems is a background goroutine that periodically checks for expired items in the cache +// and removes them. It runs indefinitely after the Cache is created. +func (c *Cache) deleteExpiredItems() { + for { + <-time.After(c.cleanupExpiredPeriod) // Adjust the time interval for checking expired items + + c.mutex.Lock() + for key, item := range c.items { + if item.expiration.Before(time.Now()) { + delete(c.items, key) + } + } + c.mutex.Unlock() + } +} diff --git a/gocache_benchmark_test.go b/gocache_benchmark_test.go new file mode 100644 index 0000000..b7d1a73 --- /dev/null +++ b/gocache_benchmark_test.go @@ -0,0 +1,80 @@ +package gocache + +import ( + "fmt" + "strconv" + "sync" + "testing" + "time" +) + +func BenchmarkCache_Set(b *testing.B) { + cache := NewCache(time.Minute) + for i := 0; i < b.N; i++ { + key := strconv.Itoa(i) + value := fmt.Sprintf("value%d", i) + cache.Set(key, value, time.Minute) + } +} + +func BenchmarkCache_Get(b *testing.B) { + cache := NewCache(time.Minute) + for i := 0; i < b.N; i++ { + key := strconv.Itoa(i) + value := fmt.Sprintf("value%d", i) + cache.Set(key, value, time.Minute) + } + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + key := strconv.Itoa(i) + cache.Get(key) + } +} + +func BenchmarkCache_Delete(b *testing.B) { + cache := NewCache(time.Minute) + for i := 0; i < b.N; i++ { + key := strconv.Itoa(i) + value := fmt.Sprintf("value%d", i) + cache.Set(key, value, time.Minute) + } + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + key := strconv.Itoa(i) + cache.Delete(key) + } +} + +func BenchmarkCache_ConcurrentAccess(b *testing.B) { + cache := NewCache(time.Minute) + concurrency := 100 + numOperations := b.N + + // Populate the cache with initial data + for i := 0; i < concurrency; i++ { + key := strconv.Itoa(i) + value := fmt.Sprintf("value%d", i) + cache.Set(key, value, time.Minute) + } + + // Run concurrent access to the cache + var wg sync.WaitGroup + wg.Add(concurrency) + + for i := 0; i < concurrency; i++ { + go func() { + defer wg.Done() + + for j := 0; j < numOperations; j++ { + key := strconv.Itoa(j % concurrency) + cache.Get(key) + } + }() + } + + wg.Wait() +} diff --git a/gocache_test.go b/gocache_test.go new file mode 100644 index 0000000..34ab6e8 --- /dev/null +++ b/gocache_test.go @@ -0,0 +1,106 @@ +package gocache + +import ( + "testing" + "time" +) + +func TestCache_Get(t *testing.T) { + cache := NewCache(time.Second) + cache.Set("key", "value", time.Second) + + value, found := cache.Get("key") + if !found { + t.Errorf("Expected key 'key' to be found") + } + if value != "value" { + t.Errorf("Expected value 'value', got %v", value) + } + // Test if it really expired + time.Sleep(time.Second * 2) + value, found = cache.Get("key") + if found || value != nil { + t.Errorf("Expected key 'key' to be expired and not found") + } + + // Test a non-existent key + value, found = cache.Get("nonexistent") + if found || value != nil { + t.Errorf("Expected non-existent key to not be found") + } +} + +func TestCache_Set(t *testing.T) { + cache := NewCache(time.Second) + + // Test setting a key-value pair + cache.Set("key", "value", time.Minute) + + // Retrieve the value to verify + value, found := cache.Get("key") + if !found { + t.Errorf("Expected key 'key' to be found") + } + if value != "value" { + t.Errorf("Expected value 'value', got %v", value) + } + + // Test updating an existing key + cache.Set("key", "newvalue", time.Minute) + + // Retrieve the value to verify the update + value, found = cache.Get("key") + if !found { + t.Errorf("Expected key 'key' to be found") + } + if value != "newvalue" { + t.Errorf("Expected value 'newvalue', got %v", value) + } +} + +func TestCache_Delete(t *testing.T) { + cache := NewCache(time.Second) + cache.Set("key", "value", time.Minute) + + // Delete an existing key + cache.Delete("key") + + // Verify the key is no longer found + _, found := cache.Get("key") + if found { + t.Errorf("Expected key 'key' to be deleted") + } + + // Delete a non-existent key + cache.Delete("nonexistent") +} + +func TestCache_Clear(t *testing.T) { + cache := NewCache(time.Second) + cache.Set("key1", "value1", time.Minute) + cache.Set("key2", "value2", time.Minute) + + // Clear the cache + cache.Clear() + + // Verify the cache is empty + if size := cache.Size(); size != 0 { + t.Errorf("Expected cache size 0, got %d", size) + } +} + +func TestCache_Size(t *testing.T) { + cache := NewCache(time.Second) + + // Empty cache + if size := cache.Size(); size != 0 { + t.Errorf("Expected cache size 0, got %d", size) + } + + // Non-empty cache + cache.Set("key1", "value1", time.Minute) + cache.Set("key2", "value2", time.Minute) + if size := cache.Size(); size != 2 { + t.Errorf("Expected cache size 2, got %d", size) + } +}