diff --git a/README.md b/README.md index 72d1972..69f8886 100644 --- a/README.md +++ b/README.md @@ -236,6 +236,11 @@ To do so - you need to enable the api by setting env variable `ENABLE_API=true` * `POST /api/user-ban` - ban the user from accessing the application * `POST /api/user-unban` - unban the user from accessing the application +#### Cache operations + +* `POST /api/cache-clear` - clear the cache +* `GET /api/cache-stats` - get the cache statistics ( hits, misses, size ) + Both endpoints require the `user_id` parameter to be present in the request body and allow you to provide the reason for the ban. Example request: diff --git a/api.go b/api.go index 15df64c..4dadf04 100644 --- a/api.go +++ b/api.go @@ -23,6 +23,8 @@ func enableApi() { api := apiserver.Group("/api") api.Post("/user-ban", apiBanUser) api.Post("/user-unban", apiUnbanUser) + api.Post("/cache-clear", apiClearCache) + api.Get("/cache-stats", apiCacheStats) go periodicallyReloadBannedUsers() err := apiserver.Listen(fmt.Sprintf(":%d", cfg.Server.ApiPort)) @@ -50,6 +52,21 @@ func checkIfUserIsBanned(c *fiber.Ctx, userID string) bool { return found } +func apiClearCache(c *fiber.Ctx) error { + cfg.Logger.Debug("Clearing cache via API", nil) + cfg.Cache.CacheClient.ClearCache() + cfg.Logger.Info("Cache cleared via API", nil) + c.Status(200).SendString("OK: cache cleared") + return nil +} + +func apiCacheStats(c *fiber.Ctx) error { + stats := cfg.Cache.CacheClient.ShowStats() + cfg.Logger.Debug("Getting cache stats via API", map[string]interface{}{"stats": stats}) + c.JSON(stats) + return nil +} + type apiBanUserRequest struct { UserID string `json:"user_id"` Reason string `json:"reason"` diff --git a/cache/cache.go b/cache/cache.go index 955146d..404e5e9 100644 --- a/cache/cache.go +++ b/cache/cache.go @@ -18,6 +18,8 @@ type Cache struct { globalTTL time.Duration compressPool sync.Pool decompressPool sync.Pool + cacheHits int + cacheMisses int sync.RWMutex // Reintroduced to provide lock methods } @@ -75,14 +77,16 @@ func (c *Cache) Get(key string) ([]byte, bool) { entry, ok := c.entries.Load(key) if !ok || entry.(CacheEntry).ExpiresAt.Before(time.Now()) { + c.cacheMisses++ return nil, false } compressedValue := entry.(CacheEntry).Value value, err := c.decompress(compressedValue) if err != nil { + c.cacheMisses++ return nil, false } - + c.cacheHits++ return value, true } @@ -109,6 +113,34 @@ func (c *Cache) CleanExpiredEntries() { }) } +type CacheStats struct { + CachedQueries int `json:"cached_queries"` + CacheHits int `json:"cache_hits"` + CacheMisses int `json:"cache_misses"` +} + +func (c *Cache) ShowStats() CacheStats { + c.RLock() + defer c.RUnlock() + var count int + c.entries.Range(func(_, _ interface{}) bool { + count++ + return true + }) + cs := CacheStats{ + CachedQueries: count, + CacheHits: c.cacheHits, + CacheMisses: c.cacheMisses, + } + return cs +} + +func (c *Cache) ClearCache() { + c.cacheHits = 0 + c.cacheMisses = 0 + c.entries = sync.Map{} +} + func (c *Cache) compress(data []byte) ([]byte, error) { w := c.compressPool.Get().(*gzip.Writer) defer c.compressPool.Put(w) diff --git a/cache/cache_bench_test.go b/cache/cache_bench_test.go new file mode 100644 index 0000000..4d805a9 --- /dev/null +++ b/cache/cache_bench_test.go @@ -0,0 +1,54 @@ +package libpack_cache + +import ( + "testing" + "time" +) + +// Assume that New function initializes the cache and it is defined somewhere in the libpack_cache package. + +func BenchmarkCacheSet(b *testing.B) { + cache := New(30 * time.Second) // Initializing the cache with a TTL of 30 seconds + key := "benchmark-key" + value := []byte("benchmark-value") + + b.ResetTimer() // Reset the timer to exclude the setup time from the benchmark + + for i := 0; i < b.N; i++ { + cache.Set(key, value, 5*time.Second) + } +} + +func BenchmarkCacheGet(b *testing.B) { + cache := New(30 * time.Second) // Initializing the cache + key := "benchmark-key" + value := []byte("benchmark-value") + cache.Set(key, value, 5*time.Second) // Pre-set a value to retrieve + + b.ResetTimer() // Start timing + + for i := 0; i < b.N; i++ { + _, _ = cache.Get(key) + } +} + +func BenchmarkCacheExpire(b *testing.B) { + key := "benchmark-expire-key" + value := []byte("benchmark-value") + ttl := 5 * time.Millisecond // Setting a short TTL for quick expiration + + for i := 0; i < b.N; i++ { + cache := New(30 * time.Second) + cache.Set(key, value, ttl) + time.Sleep(ttl) // Wait for the key to expire + _, _ = cache.Get(key) + } +} + +func BenchmarkCacheStats(b *testing.B) { + cache := New(30 * time.Second) // Initializing the cache + key := "benchmark-key" + value := []byte("benchmark-value") + cache.Set(key, value, 5*time.Second) // Pre-set a value to retrieve + cache.Get(key) +} diff --git a/cache/cache_test.go b/cache/cache_test.go index b0e2cc7..8714742 100644 --- a/cache/cache_test.go +++ b/cache/cache_test.go @@ -110,3 +110,36 @@ func (suite *CacheTestSuite) Test_CacheExpire() { }) } } + +func (suite *CacheTestSuite) Test_CacheStats() { + cache := New(30 * time.Second) + tests := []struct { + name string + cache_value string + ttl time.Duration + }{ + { + name: "test1", + cache_value: "test1-123", + ttl: 2 * time.Second, + }, + { + name: "test2", + cache_value: "test2-123", + ttl: 5 * time.Second, + }, + } + for _, tt := range tests { + suite.T().Run(tt.name, func(t *testing.T) { + cache.Set(tt.name, []byte(tt.name), tt.ttl) + c, ok := cache.Get(tt.name) + suite.Equal(true, ok) + suite.Equal(tt.name, string(c)) + }) + } + cache.Get("non-existent-non-cached-key") + stats := cache.ShowStats() + suite.Equal(2, stats.CacheHits, "CacheHits") + suite.Equal(1, stats.CacheMisses, "CacheMisses") + suite.Equal(2, stats.CachedQueries, "CachedQueries") +}