@@ -19,23 +19,35 @@ package eradb
19
19
import (
20
20
"errors"
21
21
"fmt"
22
+ "io/fs"
22
23
"os"
23
24
"path/filepath"
25
+ "sync"
26
+ "sync/atomic"
24
27
25
28
"github.com/ethereum/go-ethereum/common/lru"
26
29
"github.com/ethereum/go-ethereum/internal/era"
27
30
"github.com/ethereum/go-ethereum/log"
28
31
)
29
32
30
- const (
31
- openFileLimit = 64
32
- )
33
+ const openFileLimit = 64
34
+
35
+ var errClosed = errors . New ( "era store is closed" )
33
36
34
37
// EraDatabase manages read access to a directory of era1 files.
35
38
// The getter methods are thread-safe.
36
39
type EraDatabase struct {
37
40
datadir string
38
- cache * lru.Cache [uint64 , * era.Era ]
41
+ mu sync.Mutex
42
+ lru lru.BasicLRU [uint64 , * fileCacheEntry ]
43
+ opening map [uint64 ]* fileCacheEntry
44
+ }
45
+
46
+ type fileCacheEntry struct {
47
+ ref atomic.Int32
48
+ opened chan struct {}
49
+ file * era.Era
50
+ err error
39
51
}
40
52
41
53
// New creates a new EraDatabase instance.
@@ -56,93 +68,138 @@ func New(datadir string) (*EraDatabase, error) {
56
68
}
57
69
db := & EraDatabase {
58
70
datadir : datadir ,
59
- cache : lru.NewCache [uint64 , * era.Era ](openFileLimit ),
60
- }
61
- closeEra := func (epoch uint64 , e * era.Era ) {
62
- if e == nil {
63
- log .Warn ("Era1 cache contained nil value" , "epoch" , epoch )
64
- return
65
- }
66
- if err := e .Close (); err != nil {
67
- log .Warn ("Error closing era1 file" , "epoch" , epoch , "err" , err )
68
- }
71
+ lru : lru.NewBasicLRU [uint64 , * fileCacheEntry ](openFileLimit ),
72
+ opening : make (map [uint64 ]* fileCacheEntry ),
69
73
}
70
- // Take care to close era1 files when they are evicted from cache.
71
- db .cache .OnEvicted (closeEra )
72
-
73
- // Concurrently calling GetRaw* methods can cause the same era1 file to be
74
- // opened multiple times.
75
- db .cache .OnReplaced (closeEra )
76
-
77
74
log .Info ("Opened Era store" , "datadir" , datadir )
78
75
return db , nil
79
76
}
80
77
81
78
// Close closes all open era1 files in the cache.
82
- func (db * EraDatabase ) Close () error {
83
- // Close all open era1 files in the cache.
84
- keys := db .cache .Keys ()
85
- errs := make ([]error , len (keys ))
86
- for _ , key := range keys {
87
- if e , ok := db .cache .Get (key ); ok {
88
- if err := e .Close (); err != nil {
89
- errs = append (errs , err )
90
- }
91
- }
79
+ func (db * EraDatabase ) Close () {
80
+ db .mu .Lock ()
81
+ defer db .mu .Unlock ()
82
+
83
+ keys := db .lru .Keys ()
84
+ for _ , epoch := range keys {
85
+ entry , _ := db .lru .Get (epoch )
86
+ entry .done (epoch )
92
87
}
93
- return errors . Join ( errs ... )
88
+ db . opening = nil
94
89
}
95
90
96
91
// GetRawBody returns the raw body for a given block number.
97
92
func (db * EraDatabase ) GetRawBody (number uint64 ) ([]byte , error ) {
98
93
// Lookup the table by epoch.
99
94
epoch := number / uint64 (era .MaxEra1Size )
100
- e , err := db .getEraByEpoch (epoch )
101
- if err != nil {
102
- return nil , err
103
- }
104
- // The era1 file for given epoch may not exist.
105
- if e == nil {
106
- return nil , nil
95
+ entry := db .getEraByEpoch (epoch )
96
+ if entry .err != nil {
97
+ if errors .Is (entry .err , fs .ErrNotExist ) {
98
+ return nil , nil
99
+ }
100
+ return nil , entry .err
107
101
}
108
- return e .GetRawBodyByNumber (number )
102
+ defer entry .done (epoch )
103
+ return entry .file .GetRawBodyByNumber (number )
109
104
}
110
105
111
106
// GetRawReceipts returns the raw receipts for a given block number.
112
107
func (db * EraDatabase ) GetRawReceipts (number uint64 ) ([]byte , error ) {
113
108
epoch := number / uint64 (era .MaxEra1Size )
114
- e , err := db .getEraByEpoch (epoch )
115
- if err != nil {
116
- return nil , err
109
+ entry := db .getEraByEpoch (epoch )
110
+ if entry .err != nil {
111
+ if errors .Is (entry .err , fs .ErrNotExist ) {
112
+ return nil , nil
113
+ }
114
+ return nil , entry .err
117
115
}
118
- // The era1 file for given epoch may not exist.
119
- if e == nil {
120
- return nil , nil
116
+ defer entry .done (epoch )
117
+ return entry .file .GetRawReceiptsByNumber (number )
118
+ }
119
+
120
+ // getEraByEpoch opens an era file or gets it from the cache. The caller can access
121
+ // entry.file and entry.err and must call entry.done when done reading the file.
122
+ func (db * EraDatabase ) getEraByEpoch (epoch uint64 ) * fileCacheEntry {
123
+ // Add the requested epoch to the cache.
124
+ entry := db .getCacheEntry (epoch )
125
+ if entry == nil {
126
+ return & fileCacheEntry {err : errClosed }
127
+ }
128
+
129
+ // First goroutine to use the file has to open it.
130
+ if entry .ref .Add (1 ) == 1 {
131
+ e , err := db .openEraFile (epoch )
132
+ if err != nil {
133
+ db .fileFailedToOpen (epoch , entry , err )
134
+ } else {
135
+ db .fileOpened (epoch , entry , e )
136
+ }
137
+ close (entry .opened )
121
138
}
122
- return e .GetRawReceiptsByNumber (number )
139
+
140
+ // Bump the refcount and wait for the file to be opened.
141
+ entry .ref .Add (1 )
142
+ <- entry .opened
143
+ return entry
123
144
}
124
145
125
- func (db * EraDatabase ) openEra (name string ) (* era.Era , error ) {
126
- e , err := era .Open (name )
127
- if err != nil {
128
- return nil , err
146
+ // getCacheEntry gets an open era file from the cache.
147
+ func (db * EraDatabase ) getCacheEntry (epoch uint64 ) * fileCacheEntry {
148
+ db .mu .Lock ()
149
+ defer db .mu .Unlock ()
150
+
151
+ // Check if this epoch is already being opened.
152
+ if db .opening == nil {
153
+ return nil
129
154
}
130
- // Assign an epoch to the table.
131
- if e .Count () != uint64 (era .MaxEra1Size ) {
132
- return nil , fmt .Errorf ("pre-merge era1 files must be full. Want: %d, have: %d" , era .MaxEra1Size , e .Count ())
155
+ if entry , ok := db .opening [epoch ]; ok {
156
+ return entry
133
157
}
134
- if e .Start ()% uint64 (era .MaxEra1Size ) != 0 {
135
- return nil , fmt .Errorf ("pre-merge era1 file has invalid boundary. %d %% %d != 0" , e .Start (), era .MaxEra1Size )
158
+ // Check if it's in the cache.
159
+ if entry , ok := db .lru .Get (epoch ); ok {
160
+ return entry
136
161
}
137
- return e , nil
162
+ // It's a new file, create an entry in the 'opening' table.
163
+ entry := & fileCacheEntry {opened : make (chan struct {})}
164
+ db .opening [epoch ] = entry
165
+ return entry
138
166
}
139
167
140
- func (db * EraDatabase ) getEraByEpoch (epoch uint64 ) (* era.Era , error ) {
141
- // Check the cache first.
142
- if e , ok := db .cache .Get (epoch ); ok {
143
- return e , nil
168
+ // fileOpened is called after an era file has been successfully opened.
169
+ func (db * EraDatabase ) fileOpened (epoch uint64 , entry * fileCacheEntry , file * era.Era ) {
170
+ db .mu .Lock ()
171
+ defer db .mu .Unlock ()
172
+
173
+ // The database may have been closed while opening the file. When that happens,
174
+ // db.opening will be set to nil, so we need to handle that here and ensure the caller
175
+ // knows.
176
+ if db .opening == nil {
177
+ entry .err = errClosed
178
+ return
144
179
}
145
- // file name scheme is <network>-<epoch>-<root>.
180
+
181
+ // Remove from 'opening' table and add to the LRU.
182
+ // This may evict an existing item, which we have to close.
183
+ entry .file = file
184
+ delete (db .opening , epoch )
185
+ if _ , evictedEntry , _ := db .lru .Add3 (epoch , entry ); evictedEntry != nil {
186
+ evictedEntry .done (epoch )
187
+ }
188
+ }
189
+
190
+ // fileFailedToOpen is called when an era file could not be opened.
191
+ func (db * EraDatabase ) fileFailedToOpen (epoch uint64 , entry * fileCacheEntry , err error ) {
192
+ entry .err = err
193
+
194
+ db .mu .Lock ()
195
+ defer db .mu .Unlock ()
196
+ if db .opening != nil {
197
+ delete (db .opening , epoch )
198
+ }
199
+ }
200
+
201
+ func (db * EraDatabase ) openEraFile (epoch uint64 ) (* era.Era , error ) {
202
+ // File name scheme is <network>-<epoch>-<root>.
146
203
glob := fmt .Sprintf ("*-%05d-*.era1" , epoch )
147
204
matches , err := filepath .Glob (filepath .Join (db .datadir , glob ))
148
205
if err != nil {
@@ -155,11 +212,33 @@ func (db *EraDatabase) getEraByEpoch(epoch uint64) (*era.Era, error) {
155
212
return nil , nil
156
213
}
157
214
filename := matches [0 ]
158
- e , err := db .openEra (filename )
215
+
216
+ e , err := era .Open (filename )
159
217
if err != nil {
160
218
return nil , err
161
219
}
162
- // Add the era to the cache.
163
- db .cache .Add (epoch , e )
220
+ // Assign an epoch to the table.
221
+ if e .Count () != uint64 (era .MaxEra1Size ) {
222
+ return nil , fmt .Errorf ("pre-merge era1 files must be full. Want: %d, have: %d" , era .MaxEra1Size , e .Count ())
223
+ }
224
+ if e .Start ()% uint64 (era .MaxEra1Size ) != 0 {
225
+ return nil , fmt .Errorf ("pre-merge era1 file has invalid boundary. %d %% %d != 0" , e .Start (), era .MaxEra1Size )
226
+ }
164
227
return e , nil
165
228
}
229
+
230
+ // done signals that the caller has finished using a file.
231
+ // This decrements the refcount and ensures the file is closed by the last user.
232
+ func (f * fileCacheEntry ) done (epoch uint64 ) {
233
+ if f .err != nil {
234
+ return
235
+ }
236
+ if f .ref .Add (- 1 ) == 0 {
237
+ err := f .file .Close ()
238
+ if err == nil {
239
+ log .Debug ("Closed era1 file" , "epoch" , epoch )
240
+ } else {
241
+ log .Warn ("Error closing era1 file" , "epoch" , epoch , "err" , err )
242
+ }
243
+ }
244
+ }
0 commit comments