@@ -2,8 +2,10 @@ package store
22
33import (
44 "context"
5+ "sync"
56
67 lru "github.com/hashicorp/golang-lru/v2"
8+ "github.com/rs/zerolog"
79
810 "github.com/evstack/ev-node/types"
911)
@@ -14,15 +16,32 @@ const (
1416
1517 // DefaultBlockDataCacheSize is the default number of block data entries to cache.
1618 DefaultBlockDataCacheSize = 200_000
19+
20+ asyncWriteBufferSize = 8192
1721)
1822
23+ type asyncWriteOp struct {
24+ key string
25+ value []byte
26+ isDelete bool
27+ }
28+
1929// CachedStore wraps a Store with LRU caching for frequently accessed data.
2030// The underlying LRU cache is thread-safe, so no additional synchronization is needed.
31+ // Metadata writes (SetMetadata, DeleteMetadata) are processed asynchronously via a
32+ // buffered channel to avoid blocking Badger's write pipeline for critical operations
33+ // like block production (batch commits).
2134type CachedStore struct {
2235 Store
2336
2437 headerCache * lru.Cache [uint64 , * types.SignedHeader ]
2538 blockDataCache * lru.Cache [uint64 , * blockDataEntry ]
39+
40+ writeCh chan asyncWriteOp
41+ done chan struct {}
42+ stopMu sync.RWMutex
43+ stopped bool
44+ logger zerolog.Logger
2645}
2746
2847type blockDataEntry struct {
@@ -73,6 +92,9 @@ func NewCachedStore(store Store, opts ...CachedStoreOption) (*CachedStore, error
7392 Store : store ,
7493 headerCache : headerCache ,
7594 blockDataCache : blockDataCache ,
95+ writeCh : make (chan asyncWriteOp , asyncWriteBufferSize ),
96+ done : make (chan struct {}),
97+ logger : zerolog .Nop (),
7698 }
7799
78100 for _ , opt := range opts {
@@ -81,9 +103,30 @@ func NewCachedStore(store Store, opts ...CachedStoreOption) (*CachedStore, error
81103 }
82104 }
83105
106+ cs .startWriteLoop ()
107+
84108 return cs , nil
85109}
86110
111+ func (cs * CachedStore ) startWriteLoop () {
112+ go func () {
113+ defer close (cs .done )
114+ for op := range cs .writeCh {
115+ var err error
116+ if op .isDelete {
117+ err = cs .Store .DeleteMetadata (context .Background (), op .key )
118+ } else {
119+ err = cs .Store .SetMetadata (context .Background (), op .key , op .value )
120+ }
121+ if err != nil {
122+ cs .logger .Error ().Err (err ).Str ("key" , op .key ).
123+ Bool ("delete" , op .isDelete ).
124+ Msg ("async metadata write failed" )
125+ }
126+ }
127+ }()
128+ }
129+
87130// GetHeader returns the header at the given height, using the cache if available.
88131func (cs * CachedStore ) GetHeader (ctx context.Context , height uint64 ) (* types.SignedHeader , error ) {
89132 // Try cache first
@@ -173,8 +216,51 @@ func (cs *CachedStore) PruneBlocks(ctx context.Context, height uint64) error {
173216 return nil
174217}
175218
176- // Close closes the underlying store.
219+ // SetMetadata queues an asynchronous metadata write. The write is persisted
220+ // by the background goroutine. If the buffer is full or the store has been
221+ // stopped, the write falls back to synchronous execution on the underlying store.
222+ func (cs * CachedStore ) SetMetadata (ctx context.Context , key string , value []byte ) error {
223+ cs .stopMu .RLock ()
224+ if cs .stopped {
225+ cs .stopMu .RUnlock ()
226+ return cs .Store .SetMetadata (ctx , key , value )
227+ }
228+ select {
229+ case cs .writeCh <- asyncWriteOp {key : key , value : value }:
230+ cs .stopMu .RUnlock ()
231+ return nil
232+ default :
233+ cs .stopMu .RUnlock ()
234+ return cs .Store .SetMetadata (ctx , key , value )
235+ }
236+ }
237+
238+ // DeleteMetadata queues an asynchronous metadata delete. If the buffer is full
239+ // or the store has been stopped, the delete falls back to synchronous execution.
240+ func (cs * CachedStore ) DeleteMetadata (ctx context.Context , key string ) error {
241+ cs .stopMu .RLock ()
242+ if cs .stopped {
243+ cs .stopMu .RUnlock ()
244+ return cs .Store .DeleteMetadata (ctx , key )
245+ }
246+ select {
247+ case cs .writeCh <- asyncWriteOp {key : key , isDelete : true }:
248+ cs .stopMu .RUnlock ()
249+ return nil
250+ default :
251+ cs .stopMu .RUnlock ()
252+ return cs .Store .DeleteMetadata (ctx , key )
253+ }
254+ }
255+
256+ // Close drains pending async writes, then closes the underlying store.
177257func (cs * CachedStore ) Close () error {
258+ cs .stopMu .Lock ()
259+ cs .stopped = true
260+ close (cs .writeCh )
261+ cs .stopMu .Unlock ()
262+ <- cs .done
263+
178264 cs .ClearCache ()
179265 return cs .Store .Close ()
180266}
0 commit comments