@@ -11,6 +11,7 @@ import (
1111 "github.com/go-kit/log"
1212 "github.com/pkg/errors"
1313 "github.com/prometheus/client_golang/prometheus"
14+ promtestutil "github.com/prometheus/client_golang/prometheus/testutil"
1415 "github.com/prometheus/common/model"
1516 "github.com/prometheus/prometheus/model/rulefmt"
1617 "github.com/stretchr/testify/assert"
@@ -201,7 +202,7 @@ func TestLoadRules(t *testing.T) {
201202 // Load with missing rule groups fails.
202203 require .NoError (t , rs .DeleteRuleGroup (context .Background (), "user1" , "hello" , "first testGroup" ))
203204 _ , err = rs .LoadRuleGroups (context .Background (), allGroupsMap )
204- require .EqualError (t , err , "get rule group user=\" user2 \" , namespace=\" world \" , name=\" first testGroup\" : group does not exist" )
205+ require .EqualError (t , err , "get rule group user=\" user1 \" , namespace=\" hello \" , name=\" first testGroup\" : group does not exist" )
205206 })
206207}
207208
@@ -461,3 +462,101 @@ func (mb mockBucket) Iter(_ context.Context, dir string, f func(string) error, o
461462 }
462463 return nil
463464}
465+
466+ func TestLoadRuleGroupsCache (t * testing.T ) {
467+ bucketClient := objstore .NewInMemBucket ()
468+ reg := prometheus .NewPedanticRegistry ()
469+ usersScannerConfig := users.UsersScannerConfig {Strategy : users .UserScanStrategyList }
470+ bucketStore , err := NewBucketRuleStore (bucketClient , usersScannerConfig , nil , log .NewNopLogger (), reg )
471+ require .NoError (t , err )
472+
473+ // Setup: create a rule group.
474+ desc := rulespb .ToProto ("user1" , "ns" , rulefmt.RuleGroup {Name : "group1" , Interval : model .Duration (time .Minute )})
475+ require .NoError (t , bucketStore .SetRuleGroup (context .Background (), "user1" , "ns" , desc ))
476+
477+ allGroups , err := bucketStore .ListAllRuleGroups (context .Background ())
478+ require .NoError (t , err )
479+
480+ // First load: cold cache, should do full GET (miss).
481+ loaded , err := bucketStore .LoadRuleGroups (context .Background (), allGroups )
482+ require .NoError (t , err )
483+ require .Len (t , loaded ["user1" ], 1 )
484+ require .Equal (t , "group1" , loaded ["user1" ][0 ].Name )
485+
486+ // Second load: cache is warm, file unchanged → should be a cache hit.
487+ time .Sleep (10 * time .Millisecond ) // ensure downloadedAt is after LastModified
488+ loaded2 , err := bucketStore .LoadRuleGroups (context .Background (), allGroups )
489+ require .NoError (t , err )
490+ require .Len (t , loaded2 ["user1" ], 1 )
491+ require .Equal (t , "group1" , loaded2 ["user1" ][0 ].Name )
492+
493+ // Verify cache hit metric.
494+ hitCount := promtestutil .ToFloat64 (bucketStore .cacheOps .WithLabelValues ("hit" ))
495+ require .Equal (t , float64 (1 ), hitCount )
496+ }
497+
498+ func TestLoadRuleGroupsCacheMissOnModification (t * testing.T ) {
499+ bucketClient := objstore .NewInMemBucket ()
500+ reg := prometheus .NewPedanticRegistry ()
501+ usersScannerConfig := users.UsersScannerConfig {Strategy : users .UserScanStrategyList }
502+ bucketStore , err := NewBucketRuleStore (bucketClient , usersScannerConfig , nil , log .NewNopLogger (), reg )
503+ require .NoError (t , err )
504+
505+ // Setup: create a rule group.
506+ desc := rulespb .ToProto ("user1" , "ns" , rulefmt.RuleGroup {Name : "group1" , Interval : model .Duration (time .Minute )})
507+ require .NoError (t , bucketStore .SetRuleGroup (context .Background (), "user1" , "ns" , desc ))
508+
509+ allGroups , err := bucketStore .ListAllRuleGroups (context .Background ())
510+ require .NoError (t , err )
511+
512+ // First load: populates cache.
513+ _ , err = bucketStore .LoadRuleGroups (context .Background (), allGroups )
514+ require .NoError (t , err )
515+
516+ // Modify the rule group in S3.
517+ time .Sleep (10 * time .Millisecond )
518+ desc2 := rulespb .ToProto ("user1" , "ns" , rulefmt.RuleGroup {Name : "group1" , Interval : model .Duration (2 * time .Minute )})
519+ require .NoError (t , bucketStore .SetRuleGroup (context .Background (), "user1" , "ns" , desc2 ))
520+
521+ // Second load: file modified → cache miss → should get new content.
522+ loaded , err := bucketStore .LoadRuleGroups (context .Background (), allGroups )
523+ require .NoError (t , err )
524+ require .Equal (t , 2 * time .Minute , loaded ["user1" ][0 ].Interval )
525+ }
526+
527+ func TestLoadRuleGroupsCachePrune (t * testing.T ) {
528+ bucketClient := objstore .NewInMemBucket ()
529+ reg := prometheus .NewPedanticRegistry ()
530+ usersScannerConfig := users.UsersScannerConfig {Strategy : users .UserScanStrategyList }
531+ bucketStore , err := NewBucketRuleStore (bucketClient , usersScannerConfig , nil , log .NewNopLogger (), reg )
532+ require .NoError (t , err )
533+
534+ // Setup: create two rule groups.
535+ desc1 := rulespb .ToProto ("user1" , "ns" , rulefmt.RuleGroup {Name : "group1" , Interval : model .Duration (time .Minute )})
536+ desc2 := rulespb .ToProto ("user1" , "ns" , rulefmt.RuleGroup {Name : "group2" , Interval : model .Duration (time .Minute )})
537+ require .NoError (t , bucketStore .SetRuleGroup (context .Background (), "user1" , "ns" , desc1 ))
538+ require .NoError (t , bucketStore .SetRuleGroup (context .Background (), "user1" , "ns" , desc2 ))
539+
540+ allGroups , err := bucketStore .ListAllRuleGroups (context .Background ())
541+ require .NoError (t , err )
542+
543+ // Load both groups → cache has 2 entries.
544+ _ , err = bucketStore .LoadRuleGroups (context .Background (), allGroups )
545+ require .NoError (t , err )
546+
547+ bucketStore .ruleGroupCacheMu .RLock ()
548+ require .Len (t , bucketStore .ruleGroupCache , 2 )
549+ bucketStore .ruleGroupCacheMu .RUnlock ()
550+
551+ // Now load only group1 (simulating ring rebalance where group2 is no longer owned).
552+ partialGroups := map [string ]rulespb.RuleGroupList {
553+ "user1" : {allGroups ["user1" ][0 ]}, // only first group
554+ }
555+ _ , err = bucketStore .LoadRuleGroups (context .Background (), partialGroups )
556+ require .NoError (t , err )
557+
558+ // Cache should be pruned to 1 entry.
559+ bucketStore .ruleGroupCacheMu .RLock ()
560+ require .Len (t , bucketStore .ruleGroupCache , 1 )
561+ bucketStore .ruleGroupCacheMu .RUnlock ()
562+ }
0 commit comments