Skip to content

Commit 160cd29

Browse files
authored
Merge pull request #497 from ahrtr/split_surgery_freelist_20230516
cmd: split 'surgery freelist' into separate files
2 parents 9451390 + c2efe9f commit 160cd29

5 files changed

+277
-240
lines changed

cmd/bbolt/command_surgery.go

-116
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"github.com/spf13/cobra"
99
"github.com/spf13/pflag"
1010

11-
bolt "go.etcd.io/bbolt"
1211
"go.etcd.io/bbolt/internal/common"
1312
"go.etcd.io/bbolt/internal/guts_cli"
1413
"go.etcd.io/bbolt/internal/surgeon"
@@ -311,121 +310,6 @@ func surgeryClearPageElementFunc(srcDBPath string, cfg surgeryClearPageElementsO
311310
return nil
312311
}
313312

314-
func newSurgeryFreelistCommand() *cobra.Command {
315-
cmd := &cobra.Command{
316-
Use: "freelist <subcommand>",
317-
Short: "freelist related surgery commands",
318-
}
319-
320-
cmd.AddCommand(newSurgeryFreelistAbandonCommand())
321-
cmd.AddCommand(newSurgeryFreelistRebuildCommand())
322-
323-
return cmd
324-
}
325-
326-
func newSurgeryFreelistAbandonCommand() *cobra.Command {
327-
var o surgeryBaseOptions
328-
abandonFreelistCmd := &cobra.Command{
329-
Use: "abandon <bbolt-file> [options]",
330-
Short: "Abandon the freelist from both meta pages",
331-
Args: func(cmd *cobra.Command, args []string) error {
332-
if len(args) == 0 {
333-
return errors.New("db file path not provided")
334-
}
335-
if len(args) > 1 {
336-
return errors.New("too many arguments")
337-
}
338-
return nil
339-
},
340-
RunE: func(cmd *cobra.Command, args []string) error {
341-
if err := o.Validate(); err != nil {
342-
return err
343-
}
344-
return surgeryFreelistAbandonFunc(args[0], o)
345-
},
346-
}
347-
o.AddFlags(abandonFreelistCmd.Flags())
348-
349-
return abandonFreelistCmd
350-
}
351-
352-
func surgeryFreelistAbandonFunc(srcDBPath string, cfg surgeryBaseOptions) error {
353-
if _, err := checkSourceDBPath(srcDBPath); err != nil {
354-
return err
355-
}
356-
357-
if err := common.CopyFile(srcDBPath, cfg.outputDBFilePath); err != nil {
358-
return fmt.Errorf("[freelist abandon] copy file failed: %w", err)
359-
}
360-
361-
if err := surgeon.ClearFreelist(cfg.outputDBFilePath); err != nil {
362-
return fmt.Errorf("abandom-freelist command failed: %w", err)
363-
}
364-
365-
fmt.Fprintf(os.Stdout, "The freelist was abandoned in both meta pages.\nIt may cause some delay on next startup because bbolt needs to scan the whole db to reconstruct the free list.\n")
366-
return nil
367-
}
368-
369-
func newSurgeryFreelistRebuildCommand() *cobra.Command {
370-
var o surgeryBaseOptions
371-
rebuildFreelistCmd := &cobra.Command{
372-
Use: "rebuild <bbolt-file> [options]",
373-
Short: "Rebuild the freelist",
374-
Args: func(cmd *cobra.Command, args []string) error {
375-
if len(args) == 0 {
376-
return errors.New("db file path not provided")
377-
}
378-
if len(args) > 1 {
379-
return errors.New("too many arguments")
380-
}
381-
return nil
382-
},
383-
RunE: func(cmd *cobra.Command, args []string) error {
384-
if err := o.Validate(); err != nil {
385-
return err
386-
}
387-
return surgeryFreelistRebuildFunc(args[0], o)
388-
},
389-
}
390-
o.AddFlags(rebuildFreelistCmd.Flags())
391-
392-
return rebuildFreelistCmd
393-
}
394-
395-
func surgeryFreelistRebuildFunc(srcDBPath string, cfg surgeryBaseOptions) error {
396-
// Ensure source file exists.
397-
fi, err := checkSourceDBPath(srcDBPath)
398-
if err != nil {
399-
return err
400-
}
401-
402-
// make sure the freelist isn't present in the file.
403-
meta, err := readMetaPage(srcDBPath)
404-
if err != nil {
405-
return err
406-
}
407-
if meta.IsFreelistPersisted() {
408-
return ErrSurgeryFreelistAlreadyExist
409-
}
410-
411-
if err := common.CopyFile(srcDBPath, cfg.outputDBFilePath); err != nil {
412-
return fmt.Errorf("[freelist rebuild] copy file failed: %w", err)
413-
}
414-
415-
// bboltDB automatically reconstruct & sync freelist in write mode.
416-
db, err := bolt.Open(cfg.outputDBFilePath, fi.Mode(), &bolt.Options{NoFreelistSync: false})
417-
if err != nil {
418-
return fmt.Errorf("[freelist rebuild] open db file failed: %w", err)
419-
}
420-
err = db.Close()
421-
if err != nil {
422-
return fmt.Errorf("[freelist rebuild] close db file failed: %w", err)
423-
}
424-
425-
fmt.Fprintf(os.Stdout, "The freelist was successfully rebuilt.\n")
426-
return nil
427-
}
428-
429313
func readMetaPage(path string) (*common.Meta, error) {
430314
_, activeMetaPageId, err := guts_cli.GetRootPage(path)
431315
if err != nil {

cmd/bbolt/command_surgery_freelist.go

+128
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package main
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"os"
7+
8+
"github.com/spf13/cobra"
9+
10+
bolt "go.etcd.io/bbolt"
11+
"go.etcd.io/bbolt/internal/common"
12+
"go.etcd.io/bbolt/internal/surgeon"
13+
)
14+
15+
func newSurgeryFreelistCommand() *cobra.Command {
16+
cmd := &cobra.Command{
17+
Use: "freelist <subcommand>",
18+
Short: "freelist related surgery commands",
19+
}
20+
21+
cmd.AddCommand(newSurgeryFreelistAbandonCommand())
22+
cmd.AddCommand(newSurgeryFreelistRebuildCommand())
23+
24+
return cmd
25+
}
26+
27+
func newSurgeryFreelistAbandonCommand() *cobra.Command {
28+
var o surgeryBaseOptions
29+
abandonFreelistCmd := &cobra.Command{
30+
Use: "abandon <bbolt-file> [options]",
31+
Short: "Abandon the freelist from both meta pages",
32+
Args: func(cmd *cobra.Command, args []string) error {
33+
if len(args) == 0 {
34+
return errors.New("db file path not provided")
35+
}
36+
if len(args) > 1 {
37+
return errors.New("too many arguments")
38+
}
39+
return nil
40+
},
41+
RunE: func(cmd *cobra.Command, args []string) error {
42+
if err := o.Validate(); err != nil {
43+
return err
44+
}
45+
return surgeryFreelistAbandonFunc(args[0], o)
46+
},
47+
}
48+
o.AddFlags(abandonFreelistCmd.Flags())
49+
50+
return abandonFreelistCmd
51+
}
52+
53+
func surgeryFreelistAbandonFunc(srcDBPath string, cfg surgeryBaseOptions) error {
54+
if _, err := checkSourceDBPath(srcDBPath); err != nil {
55+
return err
56+
}
57+
58+
if err := common.CopyFile(srcDBPath, cfg.outputDBFilePath); err != nil {
59+
return fmt.Errorf("[freelist abandon] copy file failed: %w", err)
60+
}
61+
62+
if err := surgeon.ClearFreelist(cfg.outputDBFilePath); err != nil {
63+
return fmt.Errorf("abandom-freelist command failed: %w", err)
64+
}
65+
66+
fmt.Fprintf(os.Stdout, "The freelist was abandoned in both meta pages.\nIt may cause some delay on next startup because bbolt needs to scan the whole db to reconstruct the free list.\n")
67+
return nil
68+
}
69+
70+
func newSurgeryFreelistRebuildCommand() *cobra.Command {
71+
var o surgeryBaseOptions
72+
rebuildFreelistCmd := &cobra.Command{
73+
Use: "rebuild <bbolt-file> [options]",
74+
Short: "Rebuild the freelist",
75+
Args: func(cmd *cobra.Command, args []string) error {
76+
if len(args) == 0 {
77+
return errors.New("db file path not provided")
78+
}
79+
if len(args) > 1 {
80+
return errors.New("too many arguments")
81+
}
82+
return nil
83+
},
84+
RunE: func(cmd *cobra.Command, args []string) error {
85+
if err := o.Validate(); err != nil {
86+
return err
87+
}
88+
return surgeryFreelistRebuildFunc(args[0], o)
89+
},
90+
}
91+
o.AddFlags(rebuildFreelistCmd.Flags())
92+
93+
return rebuildFreelistCmd
94+
}
95+
96+
func surgeryFreelistRebuildFunc(srcDBPath string, cfg surgeryBaseOptions) error {
97+
// Ensure source file exists.
98+
fi, err := checkSourceDBPath(srcDBPath)
99+
if err != nil {
100+
return err
101+
}
102+
103+
// make sure the freelist isn't present in the file.
104+
meta, err := readMetaPage(srcDBPath)
105+
if err != nil {
106+
return err
107+
}
108+
if meta.IsFreelistPersisted() {
109+
return ErrSurgeryFreelistAlreadyExist
110+
}
111+
112+
if err := common.CopyFile(srcDBPath, cfg.outputDBFilePath); err != nil {
113+
return fmt.Errorf("[freelist rebuild] copy file failed: %w", err)
114+
}
115+
116+
// bboltDB automatically reconstruct & sync freelist in write mode.
117+
db, err := bolt.Open(cfg.outputDBFilePath, fi.Mode(), &bolt.Options{NoFreelistSync: false})
118+
if err != nil {
119+
return fmt.Errorf("[freelist rebuild] open db file failed: %w", err)
120+
}
121+
err = db.Close()
122+
if err != nil {
123+
return fmt.Errorf("[freelist rebuild] close db file failed: %w", err)
124+
}
125+
126+
fmt.Fprintf(os.Stdout, "The freelist was successfully rebuilt.\n")
127+
return nil
128+
}
+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package main_test
2+
3+
import (
4+
"path/filepath"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
10+
bolt "go.etcd.io/bbolt"
11+
main "go.etcd.io/bbolt/cmd/bbolt"
12+
"go.etcd.io/bbolt/internal/btesting"
13+
"go.etcd.io/bbolt/internal/common"
14+
)
15+
16+
func TestSurgery_Freelist_Abandon(t *testing.T) {
17+
pageSize := 4096
18+
db := btesting.MustCreateDBWithOption(t, &bolt.Options{PageSize: pageSize})
19+
srcPath := db.Path()
20+
21+
defer requireDBNoChange(t, dbData(t, srcPath), srcPath)
22+
23+
rootCmd := main.NewRootCommand()
24+
output := filepath.Join(t.TempDir(), "db")
25+
rootCmd.SetArgs([]string{
26+
"surgery", "freelist", "abandon", srcPath,
27+
"--output", output,
28+
})
29+
err := rootCmd.Execute()
30+
require.NoError(t, err)
31+
32+
meta0 := loadMetaPage(t, output, 0)
33+
assert.Equal(t, common.PgidNoFreelist, meta0.Freelist())
34+
meta1 := loadMetaPage(t, output, 1)
35+
assert.Equal(t, common.PgidNoFreelist, meta1.Freelist())
36+
}
37+
38+
func TestSurgery_Freelist_Rebuild(t *testing.T) {
39+
testCases := []struct {
40+
name string
41+
hasFreelist bool
42+
expectedError error
43+
}{
44+
{
45+
name: "normal operation",
46+
hasFreelist: false,
47+
expectedError: nil,
48+
},
49+
{
50+
name: "already has freelist",
51+
hasFreelist: true,
52+
expectedError: main.ErrSurgeryFreelistAlreadyExist,
53+
},
54+
}
55+
56+
for _, tc := range testCases {
57+
tc := tc
58+
t.Run(tc.name, func(t *testing.T) {
59+
pageSize := 4096
60+
db := btesting.MustCreateDBWithOption(t, &bolt.Options{
61+
PageSize: pageSize,
62+
NoFreelistSync: !tc.hasFreelist,
63+
})
64+
srcPath := db.Path()
65+
66+
err := db.Update(func(tx *bolt.Tx) error {
67+
// do nothing
68+
return nil
69+
})
70+
require.NoError(t, err)
71+
72+
defer requireDBNoChange(t, dbData(t, srcPath), srcPath)
73+
74+
// Verify the freelist isn't synced in the beginning
75+
meta := readMetaPage(t, srcPath)
76+
if tc.hasFreelist {
77+
if meta.Freelist() <= 1 || meta.Freelist() >= meta.Pgid() {
78+
t.Fatalf("freelist (%d) isn't in the valid range (1, %d)", meta.Freelist(), meta.Pgid())
79+
}
80+
} else {
81+
require.Equal(t, common.PgidNoFreelist, meta.Freelist())
82+
}
83+
84+
// Execute `surgery freelist rebuild` command
85+
rootCmd := main.NewRootCommand()
86+
output := filepath.Join(t.TempDir(), "db")
87+
rootCmd.SetArgs([]string{
88+
"surgery", "freelist", "rebuild", srcPath,
89+
"--output", output,
90+
})
91+
err = rootCmd.Execute()
92+
require.Equal(t, tc.expectedError, err)
93+
94+
if tc.expectedError == nil {
95+
// Verify the freelist has already been rebuilt.
96+
meta = readMetaPage(t, output)
97+
if meta.Freelist() <= 1 || meta.Freelist() >= meta.Pgid() {
98+
t.Fatalf("freelist (%d) isn't in the valid range (1, %d)", meta.Freelist(), meta.Pgid())
99+
}
100+
}
101+
})
102+
}
103+
}

0 commit comments

Comments
 (0)