Skip to content

Commit a7358dd

Browse files
committed
feat: rate limits on changes per config
1 parent 121f062 commit a7358dd

File tree

5 files changed

+156
-1
lines changed

5 files changed

+156
-1
lines changed

db/changes.go

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,21 @@
11
package db
22

3-
import "github.com/flanksource/config-db/api"
3+
import (
4+
"sync"
5+
"time"
6+
7+
sw "github.com/RussellLuo/slidingwindow"
8+
"github.com/google/uuid"
9+
10+
"github.com/flanksource/config-db/api"
11+
"github.com/flanksource/config-db/db/models"
12+
"github.com/flanksource/config-db/pkg/ratelimit"
13+
)
14+
15+
const (
16+
rateLimitWindow = time.Hour * 4
17+
maxChangesInWindow = 100
18+
)
419

520
func GetWorkflowRunCount(ctx api.ScrapeContext, workflowID string) (int64, error) {
621
var count int64
@@ -10,3 +25,84 @@ func GetWorkflowRunCount(ctx api.ScrapeContext, workflowID string) (int64, error
1025
Error
1126
return count, err
1227
}
28+
29+
var (
30+
scraperLocks = sync.Map{}
31+
configRateLimiters = map[string]*sw.Limiter{}
32+
)
33+
34+
func rateLimitChanges(ctx api.ScrapeContext, newChanges []*models.ConfigChange) ([]*models.ConfigChange, error) {
35+
if len(newChanges) == 0 {
36+
return nil, nil
37+
}
38+
39+
lock, loaded := scraperLocks.LoadOrStore(ctx.ScrapeConfig().GetPersistedID(), &sync.Mutex{})
40+
lock.(*sync.Mutex).Lock()
41+
defer lock.(*sync.Mutex).Unlock()
42+
43+
window := ctx.Properties().Duration("changes.max.window", rateLimitWindow)
44+
max := ctx.Properties().Int("changes.max.count", maxChangesInWindow)
45+
46+
if !loaded {
47+
// populate the rate limit window for the scraper
48+
query := `SELECT config_id, COUNT(*), min(created_at) FROM config_changes
49+
WHERE change_type != 'TooManyChanges'
50+
AND NOW() - created_at <= ? GROUP BY config_id`
51+
rows, err := ctx.DB().Raw(query, window).Rows()
52+
if err != nil {
53+
return nil, err
54+
}
55+
56+
for rows.Next() {
57+
var configID string
58+
var count int
59+
var earliest time.Time
60+
if err := rows.Scan(&configID, &count, &earliest); err != nil {
61+
return nil, err
62+
}
63+
64+
rateLimiter, _ := sw.NewLimiter(window, int64(max), func() (sw.Window, sw.StopFunc) {
65+
win, stopper := ratelimit.NewLocalWindow()
66+
if count > 0 {
67+
win.SetStart(earliest)
68+
win.AddCount(int64(count))
69+
}
70+
return win, stopper
71+
})
72+
configRateLimiters[configID] = rateLimiter
73+
}
74+
}
75+
76+
passingNewChanges := make([]*models.ConfigChange, 0, len(newChanges))
77+
rateLimited := map[string]struct{}{}
78+
for _, change := range newChanges {
79+
rateLimiter, ok := configRateLimiters[change.ConfigID]
80+
if !ok {
81+
rl, _ := sw.NewLimiter(window, int64(max), func() (sw.Window, sw.StopFunc) {
82+
return sw.NewLocalWindow()
83+
})
84+
configRateLimiters[change.ConfigID] = rl
85+
rateLimiter = rl
86+
}
87+
88+
if !rateLimiter.Allow() {
89+
ctx.Logger.V(2).Infof("change rate limited (config=%s)", change.ConfigID)
90+
rateLimited[change.ConfigID] = struct{}{}
91+
continue
92+
}
93+
94+
passingNewChanges = append(passingNewChanges, change)
95+
}
96+
97+
// For all the rate limited configs, we add a new "TooManyChanges" change
98+
for configID := range rateLimited {
99+
passingNewChanges = append(passingNewChanges, &models.ConfigChange{
100+
ConfigID: configID,
101+
Summary: "Changes on this config has been rate limited",
102+
ChangeType: "TooManyChanges",
103+
ExternalChangeId: uuid.New().String(),
104+
})
105+
}
106+
107+
return passingNewChanges, nil
108+
}

db/update.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,11 @@ func SaveResults(ctx api.ScrapeContext, results []v1.ScrapeResult) error {
352352
}
353353
}
354354

355+
newChanges, err = rateLimitChanges(ctx, newChanges)
356+
if err != nil {
357+
return fmt.Errorf("failed to rate limit changes: %w", err)
358+
}
359+
355360
if err := ctx.DB().CreateInBatches(newChanges, configItemsBulkInsertSize).Error; err != nil {
356361
return fmt.Errorf("failed to create config changes: %w", err)
357362
}

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ require (
1919
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/subscription/armsubscription v1.1.0
2020
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/trafficmanager/armtrafficmanager v1.0.0
2121
github.com/Jeffail/gabs/v2 v2.7.0
22+
github.com/RussellLuo/slidingwindow v0.0.0-20200528002341-535bb99d338b
2223
github.com/aws/aws-sdk-go-v2 v1.18.0
2324
github.com/aws/aws-sdk-go-v2/config v1.18.25
2425
github.com/aws/aws-sdk-go-v2/credentials v1.13.24
@@ -117,6 +118,7 @@ require (
117118
github.com/ghodss/yaml v1.0.0 // indirect
118119
github.com/go-logr/stdr v1.2.2 // indirect
119120
github.com/go-openapi/inflect v0.19.0 // indirect
121+
github.com/go-redis/redis v6.15.9+incompatible // indirect
120122
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
121123
github.com/golang-jwt/jwt/v5 v5.0.0 // indirect
122124
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -672,6 +672,8 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdko
672672
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
673673
github.com/RaveNoX/go-jsonmerge v1.0.0 h1:2e0nqnadoGUP8rAvcA0hkQelZreVO5X3BHomT2XMrAk=
674674
github.com/RaveNoX/go-jsonmerge v1.0.0/go.mod h1:qYM/NA77LhO4h51JJM7Z+xBU3ovqrNIACZe+SkSNVFo=
675+
github.com/RussellLuo/slidingwindow v0.0.0-20200528002341-535bb99d338b h1:5/++qT1/z812ZqBvqQt6ToRswSuPZ/B33m6xVHRzADU=
676+
github.com/RussellLuo/slidingwindow v0.0.0-20200528002341-535bb99d338b/go.mod h1:4+EPqMRApwwE/6yo6CxiHoSnBzjRr3jsqer7frxP8y4=
675677
github.com/TomOnTime/utfutil v0.0.0-20210710122150-437f72b26edf h1:+GdVyvpzTy3UFAS1+hbTqm9Mk0U1Xrocm28s/E2GWz0=
676678
github.com/TomOnTime/utfutil v0.0.0-20210710122150-437f72b26edf/go.mod h1:FiuynIwe98RFhWI8nZ0dnsldPVsBy9rHH1hn2WYwme4=
677679
github.com/WinterYukky/gorm-extra-clause-plugin v0.2.0 h1:s1jobT8PlSyG/FXczfoGSt4r46iPiT4ZShe35k5/2y4=
@@ -926,6 +928,8 @@ github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogB
926928
github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
927929
github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M=
928930
github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M=
931+
github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
932+
github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
929933
github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
930934
github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
931935
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=

pkg/ratelimit/ratelimit.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package ratelimit
2+
3+
import (
4+
"time"
5+
6+
sw "github.com/RussellLuo/slidingwindow"
7+
)
8+
9+
// LocalWindow represents a window that ignores sync behavior entirely
10+
// and only stores counters in memory.
11+
//
12+
// NOTE: It's an exact copy of the LocalWindow provided by RussellLuo/slidingwindow
13+
// with an added capability of setting a custom start time.
14+
type LocalWindow struct {
15+
// The start boundary (timestamp in nanoseconds) of the window.
16+
// [start, start + size)
17+
start int64
18+
19+
// The total count of events happened in the window.
20+
count int64
21+
}
22+
23+
func NewLocalWindow() (*LocalWindow, sw.StopFunc) {
24+
return &LocalWindow{}, func() {}
25+
}
26+
27+
func (w *LocalWindow) SetStart(s time.Time) {
28+
w.start = s.UnixNano()
29+
}
30+
31+
func (w *LocalWindow) Start() time.Time {
32+
return time.Unix(0, w.start)
33+
}
34+
35+
func (w *LocalWindow) Count() int64 {
36+
return w.count
37+
}
38+
39+
func (w *LocalWindow) AddCount(n int64) {
40+
w.count += n
41+
}
42+
43+
func (w *LocalWindow) Reset(s time.Time, c int64) {
44+
w.start = s.UnixNano()
45+
w.count = c
46+
}
47+
48+
func (w *LocalWindow) Sync(now time.Time) {}

0 commit comments

Comments
 (0)