@@ -8,23 +8,11 @@ import (
88 "sync"
99 "sync/atomic"
1010
11- lru "github.com/hashicorp/golang-lru/v2"
1211 ds "github.com/ipfs/go-datastore"
1312
1413 "github.com/evstack/ev-node/pkg/store"
1514)
1615
17- const (
18- // DefaultItemsCacheSize is the default size for items cache.
19- DefaultItemsCacheSize = 200_000
20-
21- // DefaultHashesCacheSize is the default size for hash tracking.
22- DefaultHashesCacheSize = 200_000
23-
24- // DefaultDAIncludedCacheSize is the default size for DA inclusion tracking.
25- DefaultDAIncludedCacheSize = 200_000
26- )
27-
2816// snapshotEntry is one record in the persisted snapshot.
2917// Encoded as 16 bytes: [blockHeight uint64 LE][daHeight uint64 LE].
3018type snapshotEntry struct {
@@ -34,142 +22,98 @@ type snapshotEntry struct {
3422
3523const snapshotEntrySize = 16 // bytes per snapshotEntry
3624
37- // Cache tracks seen blocks and DA inclusion status using bounded LRU caches.
38- type Cache [T any ] struct {
39- // itemsByHeight stores items keyed by uint64 height.
40- // Mutex needed for atomic get-and-remove in getNextItem.
41- itemsByHeight * lru.Cache [uint64 , * T ]
42- itemsByHeightMu sync.Mutex
25+ // Cache tracks seen blocks and DA inclusion status.
26+ type Cache struct {
27+ mu sync.Mutex
4328
44- // hashes tracks whether a given hash has been seen
45- hashes * lru.Cache [string , bool ]
29+ hashes map [string ]bool
30+ daIncluded map [string ]uint64
31+ hashByHeight map [uint64 ]string
32+ maxDAHeight * atomic.Uint64
4633
47- // daIncluded maps hash → daHeight. Hash may be a real content hash or a
48- // height placeholder (see HeightPlaceholderKey) immediately after restore.
49- daIncluded * lru.Cache [string , uint64 ]
50-
51- // hashByHeight maps blockHeight → hash, used for pruning and height-based
52- // lookups. Protected by hashByHeightMu only in deleteAllForHeight where a
53- // read-then-remove must be atomic.
54- hashByHeight * lru.Cache [uint64 , string ]
55- hashByHeightMu sync.Mutex
56-
57- // maxDAHeight tracks the maximum DA height seen
58- maxDAHeight * atomic.Uint64
59-
60- store store.Store // nil = ephemeral, no persistence
61- // storeKeyPrefix is the prefix used for store keys
34+ store store.Store
6235 storeKeyPrefix string
6336}
6437
65- func (c * Cache [ T ] ) snapshotKey () string {
38+ func (c * Cache ) snapshotKey () string {
6639 return c .storeKeyPrefix + "__snap"
6740}
6841
6942// NewCache creates a Cache. When store and keyPrefix are set, mutations
7043// persist a snapshot so RestoreFromStore can recover in-flight state.
71- func NewCache [T any ](s store.Store , keyPrefix string ) * Cache [T ] {
72- // LRU cache creation only fails if size <= 0, which won't happen with our defaults
73- itemsCache , _ := lru.New [uint64 , * T ](DefaultItemsCacheSize )
74- hashesCache , _ := lru.New [string , bool ](DefaultHashesCacheSize )
75- daIncludedCache , _ := lru.New [string , uint64 ](DefaultDAIncludedCacheSize )
76- hashByHeightCache , _ := lru.New [uint64 , string ](DefaultHashesCacheSize )
77-
78- return & Cache [T ]{
79- itemsByHeight : itemsCache ,
80- hashes : hashesCache ,
81- daIncluded : daIncludedCache ,
82- hashByHeight : hashByHeightCache ,
44+ func NewCache (s store.Store , keyPrefix string ) * Cache {
45+ return & Cache {
46+ hashes : make (map [string ]bool ),
47+ daIncluded : make (map [string ]uint64 ),
48+ hashByHeight : make (map [uint64 ]string ),
8349 maxDAHeight : & atomic.Uint64 {},
8450 store : s ,
8551 storeKeyPrefix : keyPrefix ,
8652 }
8753}
8854
89- // getItem returns an item from the cache by height.
90- func (c * Cache [T ]) getItem (height uint64 ) * T {
91- item , ok := c .itemsByHeight .Get (height )
92- if ! ok {
93- return nil
94- }
95- return item
96- }
97-
98- // setItem sets an item in the cache by height.
99- func (c * Cache [T ]) setItem (height uint64 , item * T ) {
100- c .itemsByHeight .Add (height , item )
101- }
102-
103- // getNextItem returns and removes the item at height, or nil if absent.
104- func (c * Cache [T ]) getNextItem (height uint64 ) * T {
105- c .itemsByHeightMu .Lock ()
106- defer c .itemsByHeightMu .Unlock ()
107-
108- item , ok := c .itemsByHeight .Get (height )
109- if ! ok {
110- return nil
111- }
112- c .itemsByHeight .Remove (height )
113- return item
114- }
115-
116- // itemCount returns the number of items currently stored by height.
117- func (c * Cache [T ]) itemCount () int {
118- return c .itemsByHeight .Len ()
55+ func (c * Cache ) isSeen (hash string ) bool {
56+ c .mu .Lock ()
57+ defer c .mu .Unlock ()
58+ return c .hashes [hash ]
11959}
12060
121- // isSeen returns true if the hash has been seen.
122- func (c * Cache [T ]) isSeen (hash string ) bool {
123- seen , ok := c .hashes .Get (hash )
124- return ok && seen
61+ func (c * Cache ) setSeen (hash string , height uint64 ) {
62+ c .mu .Lock ()
63+ defer c .mu .Unlock ()
64+ c .hashes [hash ] = true
65+ c .hashByHeight [height ] = hash
12566}
12667
127- // setSeen sets the hash as seen and tracks its height for pruning.
128- func ( c * Cache [ T ]) setSeen ( hash string , height uint64 ) {
129- c . hashes . Add ( hash , true )
130- c . hashByHeight . Add ( height , hash )
68+ func ( c * Cache ) removeSeen ( hash string ) {
69+ c . mu . Lock ()
70+ defer c . mu . Unlock ( )
71+ delete ( c . hashes , hash )
13172}
13273
133- // getDAIncluded returns the DA height if the hash has been DA-included.
134- func (c * Cache [T ]) getDAIncluded (daCommitmentHash string ) (uint64 , bool ) {
135- return c .daIncluded .Get (daCommitmentHash )
74+ func (c * Cache ) getDAIncluded (hash string ) (uint64 , bool ) {
75+ c .mu .Lock ()
76+ defer c .mu .Unlock ()
77+ v , ok := c .daIncluded [hash ]
78+ return v , ok
13679}
13780
138- // getDAIncludedByHeight resolves DA height via the height→hash index.
139- // Works for both real hashes (steady state) and snapshot placeholders
140- // (post-restart, before the DA retriever re-fires the real hash).
141- func (c * Cache [T ]) getDAIncludedByHeight (blockHeight uint64 ) (uint64 , bool ) {
142- hash , ok := c .hashByHeight .Get (blockHeight )
81+ func (c * Cache ) getDAIncludedByHeight (blockHeight uint64 ) (uint64 , bool ) {
82+ c .mu .Lock ()
83+ defer c .mu .Unlock ()
84+ hash , ok := c .hashByHeight [blockHeight ]
14385 if ! ok {
14486 return 0 , false
14587 }
146- return c .getDAIncluded (hash )
88+ v , exists := c .daIncluded [hash ]
89+ return v , exists
14790}
14891
14992// setDAIncluded records DA inclusion in memory.
15093// If a previous entry already exists at blockHeight (e.g. a placeholder from
15194// RestoreFromStore), it is evicted from daIncluded to avoid orphan leaks.
152- func (c * Cache [T ]) setDAIncluded (hash string , daHeight uint64 , blockHeight uint64 ) {
153- if prev , ok := c .hashByHeight .Get (blockHeight ); ok && prev != hash {
154- c .daIncluded .Remove (prev )
95+ func (c * Cache ) setDAIncluded (hash string , daHeight uint64 , blockHeight uint64 ) {
96+ c .mu .Lock ()
97+ defer c .mu .Unlock ()
98+ if prev , ok := c .hashByHeight [blockHeight ]; ok && prev != hash {
99+ delete (c .daIncluded , prev )
155100 }
156- c .daIncluded . Add ( hash , daHeight )
157- c .hashByHeight . Add ( blockHeight , hash )
101+ c .daIncluded [ hash ] = daHeight
102+ c .hashByHeight [ blockHeight ] = hash
158103 c .setMaxDAHeight (daHeight )
159104}
160105
161- // removeDAIncluded removes the DA-included status of the hash from the cache.
162- func (c * Cache [T ]) removeDAIncluded (hash string ) {
163- c .daIncluded .Remove (hash )
106+ func (c * Cache ) removeDAIncluded (hash string ) {
107+ c .mu .Lock ()
108+ defer c .mu .Unlock ()
109+ delete (c .daIncluded , hash )
164110}
165111
166- // daHeight returns the maximum DA height from all DA-included items.
167- func (c * Cache [T ]) daHeight () uint64 {
112+ func (c * Cache ) daHeight () uint64 {
168113 return c .maxDAHeight .Load ()
169114}
170115
171- // setMaxDAHeight sets the maximum DA height if the provided value is greater.
172- func (c * Cache [T ]) setMaxDAHeight (daHeight uint64 ) {
116+ func (c * Cache ) setMaxDAHeight (daHeight uint64 ) {
173117 for range 1_000 {
174118 current := c .maxDAHeight .Load ()
175119 if daHeight <= current {
@@ -181,49 +125,41 @@ func (c *Cache[T]) setMaxDAHeight(daHeight uint64) {
181125 }
182126}
183127
184- // removeSeen removes a hash from the seen cache.
185- func (c * Cache [T ]) removeSeen (hash string ) {
186- c .hashes .Remove (hash )
187- }
188-
189- // deleteAllForHeight removes all items and their associated data from the
190- // cache at the given height.
191- func (c * Cache [T ]) deleteAllForHeight (height uint64 ) {
192- c .itemsByHeight .Remove (height )
193-
194- c .hashByHeightMu .Lock ()
195- hash , ok := c .hashByHeight .Get (height )
196- if ok {
197- c .hashByHeight .Remove (height )
128+ func (c * Cache ) deleteAllForHeight (height uint64 ) {
129+ c .mu .Lock ()
130+ defer c .mu .Unlock ()
131+ hash , ok := c .hashByHeight [height ]
132+ if ! ok {
133+ return
198134 }
199- c .hashByHeightMu .Unlock ()
135+ delete (c .hashByHeight , height )
136+ delete (c .hashes , hash )
137+ delete (c .daIncluded , hash )
138+ }
200139
201- if ok {
202- c . hashes . Remove ( hash )
203- c . daIncluded . Remove ( hash )
204- }
140+ func ( c * Cache ) daIncludedLen () int {
141+ c . mu . Lock ( )
142+ defer c . mu . Unlock ( )
143+ return len ( c . daIncluded )
205144}
206145
207146// persistSnapshot writes all current in-flight [blockHeight, daHeight] pairs to the store under a single key.
208147// Only called explicitly via SaveToStore. NEVER CALL IT ON HOT-PATH TO AVOID BAGER WRITE AMPLIFICATION.
209- func (c * Cache [ T ] ) persistSnapshot (ctx context.Context ) error {
148+ func (c * Cache ) persistSnapshot (ctx context.Context ) error {
210149 if c .store == nil || c .storeKeyPrefix == "" {
211150 return nil
212151 }
213152
214- heights := c .hashByHeight .Keys ()
215- entries := make ([]snapshotEntry , 0 , len (heights ))
216- for _ , h := range heights {
217- hash , ok := c .hashByHeight .Peek (h )
218- if ! ok {
219- continue
220- }
221- daH , ok := c .daIncluded .Peek (hash )
153+ c .mu .Lock ()
154+ entries := make ([]snapshotEntry , 0 , len (c .hashByHeight ))
155+ for h , hash := range c .hashByHeight {
156+ daH , ok := c .daIncluded [hash ]
222157 if ! ok {
223158 continue
224159 }
225160 entries = append (entries , snapshotEntry {blockHeight : h , daHeight : daH })
226161 }
162+ c .mu .Unlock ()
227163
228164 return c .store .SetMetadata (ctx , c .snapshotKey (), encodeSnapshot (entries ))
229165}
@@ -257,8 +193,7 @@ func decodeSnapshot(buf []byte) []snapshotEntry {
257193// RestoreFromStore loads the in-flight snapshot with a single store read.
258194// Each entry is installed as a height placeholder; real hashes replace them
259195// once the DA retriever re-fires SetHeaderDAIncluded after startup.
260- // Missing snapshot key is treated as a no-op (fresh node or pre-snapshot version).
261- func (c * Cache [T ]) RestoreFromStore (ctx context.Context ) error {
196+ func (c * Cache ) RestoreFromStore (ctx context.Context ) error {
262197 if c .store == nil || c .storeKeyPrefix == "" {
263198 return nil
264199 }
@@ -271,10 +206,13 @@ func (c *Cache[T]) RestoreFromStore(ctx context.Context) error {
271206 return fmt .Errorf ("reading cache snapshot from store: %w" , err )
272207 }
273208
209+ c .mu .Lock ()
210+ defer c .mu .Unlock ()
211+
274212 for _ , e := range decodeSnapshot (buf ) {
275213 placeholder := HeightPlaceholderKey (c .storeKeyPrefix , e .blockHeight )
276- c .daIncluded . Add ( placeholder , e .daHeight )
277- c .hashByHeight . Add ( e .blockHeight , placeholder )
214+ c .daIncluded [ placeholder ] = e .daHeight
215+ c .hashByHeight [ e .blockHeight ] = placeholder
278216 c .setMaxDAHeight (e .daHeight )
279217 }
280218
@@ -297,7 +235,7 @@ func HeightPlaceholderKey(prefix string, height uint64) string {
297235}
298236
299237// SaveToStore flushes the current snapshot to the store.
300- func (c * Cache [ T ] ) SaveToStore (ctx context.Context ) error {
238+ func (c * Cache ) SaveToStore (ctx context.Context ) error {
301239 if c .store == nil {
302240 return nil
303241 }
@@ -308,7 +246,7 @@ func (c *Cache[T]) SaveToStore(ctx context.Context) error {
308246}
309247
310248// ClearFromStore deletes the snapshot key from the store.
311- func (c * Cache [ T ] ) ClearFromStore (ctx context.Context ) error {
249+ func (c * Cache ) ClearFromStore (ctx context.Context ) error {
312250 if c .store == nil {
313251 return nil
314252 }
0 commit comments