Skip to content

Commit 79a478b

Browse files
authored
core/rawdb: implement resettable freezer (#26324)
This PR implements resettable freezer by adding a ResettableFreezer wrapper. The resettable freezer wraps the original freezer in a way that makes it possible to ensure atomic resets. Implementation wise, it relies on the os.Rename and os.RemoveAll to atomically delete the original freezer data and re-create a new one from scratch.
1 parent d3411b9 commit 79a478b

File tree

2 files changed

+340
-0
lines changed

2 files changed

+340
-0
lines changed

core/rawdb/freezer_resettable.go

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
// Copyright 2022 The go-ethereum Authors
2+
// This file is part of the go-ethereum library.
3+
//
4+
// The go-ethereum library is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Lesser General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// The go-ethereum library is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Lesser General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Lesser General Public License
15+
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
16+
17+
package rawdb
18+
19+
import (
20+
"os"
21+
"path/filepath"
22+
"sync"
23+
24+
"github.com/ethereum/go-ethereum/ethdb"
25+
)
26+
27+
const tmpSuffix = ".tmp"
28+
29+
// freezerOpenFunc is the function used to open/create a freezer.
30+
type freezerOpenFunc = func() (*Freezer, error)
31+
32+
// ResettableFreezer is a wrapper of the freezer which makes the
33+
// freezer resettable.
34+
type ResettableFreezer struct {
35+
freezer *Freezer
36+
opener freezerOpenFunc
37+
datadir string
38+
lock sync.RWMutex
39+
}
40+
41+
// NewResettableFreezer creates a resettable freezer, note freezer is
42+
// only resettable if the passed file directory is exclusively occupied
43+
// by the freezer. And also the user-configurable ancient root directory
44+
// is **not** supported for reset since it might be a mount and rename
45+
// will cause a copy of hundreds of gigabyte into local directory. It
46+
// needs some other file based solutions.
47+
//
48+
// The reset function will delete directory atomically and re-create the
49+
// freezer from scratch.
50+
func NewResettableFreezer(datadir string, namespace string, readonly bool, maxTableSize uint32, tables map[string]bool) (*ResettableFreezer, error) {
51+
if err := cleanup(datadir); err != nil {
52+
return nil, err
53+
}
54+
opener := func() (*Freezer, error) {
55+
return NewFreezer(datadir, namespace, readonly, maxTableSize, tables)
56+
}
57+
freezer, err := opener()
58+
if err != nil {
59+
return nil, err
60+
}
61+
return &ResettableFreezer{
62+
freezer: freezer,
63+
opener: opener,
64+
datadir: datadir,
65+
}, nil
66+
}
67+
68+
// Reset deletes the file directory exclusively occupied by the freezer and
69+
// recreate the freezer from scratch. The atomicity of directory deletion
70+
// is guaranteed by the rename operation, the leftover directory will be
71+
// cleaned up in next startup in case crash happens after rename.
72+
func (f *ResettableFreezer) Reset() error {
73+
f.lock.Lock()
74+
defer f.lock.Unlock()
75+
76+
if err := f.freezer.Close(); err != nil {
77+
return err
78+
}
79+
tmp := tmpName(f.datadir)
80+
if err := os.Rename(f.datadir, tmp); err != nil {
81+
return err
82+
}
83+
if err := os.RemoveAll(tmp); err != nil {
84+
return err
85+
}
86+
freezer, err := f.opener()
87+
if err != nil {
88+
return err
89+
}
90+
f.freezer = freezer
91+
return nil
92+
}
93+
94+
// Close terminates the chain freezer, unmapping all the data files.
95+
func (f *ResettableFreezer) Close() error {
96+
f.lock.RLock()
97+
defer f.lock.RUnlock()
98+
99+
return f.freezer.Close()
100+
}
101+
102+
// HasAncient returns an indicator whether the specified ancient data exists
103+
// in the freezer
104+
func (f *ResettableFreezer) HasAncient(kind string, number uint64) (bool, error) {
105+
f.lock.RLock()
106+
defer f.lock.RUnlock()
107+
108+
return f.freezer.HasAncient(kind, number)
109+
}
110+
111+
// Ancient retrieves an ancient binary blob from the append-only immutable files.
112+
func (f *ResettableFreezer) Ancient(kind string, number uint64) ([]byte, error) {
113+
f.lock.RLock()
114+
defer f.lock.RUnlock()
115+
116+
return f.freezer.Ancient(kind, number)
117+
}
118+
119+
// AncientRange retrieves multiple items in sequence, starting from the index 'start'.
120+
// It will return
121+
// - at most 'max' items,
122+
// - at least 1 item (even if exceeding the maxByteSize), but will otherwise
123+
// return as many items as fit into maxByteSize
124+
func (f *ResettableFreezer) AncientRange(kind string, start, count, maxBytes uint64) ([][]byte, error) {
125+
f.lock.RLock()
126+
defer f.lock.RUnlock()
127+
128+
return f.freezer.AncientRange(kind, start, count, maxBytes)
129+
}
130+
131+
// Ancients returns the length of the frozen items.
132+
func (f *ResettableFreezer) Ancients() (uint64, error) {
133+
f.lock.RLock()
134+
defer f.lock.RUnlock()
135+
136+
return f.freezer.Ancients()
137+
}
138+
139+
// Tail returns the number of first stored item in the freezer.
140+
func (f *ResettableFreezer) Tail() (uint64, error) {
141+
f.lock.RLock()
142+
defer f.lock.RUnlock()
143+
144+
return f.freezer.Tail()
145+
}
146+
147+
// AncientSize returns the ancient size of the specified category.
148+
func (f *ResettableFreezer) AncientSize(kind string) (uint64, error) {
149+
f.lock.RLock()
150+
defer f.lock.RUnlock()
151+
152+
return f.freezer.AncientSize(kind)
153+
}
154+
155+
// ReadAncients runs the given read operation while ensuring that no writes take place
156+
// on the underlying freezer.
157+
func (f *ResettableFreezer) ReadAncients(fn func(ethdb.AncientReaderOp) error) (err error) {
158+
f.lock.RLock()
159+
defer f.lock.RUnlock()
160+
161+
return f.freezer.ReadAncients(fn)
162+
}
163+
164+
// ModifyAncients runs the given write operation.
165+
func (f *ResettableFreezer) ModifyAncients(fn func(ethdb.AncientWriteOp) error) (writeSize int64, err error) {
166+
f.lock.RLock()
167+
defer f.lock.RUnlock()
168+
169+
return f.freezer.ModifyAncients(fn)
170+
}
171+
172+
// TruncateHead discards any recent data above the provided threshold number.
173+
func (f *ResettableFreezer) TruncateHead(items uint64) error {
174+
f.lock.RLock()
175+
defer f.lock.RUnlock()
176+
177+
return f.freezer.TruncateHead(items)
178+
}
179+
180+
// TruncateTail discards any recent data below the provided threshold number.
181+
func (f *ResettableFreezer) TruncateTail(tail uint64) error {
182+
f.lock.RLock()
183+
defer f.lock.RUnlock()
184+
185+
return f.freezer.TruncateTail(tail)
186+
}
187+
188+
// Sync flushes all data tables to disk.
189+
func (f *ResettableFreezer) Sync() error {
190+
f.lock.RLock()
191+
defer f.lock.RUnlock()
192+
193+
return f.freezer.Sync()
194+
}
195+
196+
// MigrateTable processes the entries in a given table in sequence
197+
// converting them to a new format if they're of an old format.
198+
func (f *ResettableFreezer) MigrateTable(kind string, convert convertLegacyFn) error {
199+
f.lock.RLock()
200+
defer f.lock.RUnlock()
201+
202+
return f.freezer.MigrateTable(kind, convert)
203+
}
204+
205+
// cleanup removes the directory located in the specified path
206+
// has the name with deletion marker suffix.
207+
func cleanup(path string) error {
208+
parent := filepath.Dir(path)
209+
if _, err := os.Lstat(parent); os.IsNotExist(err) {
210+
return nil
211+
}
212+
dir, err := os.Open(parent)
213+
if err != nil {
214+
return err
215+
}
216+
names, err := dir.Readdirnames(0)
217+
if err != nil {
218+
return err
219+
}
220+
if cerr := dir.Close(); cerr != nil {
221+
return cerr
222+
}
223+
for _, name := range names {
224+
if name == filepath.Base(path)+tmpSuffix {
225+
return os.RemoveAll(filepath.Join(parent, name))
226+
}
227+
}
228+
return nil
229+
}
230+
231+
func tmpName(path string) string {
232+
return filepath.Join(filepath.Dir(path), filepath.Base(path)+tmpSuffix)
233+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// Copyright 2022 The go-ethereum Authors
2+
// This file is part of the go-ethereum library.
3+
//
4+
// The go-ethereum library is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Lesser General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// The go-ethereum library is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Lesser General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Lesser General Public License
15+
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
16+
17+
package rawdb
18+
19+
import (
20+
"bytes"
21+
"os"
22+
"testing"
23+
24+
"github.com/ethereum/go-ethereum/ethdb"
25+
)
26+
27+
func TestResetFreezer(t *testing.T) {
28+
items := []struct {
29+
id uint64
30+
blob []byte
31+
}{
32+
{0, bytes.Repeat([]byte{0}, 2048)},
33+
{1, bytes.Repeat([]byte{1}, 2048)},
34+
{2, bytes.Repeat([]byte{2}, 2048)},
35+
}
36+
f, _ := NewResettableFreezer(t.TempDir(), "", false, 2048, freezerTestTableDef)
37+
defer f.Close()
38+
39+
f.ModifyAncients(func(op ethdb.AncientWriteOp) error {
40+
for _, item := range items {
41+
op.AppendRaw("test", item.id, item.blob)
42+
}
43+
return nil
44+
})
45+
for _, item := range items {
46+
blob, _ := f.Ancient("test", item.id)
47+
if !bytes.Equal(blob, item.blob) {
48+
t.Fatal("Unexpected blob")
49+
}
50+
}
51+
52+
// Reset freezer
53+
f.Reset()
54+
count, _ := f.Ancients()
55+
if count != 0 {
56+
t.Fatal("Failed to reset freezer")
57+
}
58+
for _, item := range items {
59+
blob, _ := f.Ancient("test", item.id)
60+
if len(blob) != 0 {
61+
t.Fatal("Unexpected blob")
62+
}
63+
}
64+
65+
// Fill the freezer
66+
f.ModifyAncients(func(op ethdb.AncientWriteOp) error {
67+
for _, item := range items {
68+
op.AppendRaw("test", item.id, item.blob)
69+
}
70+
return nil
71+
})
72+
for _, item := range items {
73+
blob, _ := f.Ancient("test", item.id)
74+
if !bytes.Equal(blob, item.blob) {
75+
t.Fatal("Unexpected blob")
76+
}
77+
}
78+
}
79+
80+
func TestFreezerCleanup(t *testing.T) {
81+
items := []struct {
82+
id uint64
83+
blob []byte
84+
}{
85+
{0, bytes.Repeat([]byte{0}, 2048)},
86+
{1, bytes.Repeat([]byte{1}, 2048)},
87+
{2, bytes.Repeat([]byte{2}, 2048)},
88+
}
89+
datadir := t.TempDir()
90+
f, _ := NewResettableFreezer(datadir, "", false, 2048, freezerTestTableDef)
91+
f.ModifyAncients(func(op ethdb.AncientWriteOp) error {
92+
for _, item := range items {
93+
op.AppendRaw("test", item.id, item.blob)
94+
}
95+
return nil
96+
})
97+
f.Close()
98+
os.Rename(datadir, tmpName(datadir))
99+
100+
// Open the freezer again, trigger cleanup operation
101+
f, _ = NewResettableFreezer(datadir, "", false, 2048, freezerTestTableDef)
102+
f.Close()
103+
104+
if _, err := os.Lstat(tmpName(datadir)); !os.IsNotExist(err) {
105+
t.Fatal("Failed to cleanup leftover directory")
106+
}
107+
}

0 commit comments

Comments
 (0)