diff --git a/README.md b/README.md index 72d1972..9f69e63 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ This project is in active use by [telegram-bot.app](https://telegram-bot.app), a - [Blocking introspection](#blocking-introspection) - [API endpoints](#api-endpoints) - [Ban or unban the user](#ban-or-unban-the-user) + - [Cache operations](#cache-operations) - [General](#general) - [Metrics which matter](#metrics-which-matter) - [Healthcheck](#healthcheck) @@ -236,6 +237,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") +} diff --git a/go.mod b/go.mod index c4c6523..a246c62 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/VictoriaMetrics/metrics v1.33.1 github.com/avast/retry-go/v4 v4.6.0 github.com/goccy/go-json v0.10.2 + github.com/gofiber/fiber v1.14.6 github.com/gofiber/fiber/v2 v2.52.4 github.com/gofrs/flock v0.8.1 github.com/google/uuid v1.6.0 @@ -22,7 +23,9 @@ require ( require ( github.com/andybalholm/brotli v1.1.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/gofiber/utils v0.0.10 // indirect github.com/gookit/color v1.5.4 // indirect + github.com/gorilla/schema v1.1.0 // indirect github.com/klauspost/compress v1.17.8 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect diff --git a/go.sum b/go.sum index 8067eef..2935dff 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,6 @@ github.com/VictoriaMetrics/metrics v1.33.1 h1:CNV3tfm2Kpv7Y9W3ohmvqgFWPR55tV2c7M2U6OIo+UM= github.com/VictoriaMetrics/metrics v1.33.1/go.mod h1:r7hveu6xMdUACXvB8TYdAj8WEsKzWB0EkpJN+RDtOf8= +github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/avast/retry-go/v4 v4.6.0 h1:K9xNA+KeB8HHc2aWFuLb25Offp+0iVRXEvFx8IinRJA= @@ -11,8 +12,12 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gofiber/fiber v1.14.6 h1:QRUPvPmr8ijQuGo1MgupHBn8E+wW0IKqiOvIZPtV70o= +github.com/gofiber/fiber v1.14.6/go.mod h1:Yw2ekF1YDPreO9V6TMYjynu94xRxZBdaa8X5HhHsjCM= github.com/gofiber/fiber/v2 v2.52.4 h1:P+T+4iK7VaqUsq2PALYEfBBo6bJZ4q3FP8cZ84EggTM= github.com/gofiber/fiber/v2 v2.52.4/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= +github.com/gofiber/utils v0.0.10 h1:3Mr7X7JdCUo7CWf/i5sajSaDmArEDtti8bM1JUVso2U= +github.com/gofiber/utils v0.0.10/go.mod h1:9J5aHFUIjq0XfknT4+hdSMG6/jzfaAgCu4HEbWDeBlo= github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -21,8 +26,11 @@ github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= github.com/gookit/goutil v0.6.15 h1:mMQ0ElojNZoyPD0eVROk5QXJPh2uKR4g06slgPDF5Jo= github.com/gookit/goutil v0.6.15/go.mod h1:qdKdYEHQdEtyH+4fNdQNZfJHhI0jUZzHxQVAV3DaMDY= +github.com/gorilla/schema v1.1.0 h1:CamqUDOFUBqzrvxuz2vEwo8+SUdwsluFh7IlzJh30LY= +github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= github.com/graphql-go/graphql v0.8.1 h1:p7/Ou/WpmulocJeEx7wjQy611rtXGQaAcXGqanuMMgc= github.com/graphql-go/graphql v0.8.1/go.mod h1:nKiHzRM0qopJEwCITUuIsxk9PlVlwIiiI8pnJEhordQ= +github.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -38,8 +46,10 @@ github.com/lukaszraczylo/go-ratecounter v0.1.8 h1:ZYm6Wkn58ZAlFWRmC7PaD4oAYHWcu8 github.com/lukaszraczylo/go-ratecounter v0.1.8/go.mod h1:TqXEOCtFJStk1i0tkipprv1kiDHGon1MVUisjSTBSKM= github.com/lukaszraczylo/go-simple-graphql v1.2.14 h1:Dth+yZ+1ialCpnslSb6UgHbXszExjDUu/I95QZbnWVU= github.com/lukaszraczylo/go-simple-graphql v1.2.14/go.mod h1:pSKmm9OLGoS9pjmIvhBB/fo0+LganRrL29CN3fdkRPw= +github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -63,22 +73,31 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.16.0/go.mod h1:YOKImeEosDdBPnxc0gy7INqi3m1zK6A+xl6TwOBhHCA= github.com/valyala/fasthttp v1.53.0 h1:lW/+SUkOxCx2vlIu0iaImv4JLrVRnbbkpCoaawvA4zc= github.com/valyala/fasthttp v1.53.0/go.mod h1:6dt4/8olwq9QARP/TDuPmWyWcl4byhpvTJ4AAtcz+QM= github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G8= github.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ= github.com/valyala/histogram v1.2.0 h1:wyYGAZZt3CpwUiIb9AU/Zbllg1llXyrtApRS815OLoQ= github.com/valyala/histogram v1.2.0/go.mod h1:Hb4kBwb4UxsaNbbbh+RRz8ZR6pdodR57tzWUS3BUzXY= +github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -86,6 +105,7 @@ golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/main.go b/main.go index c6e026f..a33681a 100644 --- a/main.go +++ b/main.go @@ -72,7 +72,7 @@ func parseConfig() { proxy.WithClient(c.Client.FastProxyClient) // setting the global proxy client here instead of per request c.Server.EnableApi = getDetailsFromEnv("ENABLE_API", false) c.Server.ApiPort = getDetailsFromEnv("API_PORT", 9090) - c.Api.BannedUsersFile = getDetailsFromEnv("BANNED_USERS_FILE", "/go/src/app/banned_users.json") + c.Api.BannedUsersFile = getDetailsFromEnv("BANNED_USERS_FILE", "/app/banned_users.json") c.Server.PurgeOnCrawl = getDetailsFromEnv("PURGE_METRICS_ON_CRAWL", false) c.Server.PurgeEvery = getDetailsFromEnv("PURGE_METRICS_ON_TIMER", 0) cfg = &c diff --git a/main_test.go b/main_test.go index 458e976..66fcab7 100644 --- a/main_test.go +++ b/main_test.go @@ -33,6 +33,7 @@ func (suite *Tests) SetupTest() { }, ) parseConfig() + enableApi() StartMonitoringServer() cfg.Logger = libpack_logging.NewLogger() // Setup environment variables here if needed