@@ -18,20 +18,20 @@ package v3
1818import (
1919 "errors"
2020 "fmt"
21- "strings"
2221 "time"
2322
2423 "github.com/bucketeer-io/bucketeer/pkg/cache"
2524 userproto "github.com/bucketeer-io/bucketeer/proto/user"
2625)
2726
2827const (
29- userAttributeKind = "user_attr"
30- userAttributesMaxSize = int64 (100 )
28+ userAttributeKind = "user_attr"
3129)
3230
3331type UserAttributesCache interface {
34- GetUserAttributeKeyAll (environmentId string ) ([]string , error )
32+ // Returns all attribute keys for the given environment.
33+ GetUserAttributeKeyAll (environmentID string ) ([]string , error )
34+ // Stores values for each attribute key under the given TTL.
3535 Put (userAttributes * userproto.UserAttributes , ttl time.Duration ) error
3636}
3737
@@ -43,68 +43,57 @@ func NewUserAttributesCache(c cache.MultiGetDeleteCountCache) UserAttributesCach
4343 return & userAttributesCache {cache : c }
4444}
4545
46- func (u * userAttributesCache ) GetUserAttributeKeyAll (environmentId string ) ([]string , error ) {
47- scanKey := u .key (environmentId ) + ":*"
48- var cursor uint64
49- var allKeys []string
50-
51- for {
52- var keys []string
53- var err error
54- cursor , keys , err = u .cache .Scan (cursor , scanKey , userAttributesMaxSize )
55- if err != nil {
56- return nil , err
57- }
58- allKeys = append (allKeys , keys ... )
59- if cursor == 0 {
60- break
61- }
62- }
63-
64- // Extract UserAttributeKey from the full keys
65- // Key format: environmentId:user_attr:attributeKey
66- attributeKeys := u .extractAttributeKeys (allKeys )
67- return attributeKeys , nil
68- }
69-
70- func (u * userAttributesCache ) extractAttributeKeys (fullKeys []string ) []string {
71- attributeKeys := []string {}
72- for _ , fullKey := range fullKeys {
73- // Split by ":" and get the last part which is the attribute key
74- parts := strings .Split (fullKey , ":" )
75- for i , part := range parts {
76- // If userAttrKindIndex is found, use the next element as the key
77- if part == userAttributeKind && i + 1 < len (parts ) {
78- attributeKeys = append (attributeKeys , parts [i + 1 ])
79- break
80- }
81- }
82- }
83- return attributeKeys
46+ // key returns the base prefix for all user_attr entries,
47+ // e.g. "env123:user_attr"
48+ func (u * userAttributesCache ) key (environmentID string ) string {
49+ return fmt .Sprintf ("%s:%s" , environmentID , userAttributeKind )
8450}
8551
86- func (u * userAttributesCache ) Put (userAttributes * userproto.UserAttributes , ttl time.Duration ) error {
52+ // Put writes each attribute's values into its own Set, and also
53+ // adds the attribute.Key to an index Set so we can list them later.
54+ func (u * userAttributesCache ) Put (
55+ userAttributes * userproto.UserAttributes ,
56+ ttl time.Duration ,
57+ ) error {
8758 if userAttributes == nil {
88- return errors .New ("user attributes is nil" )
59+ return errors .New ("userAttributes cannot be nil" )
8960 }
61+
9062 pipe := u .cache .Pipeline (false )
63+ // indexKey holds the list of all attribute keys
64+ indexKey := u .key (userAttributes .EnvironmentId ) + ":keys"
65+
9166 for _ , attribute := range userAttributes .UserAttributes {
92- key := u .key (userAttributes .EnvironmentId ) + ":" + attribute .Key
93- for _ , value := range attribute .Values {
94- pipe .SAdd (key , value )
67+ // 1) Store attribute values in their own set: env:user_attr:country -> ["US", "JP"]
68+ attrKey := fmt .Sprintf ("%s:%s" , u .key (userAttributes .EnvironmentId ), attribute .Key )
69+ // convert []string → []interface{} for SAdd
70+ members := make ([]interface {}, len (attribute .Values ))
71+ for i , v := range attribute .Values {
72+ members [i ] = v
9573 }
96- pipe .Expire (key , ttl )
74+ // SAdd can add multiple values at once
75+ pipe .SAdd (attrKey , members ... )
76+ pipe .Expire (attrKey , ttl )
77+
78+ // 2) Store attribute key in index set: env:user_attr:keys -> ["country", "plan_type"]
79+ // This avoids scanning Redis keys and works efficiently with Redis clusters
80+ pipe .SAdd (indexKey , attribute .Key )
81+ pipe .Expire (indexKey , ttl )
9782 }
83+
9884 _ , err := pipe .Exec ()
99- if err != nil {
100- return err
101- }
102- return nil
85+ return err
10386}
10487
105- // We use a Redis Cluster “hash tag” so all user_attr keys for an environment
106- // live in the same slot. On standalone mode the `{…}` is just a literal.
107- // The key format is: {environmentId}:user_attr:{attributeKey}
108- func (u * userAttributesCache ) key (environmentId string ) string {
109- return fmt .Sprintf ("{%s}:%s" , environmentId , userAttributeKind )
88+ // GetUserAttributeKeyAll fetches the complete list of attribute keys
89+ // via a single SMEMBERS call on the index Set.
90+ func (u * userAttributesCache ) GetUserAttributeKeyAll (
91+ environmentID string ,
92+ ) ([]string , error ) {
93+ indexKey := u .key (environmentID ) + ":keys"
94+ keys , err := u .cache .SMembers (indexKey )
95+ if err != nil {
96+ return nil , err
97+ }
98+ return keys , nil
11099}
0 commit comments