Skip to content

Commit 58e5e11

Browse files
klaidliadonCopilot
andauthored
Implement cache versioning for Redis keys in Usage and Permission caches (#139)
Co-authored-by: Copilot <[email protected]>
1 parent 65ca903 commit 58e5e11

File tree

3 files changed

+50
-36
lines changed

3 files changed

+50
-36
lines changed

cache.go

Lines changed: 42 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@ type QuotaCache interface {
2525
}
2626

2727
type UsageCache interface {
28-
SetUsage(ctx context.Context, redisKey string, amount int64) error
29-
ClearUsage(ctx context.Context, redisKey string) (bool, error)
30-
PeekUsage(ctx context.Context, redisKey string) (int64, error)
31-
SpendUsage(ctx context.Context, redisKey string, amount, limit int64) (int64, error)
28+
SetUsage(ctx context.Context, key string, amount int64) error
29+
ClearUsage(ctx context.Context, key string) (bool, error)
30+
PeekUsage(ctx context.Context, key string) (int64, error)
31+
SpendUsage(ctx context.Context, key string, amount, limit int64) (int64, error)
3232
}
3333

3434
type PermissionCache interface {
@@ -45,8 +45,7 @@ var (
4545
)
4646

4747
const (
48-
redisQuotaPrefix = "quota:"
49-
redisRLPrefix = "rl:"
48+
redisRLPrefix = "rl:"
5049
)
5150

5251
var (
@@ -92,8 +91,27 @@ func NewLimitCounter(svc Service, cfg RedisConfig, logger *slog.Logger) httprate
9291
const (
9392
defaultExpRedis = time.Hour
9493
defaultExpLRU = time.Minute
94+
cacheVersion = "v1"
9595
)
9696

97+
// usageKey returns the redis key for storing usage amount.
98+
// It does not include version because usage is just a number, and it's safe to share across versions.
99+
func usageKey(key string) string {
100+
return fmt.Sprintf("usage:%s", key)
101+
}
102+
103+
// quotaKey returns the redis key for storing AccessQuota.
104+
// It includes version to avoid conflicts when the structure changes.
105+
func quotaKey(key string) string {
106+
return fmt.Sprintf("quota:%s:%s", cacheVersion, key)
107+
}
108+
109+
// permissionKey returns the redis key for storing user permission for a project.
110+
// It includes version to avoid conflicts when the structure changes.
111+
func permissionKey(projectID uint64, userID string) string {
112+
return fmt.Sprintf("perm:%s:project:%d:user:%s", cacheVersion, projectID, userID)
113+
}
114+
97115
func NewRedisCache(redisClient *redis.Client, ttl time.Duration) *RedisCache {
98116
if ttl <= 0 {
99117
ttl = defaultExpRedis
@@ -134,8 +152,7 @@ func (s *RedisCache) SetProjectQuota(ctx context.Context, quota *proto.AccessQuo
134152
}
135153

136154
func (s *RedisCache) getQuota(ctx context.Context, key string) (*proto.AccessQuota, error) {
137-
cacheKey := fmt.Sprintf("%s%s", redisQuotaPrefix, key)
138-
raw, err := s.client.Get(ctx, cacheKey).Bytes()
155+
raw, err := s.client.Get(ctx, quotaKey(key)).Bytes()
139156
if err != nil {
140157
if err == redis.Nil {
141158
return nil, proto.ErrAccessKeyNotFound
@@ -150,8 +167,7 @@ func (s *RedisCache) getQuota(ctx context.Context, key string) (*proto.AccessQuo
150167
}
151168

152169
func (s *RedisCache) deleteQuota(ctx context.Context, key string) error {
153-
cacheKey := fmt.Sprintf("%s%s", redisQuotaPrefix, key)
154-
if err := s.client.Del(ctx, cacheKey).Err(); err != nil {
170+
if err := s.client.Del(ctx, quotaKey(key)).Err(); err != nil {
155171
return fmt.Errorf("delete quota: %w", err)
156172
}
157173
return nil
@@ -162,33 +178,31 @@ func (s *RedisCache) setQuota(ctx context.Context, key string, quota *proto.Acce
162178
if err != nil {
163179
return fmt.Errorf("marshal quota: %w", err)
164180
}
165-
cacheKey := fmt.Sprintf("%s%s", redisQuotaPrefix, key)
166-
if err := s.client.Set(ctx, cacheKey, raw, s.ttl).Err(); err != nil {
181+
if err := s.client.Set(ctx, quotaKey(key), raw, s.ttl).Err(); err != nil {
167182
return fmt.Errorf("set quota: %w", err)
168183
}
169184
return nil
170185
}
171186

172-
func (s *RedisCache) SetUsage(ctx context.Context, redisKey string, amount int64) error {
173-
cacheKey := fmt.Sprintf("%s%s", redisQuotaPrefix, redisKey)
174-
if err := s.client.Set(ctx, cacheKey, amount, s.ttl).Err(); err != nil {
187+
func (s *RedisCache) SetUsage(ctx context.Context, key string, amount int64) error {
188+
if err := s.client.Set(ctx, usageKey(key), amount, s.ttl).Err(); err != nil {
175189
return fmt.Errorf("set usage: %w", err)
176190
}
177191
return nil
178192
}
179193

180-
func (s *RedisCache) ClearUsage(ctx context.Context, redisKey string) (bool, error) {
181-
cacheKey := fmt.Sprintf("%s%s", redisQuotaPrefix, redisKey)
182-
count, err := s.client.Del(ctx, cacheKey).Result()
194+
func (s *RedisCache) ClearUsage(ctx context.Context, key string) (bool, error) {
195+
count, err := s.client.Del(ctx, usageKey(key)).Result()
183196
if err != nil {
184197
return false, fmt.Errorf("clear usage: %w", err)
185198
}
186199
return count != 0, nil
187200
}
188201

189-
func (s *RedisCache) PeekUsage(ctx context.Context, redisKey string) (int64, error) {
202+
func (s *RedisCache) PeekUsage(ctx context.Context, key string) (int64, error) {
190203
const SpecialValue = -1
191-
cacheKey := fmt.Sprintf("%s%s", redisQuotaPrefix, redisKey)
204+
cacheKey := usageKey(key)
205+
192206
v, err := s.client.Get(ctx, cacheKey).Int64()
193207
if err == nil {
194208
if v == SpecialValue {
@@ -209,17 +223,17 @@ func (s *RedisCache) PeekUsage(ctx context.Context, redisKey string) (int64, err
209223
return 0, errCacheReady
210224
}
211225

212-
func (s *RedisCache) SpendUsage(ctx context.Context, redisKey string, amount, limit int64) (int64, error) {
213-
// NOTE: skip redisKeyPrefix as it's already in PeekCost
214-
v, err := s.PeekUsage(ctx, redisKey)
226+
func (s *RedisCache) SpendUsage(ctx context.Context, key string, amount, limit int64) (int64, error) {
227+
// NOTE: don't use usageKey yet, PeekUsage is doing that
228+
v, err := s.PeekUsage(ctx, key)
215229
if err != nil {
216230
return 0, fmt.Errorf("spend usage - peek: %w", err)
217231
}
218232
if v >= limit {
219233
return v, proto.ErrQuotaExceeded
220234
}
221-
cacheKey := fmt.Sprintf("%s%s", redisQuotaPrefix, redisKey)
222-
value, err := s.client.IncrBy(ctx, cacheKey, amount).Result()
235+
236+
value, err := s.client.IncrBy(ctx, usageKey(key), amount).Result()
223237
if err != nil {
224238
return v, fmt.Errorf("spend usage - incrby: %w", err)
225239
}
@@ -232,8 +246,7 @@ type cacheUserPermission struct {
232246
}
233247

234248
func (s *RedisCache) GetUserPermission(ctx context.Context, projectID uint64, userID string) (proto.UserPermission, *proto.ResourceAccess, error) {
235-
cacheKey := fmt.Sprintf("%s%s", redisQuotaPrefix, getUserPermKey(projectID, userID))
236-
raw, err := s.client.Get(ctx, cacheKey).Bytes()
249+
raw, err := s.client.Get(ctx, permissionKey(projectID, userID)).Bytes()
237250
if err != nil {
238251
if err == redis.Nil {
239252
return proto.UserPermission_UNAUTHORIZED, nil, nil // not found, without error
@@ -258,13 +271,11 @@ func (s *RedisCache) SetUserPermission(ctx context.Context, projectID uint64, us
258271
}
259272

260273
// cache userPermissions for 10 seconds
261-
cacheKey := fmt.Sprintf("%s%s", redisQuotaPrefix, getUserPermKey(projectID, userID))
262-
return s.client.Set(ctx, cacheKey, raw, 10*time.Second).Err()
274+
return s.client.Set(ctx, permissionKey(projectID, userID), raw, 10*time.Second).Err()
263275
}
264276

265277
func (s *RedisCache) DeleteUserPermission(ctx context.Context, projectID uint64, userID string) error {
266-
cacheKey := fmt.Sprintf("%s%s", redisQuotaPrefix, getUserPermKey(projectID, userID))
267-
return s.client.Del(ctx, cacheKey).Err()
278+
return s.client.Del(ctx, permissionKey(projectID, userID)).Err()
268279
}
269280

270281
type LRU struct {
@@ -336,7 +347,3 @@ func (s *LRU) deleteQuota(ctx context.Context, key string) error {
336347
func getProjectKey(projectID uint64) string {
337348
return fmt.Sprintf("project:%d", projectID)
338349
}
339-
340-
func getUserPermKey(projectID uint64, userID string) string {
341-
return fmt.Sprintf("project:%d:userPerm:%s", projectID, userID)
342-
}

client.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ func (c *Client) FetchKeyQuota(ctx context.Context, accessKey, origin string, ch
160160
return nil, err
161161
}
162162
if err := c.cache.QuotaCache.SetAccessQuota(ctx, quota); err != nil {
163-
logger.Error("failed to cache access quota", slog.Any("error", err))
163+
logger.Warn("failed to cache access quota", slog.Any("error", err))
164164
}
165165
}
166166
// validate access key
@@ -254,6 +254,11 @@ func (c *Client) FetchPermission(ctx context.Context, projectID uint64) (proto.U
254254
logger.Error("unexpected client error", slog.Any("error", err))
255255
return proto.UserPermission_UNAUTHORIZED, nil, fmt.Errorf("get user permission from quotacontrol server: %w", err)
256256
}
257+
if !perm.Is(proto.UserPermission_UNAUTHORIZED) {
258+
if err := c.cache.PermissionCache.SetUserPermission(ctx, projectID, userID, perm, access); err != nil {
259+
c.logger.Warn("set user perm in cache", slog.Any("error", err))
260+
}
261+
}
257262
return perm, access, nil
258263
}
259264

server.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ func (s server) GetProjectQuota(ctx context.Context, projectID uint64, now time.
226226
AccessKey: &proto.AccessKey{ProjectID: projectID},
227227
}
228228

229+
// deprecated: cache is set by the client side now
229230
if err := s.cache.QuotaCache.SetProjectQuota(ctx, &record); err != nil {
230231
s.log.Error("set access quota in cache", slog.Any("error", err))
231232
}
@@ -535,6 +536,7 @@ func (s server) GetUserPermission(ctx context.Context, projectID uint64, userID
535536
return proto.UserPermission_UNAUTHORIZED, nil, proto.ErrUnauthorizedUser
536537
}
537538

539+
// deprecated: cache is set by the client side now
538540
if !perm.Is(proto.UserPermission_UNAUTHORIZED) {
539541
if err := s.cache.PermissionCache.SetUserPermission(ctx, projectID, userID, perm, access); err != nil {
540542
s.log.Error("set user perm in cache", slog.Any("error", err))

0 commit comments

Comments
 (0)