Skip to content

Commit a2e4e2a

Browse files
committed
add cached store
1 parent aa07007 commit a2e4e2a

File tree

2 files changed

+458
-0
lines changed

2 files changed

+458
-0
lines changed

pkg/store/cached_store.go

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
package store
2+
3+
import (
4+
"context"
5+
"sync"
6+
7+
lru "github.com/hashicorp/golang-lru/v2"
8+
9+
"github.com/evstack/ev-node/types"
10+
)
11+
12+
const (
13+
// DefaultHeaderCacheSize is the default number of headers to cache in memory.
14+
// Each SignedHeader is roughly 1-2KB, so 2M headers ≈ 2-4GB of memory.
15+
DefaultHeaderCacheSize = 2_000_000
16+
17+
// DefaultBlockDataCacheSize is the default number of block data entries to cache.
18+
// Block data entries are larger, so we cache fewer of them.
19+
DefaultBlockDataCacheSize = 100_000
20+
)
21+
22+
// CachedStore wraps a Store with LRU caching for frequently accessed data.
23+
type CachedStore struct {
24+
Store
25+
26+
headerCache *lru.Cache[uint64, *types.SignedHeader]
27+
headerCacheMu sync.RWMutex
28+
29+
// Optional: cache for block data (headers + data together)
30+
blockDataCache *lru.Cache[uint64, *blockDataEntry]
31+
blockDataCacheMu sync.RWMutex
32+
}
33+
34+
type blockDataEntry struct {
35+
header *types.SignedHeader
36+
data *types.Data
37+
}
38+
39+
// CachedStoreOption configures a CachedStore.
40+
type CachedStoreOption func(*CachedStore) error
41+
42+
// WithHeaderCacheSize sets the header cache size.
43+
func WithHeaderCacheSize(size int) CachedStoreOption {
44+
return func(cs *CachedStore) error {
45+
cache, err := lru.New[uint64, *types.SignedHeader](size)
46+
if err != nil {
47+
return err
48+
}
49+
cs.headerCache = cache
50+
return nil
51+
}
52+
}
53+
54+
// WithBlockDataCacheSize sets the block data cache size.
55+
func WithBlockDataCacheSize(size int) CachedStoreOption {
56+
return func(cs *CachedStore) error {
57+
cache, err := lru.New[uint64, *blockDataEntry](size)
58+
if err != nil {
59+
return err
60+
}
61+
cs.blockDataCache = cache
62+
return nil
63+
}
64+
}
65+
66+
// NewCachedStore creates a new CachedStore wrapping the given store.
67+
func NewCachedStore(store Store, opts ...CachedStoreOption) (*CachedStore, error) {
68+
headerCache, err := lru.New[uint64, *types.SignedHeader](DefaultHeaderCacheSize)
69+
if err != nil {
70+
return nil, err
71+
}
72+
73+
blockDataCache, err := lru.New[uint64, *blockDataEntry](DefaultBlockDataCacheSize)
74+
if err != nil {
75+
return nil, err
76+
}
77+
78+
cs := &CachedStore{
79+
Store: store,
80+
headerCache: headerCache,
81+
blockDataCache: blockDataCache,
82+
}
83+
84+
for _, opt := range opts {
85+
if err := opt(cs); err != nil {
86+
return nil, err
87+
}
88+
}
89+
90+
return cs, nil
91+
}
92+
93+
// GetHeader returns the header at the given height, using the cache if available.
94+
func (cs *CachedStore) GetHeader(ctx context.Context, height uint64) (*types.SignedHeader, error) {
95+
// Try cache first
96+
cs.headerCacheMu.RLock()
97+
if header, ok := cs.headerCache.Get(height); ok {
98+
cs.headerCacheMu.RUnlock()
99+
return header, nil
100+
}
101+
cs.headerCacheMu.RUnlock()
102+
103+
// Cache miss, fetch from underlying store
104+
header, err := cs.Store.GetHeader(ctx, height)
105+
if err != nil {
106+
return nil, err
107+
}
108+
109+
// Add to cache
110+
cs.headerCacheMu.Lock()
111+
cs.headerCache.Add(height, header)
112+
cs.headerCacheMu.Unlock()
113+
114+
return header, nil
115+
}
116+
117+
// GetBlockData returns block header and data at given height, using cache if available.
118+
func (cs *CachedStore) GetBlockData(ctx context.Context, height uint64) (*types.SignedHeader, *types.Data, error) {
119+
// Try cache first
120+
cs.blockDataCacheMu.RLock()
121+
if entry, ok := cs.blockDataCache.Get(height); ok {
122+
cs.blockDataCacheMu.RUnlock()
123+
return entry.header, entry.data, nil
124+
}
125+
cs.blockDataCacheMu.RUnlock()
126+
127+
// Cache miss, fetch from underlying store
128+
header, data, err := cs.Store.GetBlockData(ctx, height)
129+
if err != nil {
130+
return nil, nil, err
131+
}
132+
133+
// Add to cache
134+
cs.blockDataCacheMu.Lock()
135+
cs.blockDataCache.Add(height, &blockDataEntry{header: header, data: data})
136+
cs.blockDataCacheMu.Unlock()
137+
138+
// Also add header to header cache
139+
cs.headerCacheMu.Lock()
140+
cs.headerCache.Add(height, header)
141+
cs.headerCacheMu.Unlock()
142+
143+
return header, data, nil
144+
}
145+
146+
// InvalidateRange removes headers in the given range from the cache.
147+
func (cs *CachedStore) InvalidateRange(fromHeight, toHeight uint64) {
148+
cs.headerCacheMu.Lock()
149+
for h := fromHeight; h <= toHeight; h++ {
150+
cs.headerCache.Remove(h)
151+
}
152+
cs.headerCacheMu.Unlock()
153+
154+
cs.blockDataCacheMu.Lock()
155+
for h := fromHeight; h <= toHeight; h++ {
156+
cs.blockDataCache.Remove(h)
157+
}
158+
cs.blockDataCacheMu.Unlock()
159+
}
160+
161+
// ClearCache clears all cached entries.
162+
func (cs *CachedStore) ClearCache() {
163+
cs.headerCacheMu.Lock()
164+
cs.headerCache.Purge()
165+
cs.headerCacheMu.Unlock()
166+
167+
cs.blockDataCacheMu.Lock()
168+
cs.blockDataCache.Purge()
169+
cs.blockDataCacheMu.Unlock()
170+
}
171+
172+
// Rollback wraps the underlying store's Rollback and invalidates affected cache entries.
173+
func (cs *CachedStore) Rollback(ctx context.Context, height uint64, aggregator bool) error {
174+
currentHeight, err := cs.Store.Height(ctx)
175+
if err != nil {
176+
return err
177+
}
178+
179+
// First do the rollback
180+
if err := cs.Store.Rollback(ctx, height, aggregator); err != nil {
181+
return err
182+
}
183+
184+
// Then invalidate cache entries for rolled back heights
185+
cs.InvalidateRange(height+1, currentHeight)
186+
187+
return nil
188+
}
189+
190+
// Close closes the underlying store.
191+
func (cs *CachedStore) Close() error {
192+
cs.ClearCache()
193+
return cs.Store.Close()
194+
}

0 commit comments

Comments
 (0)