Skip to content

Commit 6ee9f1d

Browse files
authored
Merge pull request #389 from ahrtr/surgery_write_empty_page_20230120
Add `surgery clear-page` command
2 parents 774edab + 834868d commit 6ee9f1d

File tree

5 files changed

+137
-19
lines changed

5 files changed

+137
-19
lines changed

cmd/bbolt/surgery_commands.go

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ func (cmd *surgeryCommand) Run(args ...string) error {
4545
return newRevertMetaPageCommand(cmd).Run(args[1:]...)
4646
case "copy-page":
4747
return newCopyPageCommand(cmd).Run(args[1:]...)
48+
case "clear-page":
49+
return newClearPageCommand(cmd).Run(args[1:]...)
4850
default:
4951
return ErrUnknownCommand
5052
}
@@ -225,7 +227,7 @@ func (cmd *copyPageCommand) Run(args ...string) error {
225227
return fmt.Errorf("copyPageCommand failed: %w", err)
226228
}
227229

228-
fmt.Fprintf(cmd.Stdout, "The page %d was copied to page %d", srcPageId, dstPageId)
230+
fmt.Fprintf(cmd.Stdout, "The page %d was copied to page %d\n", srcPageId, dstPageId)
229231
return nil
230232
}
231233

@@ -241,3 +243,58 @@ at dstPageId in DST.
241243
The original database is left untouched.
242244
`, "\n")
243245
}
246+
247+
// clearPageCommand represents the "surgery clear-page" command execution.
248+
type clearPageCommand struct {
249+
*surgeryCommand
250+
}
251+
252+
// newClearPageCommand returns a clearPageCommand.
253+
func newClearPageCommand(m *surgeryCommand) *clearPageCommand {
254+
c := &clearPageCommand{}
255+
c.surgeryCommand = m
256+
return c
257+
}
258+
259+
// Run executes the command.
260+
func (cmd *clearPageCommand) Run(args ...string) error {
261+
// Parse flags.
262+
fs := flag.NewFlagSet("", flag.ContinueOnError)
263+
help := fs.Bool("h", false, "")
264+
if err := fs.Parse(args); err != nil {
265+
return err
266+
} else if *help {
267+
fmt.Fprintln(cmd.Stderr, cmd.Usage())
268+
return ErrUsage
269+
}
270+
271+
if err := cmd.parsePathsAndCopyFile(fs); err != nil {
272+
return fmt.Errorf("clearPageCommand failed to parse paths and copy file: %w", err)
273+
}
274+
275+
// Read page id.
276+
pageId, err := strconv.ParseUint(fs.Arg(2), 10, 64)
277+
if err != nil {
278+
return err
279+
}
280+
281+
if err := surgeon.ClearPage(cmd.dstPath, guts_cli.Pgid(pageId)); err != nil {
282+
return fmt.Errorf("clearPageCommand failed: %w", err)
283+
}
284+
285+
fmt.Fprintf(cmd.Stdout, "Page (%d) was cleared\n", pageId)
286+
return nil
287+
}
288+
289+
// Usage returns the help message.
290+
func (cmd *clearPageCommand) Usage() string {
291+
return strings.TrimLeft(`
292+
usage: bolt surgery clear-page SRC DST pageId
293+
294+
ClearPage copies the database file at SRC to a newly created database
295+
file at DST. Afterwards, it clears all elements in the page at pageId
296+
in DST.
297+
298+
The original database is left untouched.
299+
`, "\n")
300+
}

cmd/bbolt/surgery_commands_test.go

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ func TestSurgery_CopyPage(t *testing.T) {
7070

7171
defer requireDBNoChange(t, dbData(t, srcPath), srcPath)
7272

73-
// revert the meta page
73+
// copy page 3 to page 2
7474
t.Log("copy page 3 to page 2")
7575
dstPath := filepath.Join(t.TempDir(), "dstdb")
7676
m := NewMain()
@@ -87,6 +87,39 @@ func TestSurgery_CopyPage(t *testing.T) {
8787
assert.Equal(t, pageDataWithoutPageId(srcPageId3Data), pageDataWithoutPageId(dstPageId2Data))
8888
}
8989

90+
// TODO(ahrtr): add test case below for `surgery clear-page` command:
91+
// 1. The page is a branch page. All its children should become free pages.
92+
func TestSurgery_ClearPage(t *testing.T) {
93+
pageSize := 4096
94+
db := btesting.MustCreateDBWithOption(t, &bolt.Options{PageSize: pageSize})
95+
srcPath := db.Path()
96+
97+
// Insert some sample data
98+
t.Log("Insert some sample data")
99+
err := db.Fill([]byte("data"), 1, 20,
100+
func(tx int, k int) []byte { return []byte(fmt.Sprintf("%04d", k)) },
101+
func(tx int, k int) []byte { return make([]byte, 10) },
102+
)
103+
require.NoError(t, err)
104+
105+
defer requireDBNoChange(t, dbData(t, srcPath), srcPath)
106+
107+
// clear page 3
108+
t.Log("clear page 3")
109+
dstPath := filepath.Join(t.TempDir(), "dstdb")
110+
m := NewMain()
111+
err = m.Run("surgery", "clear-page", srcPath, dstPath, "3")
112+
require.NoError(t, err)
113+
114+
// The page 2 should have exactly the same data as page 3.
115+
t.Log("Verify result")
116+
dstPageId3Data := readPage(t, dstPath, 3, pageSize)
117+
118+
p := guts_cli.LoadPage(dstPageId3Data)
119+
assert.Equal(t, uint16(0), p.Count())
120+
assert.Equal(t, uint32(0), p.Overflow())
121+
}
122+
90123
func readPage(t *testing.T, path string, pageId int, pageSize int) []byte {
91124
dbFile, err := os.Open(path)
92125
require.NoError(t, err)

internal/guts_cli/guts_cli.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,10 @@ func (p *Page) Overflow() uint32 {
139139
return p.overflow
140140
}
141141

142+
func (p *Page) String() string {
143+
return fmt.Sprintf("ID: %d, Type: %s, count: %d, overflow: %d", p.id, p.Type(), p.count, p.overflow)
144+
}
145+
142146
// DO NOT EDIT. Copied from the "bolt" package.
143147

144148
// TODO(ptabor): Make the page-types an enum.
@@ -178,6 +182,14 @@ func (p *Page) SetId(target Pgid) {
178182
p.id = target
179183
}
180184

185+
func (p *Page) SetCount(target uint16) {
186+
p.count = target
187+
}
188+
189+
func (p *Page) SetOverflow(target uint32) {
190+
p.overflow = target
191+
}
192+
181193
// DO NOT EDIT. Copied from the "bolt" package.
182194
type BranchPageElement struct {
183195
pos uint32
@@ -276,6 +288,25 @@ func ReadPage(path string, pageID uint64) (*Page, []byte, error) {
276288
return p, buf, nil
277289
}
278290

291+
func WritePage(path string, pageBuf []byte) error {
292+
page := LoadPage(pageBuf)
293+
pageSize, _, err := ReadPageAndHWMSize(path)
294+
if err != nil {
295+
return err
296+
}
297+
expectedLen := pageSize * (uint64(page.Overflow()) + 1)
298+
if expectedLen != uint64(len(pageBuf)) {
299+
return fmt.Errorf("WritePage: len(buf):%d != pageSize*(overflow+1):%d", len(pageBuf), expectedLen)
300+
}
301+
f, err := os.OpenFile(path, os.O_WRONLY, 0)
302+
if err != nil {
303+
return err
304+
}
305+
defer f.Close()
306+
_, err = f.WriteAt(pageBuf, int64(page.Id())*int64(pageSize))
307+
return err
308+
}
309+
279310
// ReadPageAndHWMSize reads Page size and HWM (id of the last+1 Page).
280311
// This is not transactionally safe.
281312
func ReadPageAndHWMSize(path string) (uint64, Pgid, error) {

internal/surgeon/surgeon.go

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ package surgeon
22

33
import (
44
"fmt"
5-
"os"
6-
75
"go.etcd.io/bbolt/internal/guts_cli"
86
)
97

@@ -13,25 +11,24 @@ func CopyPage(path string, srcPage guts_cli.Pgid, target guts_cli.Pgid) error {
1311
return err1
1412
}
1513
p1.SetId(target)
16-
return WritePage(path, d1)
14+
return guts_cli.WritePage(path, d1)
1715
}
1816

19-
func WritePage(path string, pageBuf []byte) error {
20-
page := guts_cli.LoadPage(pageBuf)
21-
pageSize, _, err := guts_cli.ReadPageAndHWMSize(path)
17+
func ClearPage(path string, pgId guts_cli.Pgid) error {
18+
// Read the page
19+
p, buf, err := guts_cli.ReadPage(path, uint64(pgId))
2220
if err != nil {
23-
return err
21+
return fmt.Errorf("ReadPage failed: %w", err)
2422
}
25-
if pageSize != uint64(len(pageBuf)) {
26-
return fmt.Errorf("WritePage: len(buf)=%d != pageSize=%d", len(pageBuf), pageSize)
27-
}
28-
f, err := os.OpenFile(path, os.O_WRONLY, 0)
29-
if err != nil {
30-
return err
23+
24+
// Update and rewrite the page
25+
p.SetCount(0)
26+
p.SetOverflow(0)
27+
if err := guts_cli.WritePage(path, buf); err != nil {
28+
return fmt.Errorf("WritePage failed: %w", err)
3129
}
32-
defer f.Close()
33-
_, err = f.WriteAt(pageBuf, int64(page.Id())*int64(pageSize))
34-
return err
30+
31+
return nil
3532
}
3633

3734
// RevertMetaPage replaces the newer metadata page with the older.

internal/tests/tx_check_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ func TestTx_RecursivelyCheckPages_CorruptedLeaf(t *testing.T) {
6969
require.NoError(t, err)
7070
require.Positive(t, p.Count(), "page must be not empty")
7171
p.LeafPageElement(p.Count() / 2).Key()[0] = 'z'
72-
require.NoError(t, surgeon.WritePage(db.Path(), pbuf))
72+
require.NoError(t, guts_cli.WritePage(db.Path(), pbuf))
7373

7474
db.MustReopen()
7575
require.NoError(t, db.Update(func(tx *bolt.Tx) error {

0 commit comments

Comments
 (0)