From 5d19608fa29122997cd264ede4ff8fb7bbd8e8fe Mon Sep 17 00:00:00 2001 From: e11sy <130844513+e11sy@users.noreply.github.com> Date: Sat, 24 Jan 2026 19:53:54 +0300 Subject: [PATCH 1/5] feat(redis): do not update rate limit counters --- pkg/redis/client.go | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/pkg/redis/client.go b/pkg/redis/client.go index 15feb6a..e2e08ab 100644 --- a/pkg/redis/client.go +++ b/pkg/redis/client.go @@ -191,10 +191,10 @@ func (r *RedisClient) UpdateRateLimit(projectID string, eventsLimit int64, event local limit = tonumber(ARGV[3]) local period = tonumber(ARGV[4]) + -- Read current window value local current = redis.call('HGET', key, field) if not current then - -- No existing record, create new window - redis.call('HSET', key, field, now .. ':1') + -- No existing record, event count is within limit return 1 end @@ -202,21 +202,17 @@ func (r *RedisClient) UpdateRateLimit(projectID string, eventsLimit int64, event timestamp = tonumber(timestamp) count = tonumber(count) - -- Check if we're in a new time window + -- If we're in a new time window - event count is within limit if now - timestamp >= period then - -- Reset for new window - redis.call('HSET', key, field, now .. ':1') return 1 end - -- Check if incrementing would exceed limit - if count + 1 > limit then - return 0 + -- Still in current window: check if event count is within limit + if count < limit then + return 1 end - -- Increment counter - redis.call('HSET', key, field, timestamp .. ':' .. (count + 1)) - return 1 + return 0 ` // Run the script From 9ff2cd9de65791964c517a8fa0c2e73e36b866df Mon Sep 17 00:00:00 2001 From: e11sy <130844513+e11sy@users.noreply.github.com> Date: Sat, 24 Jan 2026 20:49:41 +0300 Subject: [PATCH 2/5] chore(): update client test and rename the method of the client --- pkg/redis/client.go | 4 +-- pkg/redis/client_test.go | 41 ++++++++++++---------- pkg/server/errorshandler/handler.go | 2 +- pkg/server/errorshandler/handler_sentry.go | 2 +- 4 files changed, 26 insertions(+), 23 deletions(-) diff --git a/pkg/redis/client.go b/pkg/redis/client.go index e2e08ab..8ad5583 100644 --- a/pkg/redis/client.go +++ b/pkg/redis/client.go @@ -176,8 +176,8 @@ func (r *RedisClient) CheckAvailability() bool { return pong == "PONG" } -// UpdateRateLimit checks and updates the rate limit for a project using a Lua script -func (r *RedisClient) UpdateRateLimit(projectID string, eventsLimit int64, eventsPeriod int64) (bool, error) { +// CheckRateLimit checks and updates the rate limit for a project using a Lua script +func (r *RedisClient) CheckRateLimit(projectID string, eventsLimit int64, eventsPeriod int64) (bool, error) { // If eventsLimit is 0, we don't need to update the rate limit if eventsLimit == 0 { return true, nil diff --git a/pkg/redis/client_test.go b/pkg/redis/client_test.go index 2a11d2b..3e4e75e 100644 --- a/pkg/redis/client_test.go +++ b/pkg/redis/client_test.go @@ -29,7 +29,7 @@ func setupTestRedis(t *testing.T) (*RedisClient, *miniredis.Miniredis) { return client, mr } -func TestUpdateRateLimit(t *testing.T) { +func TestCheckRateLimit(t *testing.T) { client, mr := setupTestRedis(t) defer mr.Close() @@ -79,7 +79,7 @@ func TestUpdateRateLimit(t *testing.T) { wantErr: false, }, { - name: "should reset count after period expires", + name: "should ignore existing counter (treat as allowed) after period expires", projectID: "project4", eventsLimit: 5, eventsPeriod: 60, @@ -101,12 +101,16 @@ func TestUpdateRateLimit(t *testing.T) { wantErr: false, }, { - name: "should handle multiple calls up to limit", + name: "should fail if limit is already reached", projectID: "project6", eventsLimit: 3, eventsPeriod: 60, - calls: 4, - wantAllowed: false, // Last call should be denied + setup: func() { + client.rdb.HSet(client.ctx, "rate_limits", "project6", + fmt.Sprintf("%d:%d", time.Now().Unix(), 3)) + }, + calls: 4, + wantAllowed: false, wantErr: false, }, } @@ -123,7 +127,7 @@ func TestUpdateRateLimit(t *testing.T) { // Make the specified number of calls for i := 0; i < tt.calls; i++ { - lastAllowed, lastErr = client.UpdateRateLimit(tt.projectID, tt.eventsLimit, tt.eventsPeriod) + lastAllowed, lastErr = client.CheckRateLimit(tt.projectID, tt.eventsLimit, tt.eventsPeriod) } if tt.wantErr { @@ -136,7 +140,7 @@ func TestUpdateRateLimit(t *testing.T) { } } -func TestUpdateRateLimitConcurrent(t *testing.T) { +func TestCheckRateLimitConcurrent(t *testing.T) { client, mr := setupTestRedis(t) defer mr.Close() @@ -148,7 +152,12 @@ func TestUpdateRateLimitConcurrent(t *testing.T) { callsPerRoutine = 20 ) - var rejectedCount int = 0 + initialValue := fmt.Sprintf("%d:%d", time.Now().Unix(), eventsLimit) + if err := client.rdb.HSet(client.ctx, "rate_limits", projectID, initialValue).Err(); err != nil { + t.Fatalf("failed to seed rate limit: %v", err) + } + + var rejectedCount int := 0 done := make(chan bool) @@ -156,7 +165,7 @@ func TestUpdateRateLimitConcurrent(t *testing.T) { for i := 0; i < goroutines; i++ { go func() { for j := 0; j < callsPerRoutine; j++ { - allowed, err := client.UpdateRateLimit(projectID, eventsLimit, eventsPeriod) + allowed, err := client.CheckRateLimit(projectID, eventsLimit, eventsPeriod) assert.NoError(t, err) if !allowed { rejectedCount++ @@ -171,17 +180,11 @@ func TestUpdateRateLimitConcurrent(t *testing.T) { <-done } - // Verify the total number of successful updates doesn't exceed the limit + // Verify the stored value remains unchanged and all checks are denied val, err := client.rdb.HGet(client.ctx, "rate_limits", projectID).Result() assert.NoError(t, err) - assert.NotEmpty(t, val) - - // The total count should not exceed the events limit - count := 0 - _, err = fmt.Sscanf(val, "%d:%d", &count, &count) - assert.NoError(t, err) - assert.Equal(t, count, eventsLimit) - assert.Equal(t, rejectedCount, goroutines*callsPerRoutine-eventsLimit) - t.Logf("count: %d", count) + assert.Equal(t, initialValue, val) + assert.Equal(t, int64(goroutines*callsPerRoutine), rejectedCount) + t.Logf("stored value: %s", val) t.Logf("rejectedCount: %d", rejectedCount) } diff --git a/pkg/server/errorshandler/handler.go b/pkg/server/errorshandler/handler.go index 3454a89..8032787 100644 --- a/pkg/server/errorshandler/handler.go +++ b/pkg/server/errorshandler/handler.go @@ -77,7 +77,7 @@ func (handler *Handler) process(body []byte) ResponseMessage { return ResponseMessage{402, true, "Project has exceeded the events limit"} } - rateWithinLimit, err := handler.RedisClient.UpdateRateLimit(projectId, projectLimits.EventsLimit, projectLimits.EventsPeriod) + rateWithinLimit, err := handler.RedisClient.CheckRateLimit(projectId, projectLimits.EventsLimit, projectLimits.EventsPeriod) if err != nil { log.Errorf("Failed to update rate limit: %s", err) return ResponseMessage{402, true, "Failed to update rate limit"} diff --git a/pkg/server/errorshandler/handler_sentry.go b/pkg/server/errorshandler/handler_sentry.go index 16e9c58..5de16c9 100644 --- a/pkg/server/errorshandler/handler_sentry.go +++ b/pkg/server/errorshandler/handler_sentry.go @@ -107,7 +107,7 @@ func (handler *Handler) HandleSentry(ctx *fasthttp.RequestCtx) { return } - rateWithinLimit, err := handler.RedisClient.UpdateRateLimit(projectId, projectLimits.EventsLimit, projectLimits.EventsPeriod) + rateWithinLimit, err := handler.RedisClient.CheckRateLimit(projectId, projectLimits.EventsLimit, projectLimits.EventsPeriod) if err != nil { log.Errorf("Failed to update rate limit: %s", err) sendAnswerHTTP(ctx, ResponseMessage{402, true, "Failed to update rate limit"}) From a399b21b21fb8b4792de3c848e4533ac4325f244 Mon Sep 17 00:00:00 2001 From: e11sy <130844513+e11sy@users.noreply.github.com> Date: Sat, 24 Jan 2026 20:53:35 +0300 Subject: [PATCH 3/5] chore(): typo --- pkg/redis/client_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/redis/client_test.go b/pkg/redis/client_test.go index 3e4e75e..dcc9a3d 100644 --- a/pkg/redis/client_test.go +++ b/pkg/redis/client_test.go @@ -157,7 +157,7 @@ func TestCheckRateLimitConcurrent(t *testing.T) { t.Fatalf("failed to seed rate limit: %v", err) } - var rejectedCount int := 0 + var rejectedCount int = 0 done := make(chan bool) From 51b3aa8a9935e96ace9299f1c29b8ee41911664f Mon Sep 17 00:00:00 2001 From: e11sy <130844513+e11sy@users.noreply.github.com> Date: Sat, 24 Jan 2026 20:56:04 +0300 Subject: [PATCH 4/5] chore(): test type assertion mismatch --- pkg/redis/client_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/redis/client_test.go b/pkg/redis/client_test.go index dcc9a3d..8f3ffb2 100644 --- a/pkg/redis/client_test.go +++ b/pkg/redis/client_test.go @@ -157,7 +157,7 @@ func TestCheckRateLimitConcurrent(t *testing.T) { t.Fatalf("failed to seed rate limit: %v", err) } - var rejectedCount int = 0 + var rejectedCount int64 = 0 done := make(chan bool) From 03bd5204e20ef84a82aac7f9ca6d9d1e263f745e Mon Sep 17 00:00:00 2001 From: e11sy <130844513+e11sy@users.noreply.github.com> Date: Sat, 7 Feb 2026 17:54:22 +0300 Subject: [PATCH 5/5] chore(): change redis time-series key --- pkg/server/errorshandler/handler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/server/errorshandler/handler.go b/pkg/server/errorshandler/handler.go index 8032787..fdf15ed 100644 --- a/pkg/server/errorshandler/handler.go +++ b/pkg/server/errorshandler/handler.go @@ -134,7 +134,7 @@ func GetQueueCache(nonDefaultQueues []string) map[string]bool { // getTimeSeriesKey generates a Redis TimeSeries key for project metrics func getTimeSeriesKey(projectId, metricType, granularity string) string { - return fmt.Sprintf("ts:project-%s:%s:%s", metricType, projectId, granularity) + return fmt.Sprintf("ts:collected-project-%s:%s:%s", metricType, projectId, granularity) } // recordProjectMetrics records project metrics to Redis TimeSeries