Skip to content

Commit 42f7c8d

Browse files
authored
Improve Project Caching and Invalidation Consistency (#1600)
Enhanced the project caching mechanism with a dual-lookup strategy that uses project ID as the primary key and API key as a secondary key to achieve faster and more reliable lookups.
1 parent e816a17 commit 42f7c8d

File tree

5 files changed

+166
-17
lines changed

5 files changed

+166
-17
lines changed

api/yorkie/v1/cluster.proto

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,6 @@ enum CacheType {
9090
}
9191

9292
message InvalidateCacheRequest {
93-
9493
CacheType cache_type = 1;
9594
string key = 2;
9695
}

pkg/cache/lru_with_expires.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,18 @@ type LRUWithExpires[K comparable, V any] struct {
3535
}
3636

3737
// NewLRUWithExpires creates a new expirable LRU with the given size and ttl.
38-
func NewLRUWithExpires[K comparable, V any](size int, ttl time.Duration, name string) (*LRUWithExpires[K, V], error) {
39-
c := expirable.NewLRU[K, V](size, nil, ttl)
38+
func NewLRUWithExpires[K comparable, V any](
39+
size int,
40+
ttl time.Duration,
41+
name string,
42+
onEvict ...func(key K, value V),
43+
) (*LRUWithExpires[K, V], error) {
44+
var callback func(key K, value V)
45+
if len(onEvict) > 0 {
46+
callback = onEvict[0]
47+
}
48+
49+
c := expirable.NewLRU(size, callback, ttl)
4050
return &LRUWithExpires[K, V]{
4151
cache: c,
4252
stats: &Stats{},

server/backend/database/mongo/client.go

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ type Client struct {
5252
client *mongo.Client
5353

5454
cacheManager *cache.Manager
55-
projectCache *cache.LRUWithExpires[string, *database.ProjectInfo]
55+
projectCache *ProjectCache
5656
clientCache *cache.LRU[types.ClientRefKey, *database.ClientInfo]
5757
docCache *cache.LRU[types.DocRefKey, *database.DocInfo]
5858
changeCache *cache.LRU[types.DocRefKey, *ChangeStore]
@@ -104,11 +104,7 @@ func Dial(conf *Config) (*Client, error) {
104104

105105
cacheManager := cache.NewManager(conf.ParseCacheStatsInterval())
106106

107-
projectCache, err := cache.NewLRUWithExpires[string, *database.ProjectInfo](
108-
conf.ProjectCacheSize,
109-
conf.ParseProjectCacheTTL(),
110-
"projects",
111-
)
107+
projectCache, err := NewProjectCache(conf.ProjectCacheSize, conf.ParseProjectCacheTTL())
112108
if err != nil {
113109
return nil, fmt.Errorf("initialize project cache: %w", err)
114110
}
@@ -152,8 +148,7 @@ func Dial(conf *Config) (*Client, error) {
152148
config: conf,
153149
client: client,
154150

155-
cacheManager: cacheManager,
156-
151+
cacheManager: cacheManager,
157152
projectCache: projectCache,
158153
clientCache: clientCache,
159154
docCache: docCache,
@@ -191,7 +186,9 @@ func (c *Client) Close() error {
191186
func (c *Client) InvalidateCache(cacheType types.CacheType, key string) {
192187
switch cacheType {
193188
case types.CacheTypeProject:
194-
c.projectCache.Remove(key)
189+
if id := types.ID(key); id.Validate() == nil {
190+
c.projectCache.Remove(id)
191+
}
195192
}
196193
}
197194

@@ -566,7 +563,7 @@ func (c *Client) ListProjectInfos(
566563

567564
// FindProjectInfoByPublicKey returns a project by public key.
568565
func (c *Client) FindProjectInfoByPublicKey(ctx context.Context, publicKey string) (*database.ProjectInfo, error) {
569-
if cached, ok := c.projectCache.Get(publicKey); ok {
566+
if cached, ok := c.projectCache.GetByAPIKey(publicKey); ok {
570567
return cached.DeepCopy(), nil
571568
}
572569

@@ -583,7 +580,7 @@ func (c *Client) FindProjectInfoByPublicKey(ctx context.Context, publicKey strin
583580
return nil, fmt.Errorf("find project by public key %s: %w", publicKey, err)
584581
}
585582

586-
c.projectCache.Add(publicKey, info.DeepCopy())
583+
c.projectCache.Add(info)
587584

588585
return info, nil
589586
}
@@ -631,6 +628,10 @@ func (c *Client) FindProjectInfoByName(
631628

632629
// FindProjectInfoByID returns a project by the given id.
633630
func (c *Client) FindProjectInfoByID(ctx context.Context, id types.ID) (*database.ProjectInfo, error) {
631+
if cached, ok := c.projectCache.GetByID(id); ok {
632+
return cached.DeepCopy(), nil
633+
}
634+
634635
result := c.collection(ColProjects).FindOne(ctx, bson.M{
635636
"_id": id,
636637
})
@@ -643,6 +644,8 @@ func (c *Client) FindProjectInfoByID(ctx context.Context, id types.ID) (*databas
643644
return nil, fmt.Errorf("find project by id %s: %w", id, err)
644645
}
645646

647+
c.projectCache.Add(info)
648+
646649
return info, nil
647650
}
648651

@@ -682,6 +685,8 @@ func (c *Client) UpdateProjectInfo(
682685
return nil, fmt.Errorf("decode project: %w", err)
683686
}
684687

688+
c.projectCache.Remove(info.ID)
689+
685690
return info, nil
686691
}
687692

@@ -697,7 +702,7 @@ func (c *Client) RotateProjectKeys(
697702
prevInfo := &database.ProjectInfo{}
698703
res := c.collection(ColProjects).FindOne(ctx, bson.M{"_id": id, "owner": owner})
699704
if err := res.Decode(prevInfo); err == nil {
700-
c.projectCache.Remove(prevInfo.PublicKey)
705+
c.projectCache.Remove(prevInfo.ID)
701706
}
702707

703708
res = c.collection(ColProjects).FindOneAndUpdate(ctx, bson.M{
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/*
2+
* Copyright 2025 The Yorkie Authors. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package mongo
18+
19+
import (
20+
"sync"
21+
"time"
22+
23+
"github.com/yorkie-team/yorkie/api/types"
24+
"github.com/yorkie-team/yorkie/pkg/cache"
25+
"github.com/yorkie-team/yorkie/server/backend/database"
26+
)
27+
28+
// ProjectCache is a cache for project information with multiple access paths.
29+
// It uses a single LRU cache with ID as the primary key and maintains a
30+
// secondary index for API key lookups to minimize memory usage and ensure
31+
// cache consistency.
32+
type ProjectCache struct {
33+
// Primary cache: ID is the primary key
34+
cache *cache.LRUWithExpires[types.ID, *database.ProjectInfo]
35+
36+
// Secondary index: API key -> ID mapping
37+
apiKeyToID sync.Map // map[string]types.ID
38+
}
39+
40+
// NewProjectCache creates a new project cache with the given size and TTL.
41+
func NewProjectCache(size int, ttl time.Duration) (*ProjectCache, error) {
42+
pc := &ProjectCache{
43+
apiKeyToID: sync.Map{},
44+
}
45+
46+
// Create cache with eviction callback to clean up secondary index
47+
onEvict := func(id types.ID, info *database.ProjectInfo) {
48+
// When an entry is evicted from primary cache (by TTL or LRU),
49+
// remove it from the secondary index as well
50+
pc.apiKeyToID.Delete(info.PublicKey)
51+
}
52+
53+
c, err := cache.NewLRUWithExpires[types.ID, *database.ProjectInfo](
54+
size,
55+
ttl,
56+
"project",
57+
onEvict,
58+
)
59+
if err != nil {
60+
return nil, err
61+
}
62+
63+
pc.cache = c
64+
return pc, nil
65+
}
66+
67+
// Name returns the cache name.
68+
func (pc *ProjectCache) Name() string {
69+
return pc.cache.Name()
70+
}
71+
72+
// Stats returns the cache statistics.
73+
func (pc *ProjectCache) Stats() *cache.Stats {
74+
return pc.cache.Stats()
75+
}
76+
77+
// Len returns the number of items in the cache.
78+
func (pc *ProjectCache) Len() int {
79+
return pc.cache.Len()
80+
}
81+
82+
// GetByAPIKey retrieves a project by API key.
83+
func (pc *ProjectCache) GetByAPIKey(apiKey string) (*database.ProjectInfo, bool) {
84+
// Look up ID from secondary index
85+
idVal, ok := pc.apiKeyToID.Load(apiKey)
86+
if !ok {
87+
return nil, false
88+
}
89+
90+
id := idVal.(types.ID)
91+
92+
// Get from primary cache
93+
info, found := pc.cache.Get(id)
94+
if !found {
95+
// Primary cache entry was evicted but secondary index still exists
96+
// Clean up the stale secondary index entry
97+
pc.apiKeyToID.Delete(apiKey)
98+
return nil, false
99+
}
100+
101+
return info, true
102+
}
103+
104+
// GetByID retrieves a project by ID.
105+
func (pc *ProjectCache) GetByID(id types.ID) (*database.ProjectInfo, bool) {
106+
return pc.cache.Get(id)
107+
}
108+
109+
// Add adds a project to the cache.
110+
func (pc *ProjectCache) Add(info *database.ProjectInfo) {
111+
project := info.DeepCopy()
112+
113+
// Add to primary cache
114+
pc.cache.Add(info.ID, project)
115+
116+
// Update secondary index
117+
pc.apiKeyToID.Store(info.PublicKey, info.ID)
118+
}
119+
120+
// Remove removes a project from the cache using both API key and ID.
121+
func (pc *ProjectCache) Remove(id types.ID) {
122+
if info, ok := pc.cache.Peek(id); ok {
123+
pc.apiKeyToID.Delete(info.PublicKey)
124+
}
125+
pc.cache.Remove(id)
126+
}
127+
128+
// Purge removes all entries from the cache.
129+
func (pc *ProjectCache) Purge() {
130+
pc.cache.Purge()
131+
pc.apiKeyToID.Range(func(key, _ any) bool {
132+
pc.apiKeyToID.Delete(key)
133+
return true
134+
})
135+
}

server/rpc/admin_server.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ func (s *adminServer) UpdateProject(
239239
if err := s.backend.BroadcastCacheInvalidation(
240240
ctx,
241241
types.CacheTypeProject,
242-
project.PublicKey,
242+
project.ID.String(),
243243
); err != nil {
244244
logging.From(ctx).Warnf("failed to broadcast cache invalidation: %v", err)
245245
}
@@ -817,7 +817,7 @@ func (s *adminServer) RotateProjectKeys(
817817
if err := s.backend.BroadcastCacheInvalidation(
818818
ctx,
819819
types.CacheTypeProject,
820-
prev.PublicKey,
820+
prev.ID.String(),
821821
); err != nil {
822822
logging.From(ctx).Warnf("failed to broadcast cache invalidation: %v", err)
823823
}

0 commit comments

Comments
 (0)