Skip to content

Commit 21a7324

Browse files
committed
chore: change to use smembers instead of scan
Signed-off-by: Alessandro Yuichi Okimoto <yuichijpn@gmail.com>
1 parent 8186895 commit 21a7324

9 files changed

Lines changed: 200 additions & 101 deletions

File tree

pkg/cache/cache.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ type PFGetter interface {
6060
type MultiGetter interface {
6161
GetMulti(keys interface{}, ignoreNotFound bool) ([]interface{}, error)
6262
Scan(cursor, key, count interface{}) (uint64, []string, error)
63+
SMembers(key string) ([]string, error)
6364
}
6465

6566
type Putter interface {

pkg/cache/mock/cache.go

Lines changed: 45 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/cache/testing/cache.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ func (c *inMemoryCache) Scan(cursor, key, count interface{}) (uint64, []string,
5959
return 0, nil, nil
6060
}
6161

62+
func (c *inMemoryCache) SMembers(key string) ([]string, error) {
63+
// TODO: implement
64+
return nil, nil
65+
}
66+
6267
func (c *inMemoryCache) Delete(key string) error {
6368
c.mutex.Lock()
6469
defer c.mutex.Unlock()

pkg/cache/v3/mock/user.go

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/cache/v3/redis_cache.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ func (r *redisCache) SAdd(key string, members ...interface{}) (int64, error) {
5050
return r.client.SAdd(key, members...)
5151
}
5252

53+
func (r *redisCache) SMembers(key string) ([]string, error) {
54+
return r.client.SMembers(key)
55+
}
56+
5357
func (r *redisCache) GetMulti(keys interface{}, ignoreNotFound bool) ([]interface{}, error) {
5458
value, err := r.client.GetMulti(keys.([]string), ignoreNotFound)
5559
switch err {

pkg/cache/v3/user.go

Lines changed: 46 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,20 @@ package v3
1818
import (
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

2827
const (
29-
userAttributeKind = "user_attr"
30-
userAttributesMaxSize = int64(100)
28+
userAttributeKind = "user_attr"
3129
)
3230

3331
type 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

Comments
 (0)