@@ -6,8 +6,10 @@ import (
66 "slices"
77 "strconv"
88 "strings"
9+ "sync"
910 "time"
1011
12+ sw "github.com/RussellLuo/slidingwindow"
1113 "github.com/aws/smithy-go/ptr"
1214 "github.com/dominikbraun/graph"
1315 jsonpatch "github.com/evanphx/json-patch"
@@ -29,9 +31,16 @@ import (
2931 "github.com/samber/lo"
3032 "gorm.io/gorm"
3133 "gorm.io/gorm/clause"
34+
35+ "github.com/flanksource/config-db/pkg/ratelimit"
3236)
3337
34- const configItemsBulkInsertSize = 200
38+ const (
39+ configItemsBulkInsertSize = 200
40+
41+ rateLimitWindow = time .Hour * 4
42+ maxChangesInWindow = 100
43+ )
3544
3645func deleteChangeHandler (ctx api.ScrapeContext , change v1.ChangeResult ) error {
3746 var deletedAt interface {}
@@ -325,6 +334,83 @@ func UpdateAnalysisStatusBefore(ctx api.ScrapeContext, before time.Time, scraper
325334 Error
326335}
327336
337+ var muMap = sync.Map {}
338+ var configMap = map [string ]* sw.Limiter {}
339+
340+ func rateLimitChanges (ctx api.ScrapeContext , newChanges []* models.ConfigChange ) ([]* models.ConfigChange , error ) {
341+ if len (newChanges ) == 0 {
342+ return nil , nil
343+ }
344+
345+ lock , loaded := muMap .LoadOrStore (ctx .ScrapeConfig ().GetPersistedID (), & sync.Mutex {})
346+ lock .(* sync.Mutex ).Lock ()
347+ defer lock .(* sync.Mutex ).Unlock ()
348+
349+ window := ctx .Properties ().Duration ("changes.max.window" , rateLimitWindow )
350+ max := ctx .Properties ().Int ("changes.max.count" , maxChangesInWindow )
351+
352+ if ! loaded {
353+ // populate the rate limit window for the scraper
354+ query := "SELECT config_id, COUNT(*), min(created_at) FROM config_changes WHERE NOW() - created_at <= ? GROUP BY config_id"
355+ rows , err := ctx .DB ().Raw (query , window ).Rows ()
356+ if err != nil {
357+ return nil , err
358+ }
359+
360+ for rows .Next () {
361+ var configID string
362+ var count int
363+ var earliest time.Time
364+ if err := rows .Scan (& configID , & count , & earliest ); err != nil {
365+ return nil , err
366+ }
367+
368+ rateLimiter , _ := sw .NewLimiter (window , int64 (max ), func () (sw.Window , sw.StopFunc ) {
369+ win , stopper := ratelimit .NewLocalWindow ()
370+ if count > 0 {
371+ win .SetStart (earliest )
372+ win .AddCount (int64 (count ))
373+ }
374+ return win , stopper
375+ })
376+ configMap [configID ] = rateLimiter
377+ }
378+ }
379+
380+ passingNewChanges := make ([]* models.ConfigChange , 0 , len (newChanges ))
381+ rateLimited := map [string ]struct {}{}
382+ for _ , change := range newChanges {
383+ rateLimiter , ok := configMap [change .ConfigID ]
384+ if ! ok {
385+ rl , _ := sw .NewLimiter (window , int64 (max ), func () (sw.Window , sw.StopFunc ) {
386+ return sw .NewLocalWindow ()
387+ })
388+ configMap [change .ConfigID ] = rl
389+ rateLimiter = rl
390+ }
391+
392+ if ! rateLimiter .Allow () {
393+ ctx .Logger .V (2 ).Infof ("change rate limited (config=%s)" , change .ConfigID )
394+ rateLimited [change .ConfigID ] = struct {}{}
395+ continue
396+ }
397+
398+ passingNewChanges = append (passingNewChanges , change )
399+ }
400+
401+ // For all the rate limited configs, we add a new "TooManyChanges" change
402+ for configID := range rateLimited {
403+ passingNewChanges = append (passingNewChanges , & models.ConfigChange {
404+ ConfigID : configID ,
405+ Summary : "Changes on this config has been rate limited" ,
406+ ChangeType : "TooManyChanges" ,
407+ ExternalChangeId : uuid .New ().String (),
408+ })
409+ }
410+
411+ return passingNewChanges , nil
412+ }
413+
328414// SaveResults creates or update a configuration with config changes
329415func SaveResults (ctx api.ScrapeContext , results []v1.ScrapeResult ) error {
330416 startTime , err := GetCurrentDBTime (ctx )
@@ -352,6 +438,11 @@ func SaveResults(ctx api.ScrapeContext, results []v1.ScrapeResult) error {
352438 }
353439 }
354440
441+ newChanges , err = rateLimitChanges (ctx , newChanges )
442+ if err != nil {
443+ return fmt .Errorf ("failed to rate limit changes: %w" , err )
444+ }
445+
355446 if err := ctx .DB ().CreateInBatches (newChanges , configItemsBulkInsertSize ).Error ; err != nil {
356447 return fmt .Errorf ("failed to create config changes: %w" , err )
357448 }
0 commit comments