Repository: vanus-labs/vanus
Pull request #322 (2022-12-08 11:13:20)

feat: vsrepair tools
<!--

Thank you for contributing to Vanus!

PR Title Format: 

feat/fix/refactor/docs/...: what's changed

Note: see https://github.com/linkall-labs/vanus/blob/main/CONTRIBUTING.md to know details about title format
-->

### What problem does this PR solve?
<!--

Please create an issue first to describe the problem

There MUST be one line starting with "Issue Number:  ".

-->

Issue Number: None

### Problem Summary

### What is changed and how does it work?

### Check List

<!-- At least one of them must be included. -->

#### Tests

- [ ] Unit test
- [ ] Integration test
- [ ] Manual test (add detailed scripts or steps below)
- [ ] No code



Patch:
diff --git a/Makefile b/Makefile
index 2ed5ca2..ad9a548 100644
--- a/Makefile
+++ b/Makefile
@@ -61,6 +61,9 @@ docker-push-toolbox:
 build-cmd:
 	$(GO_BUILD) -ldflags "${LD_FLAGS}" -o bin/vsctl ./vsctl/
 
+build-repair-tool:
+	$(GO_BUILD) -ldflags "${LD_FLAGS}" -o bin/vsrepair ./vsrepair/
+
 docker-push-root:
 	docker buildx build --platform ${DOCKER_PLATFORM} -t ${DOCKER_REPO}/root-controller:${IMAGE_TAG} -f build/images/root-controller/Dockerfile . --push
 docker-build-root:
diff --git a/internal/store/meta/async.go b/internal/store/meta/async.go
index 7d0a087..e2b8f20 100644
--- a/internal/store/meta/async.go
+++ b/internal/store/meta/async.go
@@ -76,7 +76,7 @@ func (s *AsyncStore) Load(key []byte) (interface{}, bool) {
 	s.mu.RLock()
 	defer s.mu.RUnlock()
 	if v, ok := s.pending.GetValue(key); ok {
-		if v == deletedMark {
+		if v == DeletedMark {
 			return nil, false
 		}
 		return v, true
@@ -93,7 +93,7 @@ func (s *AsyncStore) BatchStore(_ context.Context, kvs Ranger) {
 }
 
 func (s *AsyncStore) Delete(key []byte) {
-	_ = s.set(KVRange(key, deletedMark))
+	_ = s.set(KVRange(key, DeletedMark))
 }
 
 func (s *AsyncStore) BatchDelete(keys [][]byte) {
diff --git a/internal/store/meta/codec.go b/internal/store/meta/codec.go
index 40ca14d..b74e35d 100644
--- a/internal/store/meta/codec.go
+++ b/internal/store/meta/codec.go
@@ -25,7 +25,7 @@ import (
 )
 
 var (
-	deletedMark  deletedMarkType
+	DeletedMark  deletedMarkType
 	defaultCodec codec
 )
 
@@ -65,8 +65,10 @@ const (
 type codec struct{}
 
 // Make sure codec implements Marshaler and Unmarshaler.
-var _ Marshaler = (*codec)(nil)
-var _ Unmarshaler = (*codec)(nil)
+var (
+	_ Marshaler   = (*codec)(nil)
+	_ Unmarshaler = (*codec)(nil)
+)
 
 func (codec) Marshal(data Ranger) ([]byte, error) {
 	buf := make([]byte, 0)
@@ -118,7 +120,7 @@ func encodeKey(key, last []byte) (int, []byte) {
 }
 
 func appendValue(buf []byte, value interface{}) ([]byte, error) {
-	if value == deletedMark {
+	if value == DeletedMark {
 		buf = protowire.AppendVarint(buf, uint64(Deleted))
 		return buf, nil
 	}
@@ -232,7 +234,7 @@ func consumeValue(buf []byte) (interface{}, int) {
 	kind := Kind(k)
 	switch kind {
 	case Deleted:
-		return deletedMark, n
+		return DeletedMark, n
 	case True:
 		return true, n
 	case False:
diff --git a/internal/store/meta/range.go b/internal/store/meta/range.go
index e627adc..2b435bf 100644
--- a/internal/store/meta/range.go
+++ b/internal/store/meta/range.go
@@ -73,7 +73,7 @@ var _ Ranger = (*deleteRange)(nil)
 
 func (r *deleteRange) Range(cb RangeCallback) error {
 	for _, key := range r.keys {
-		if err := cb(key, deletedMark); err != nil {
+		if err := cb(key, DeletedMark); err != nil {
 			return err
 		}
 	}
diff --git a/internal/store/meta/review.go b/internal/store/meta/review.go
new file mode 100644
index 0000000..aad63fe
--- /dev/null
+++ b/internal/store/meta/review.go
@@ -0,0 +1,103 @@
+// Copyright 2023 Linkall Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package meta
+
+import (
+	// standard libraries.
+	"bytes"
+	"context"
+	"errors"
+	"os"
+	"path/filepath"
+	"strconv"
+
+	// this project.
+	walog "github.com/vanus-labs/vanus/internal/store/wal"
+)
+
+var errFound = errors.New("found")
+
+type ReviewWatcher = func(value interface{}, version int64)
+
+func ReviewSyncStore(ctx context.Context, dir string, key []byte, watcher ReviewWatcher, opts ...walog.Option) error {
+	snapshot, err := reviewLatestSnapshot(ctx, dir, defaultCodec, key, watcher)
+	if err != nil {
+		return err
+	}
+
+	opts = append([]walog.Option{
+		walog.FromPosition(snapshot),
+		walog.WithRecoveryCallback(func(data []byte, r walog.Range) error {
+			err2 := defaultCodec.Unmarshal(data, func(k []byte, v interface{}) error {
+				if bytes.Equal(k, key) {
+					watcher(v, r.EO)
+				}
+				return nil
+			})
+			if err2 != nil {
+				return err2
+			}
+			return nil
+		}),
+	}, opts...)
+	wal, err := walog.Open(ctx, dir, opts...)
+	if err != nil {
+		return err
+	}
+
+	wal.Close()
+
+	return nil
+}
+
+func reviewLatestSnapshot(
+	_ context.Context, dir string, unmarshaler Unmarshaler, key []byte, watcher ReviewWatcher,
+) (int64, error) {
+	files, err := os.ReadDir(dir)
+	if err != nil {
+		return 0, err
+	}
+	latest, _ := filterLatestSnapshot(files)
+
+	if latest == nil {
+		return 0, nil
+	}
+
+	filename := latest.Name()
+	snapshot, err := strconv.ParseInt(filename[:len(filename)-len(snapshotExt)], 10, 64)
+	if err != nil {
+		return 0, err
+	}
+
+	path := filepath.Join(dir, filename)
+	data, err := os.ReadFile(path)
+	if err != nil {
+		return 0, err
+	}
+
+	err = unmarshaler.Unmarshal(data, func(k []byte, v interface{}) error {
+		if bytes.Equal(k, key) {
+			watcher(v, -1)
+			// TODO(james.yin): don't skip remaind data?
+			return errFound
+		}
+		return nil
+	})
+	if err != nil && err != errFound { //nolint:errorlint // compare to errFound is ok.
+		return 0, err
+	}
+
+	return snapshot, nil
+}
diff --git a/internal/store/meta/store.go b/internal/store/meta/store.go
index 3f6a264..00c7237 100644
--- a/internal/store/meta/store.go
+++ b/internal/store/meta/store.go
@@ -53,7 +53,7 @@ func (s *store) load(key []byte) (interface{}, bool) {
 // }
 
 func set(m *skiplist.SkipList, key []byte, value interface{}) {
-	if value == deletedMark {
+	if value == DeletedMark {
 		m.Remove(key)
 	} else {
 		m.Set(key, value)
diff --git a/internal/store/meta/sync.go b/internal/store/meta/sync.go
index 84a948c..8344c93 100644
--- a/internal/store/meta/sync.go
+++ b/internal/store/meta/sync.go
@@ -98,7 +98,7 @@ func (s *SyncStore) BatchStore(ctx context.Context, kvs Ranger, cb StoreCallback
 }
 
 func (s *SyncStore) Delete(ctx context.Context, key []byte, cb StoreCallback) {
-	s.set(ctx, KVRange(key, deletedMark), cb)
+	s.set(ctx, KVRange(key, DeletedMark), cb)
 }
 
 func (s *SyncStore) BatchDelete(ctx context.Context, keys [][]byte, cb StoreCallback) {
@@ -126,7 +126,7 @@ func (s *SyncStore) set(ctx context.Context, kvs Ranger, cb StoreCallback) {
 		// Update state.
 		s.mu.Lock()
 		_ = kvs.Range(func(key []byte, value interface{}) error {
-			if value == deletedMark {
+			if value == DeletedMark {
 				s.committed.Remove(key)
 			} else {
 				s.committed.Set(key, value)
diff --git a/internal/store/vsb/block_header.go b/internal/store/vsb/block_header.go
index 5fadcc9..f629977 100644
--- a/internal/store/vsb/block_header.go
+++ b/internal/store/vsb/block_header.go
@@ -19,6 +19,7 @@ import (
 	"context"
 	"encoding/binary"
 	"hash/crc32"
+	"os"
 
 	// this project.
 	"github.com/vanus-labs/vanus/internal/store/block/raw"
@@ -46,6 +47,54 @@ var (
 	emptyHeader = make([]byte, headerBlockSize)
 )
 
+type Header struct {
+	Magic       uint32
+	Crc         uint32
+	Flags       uint32
+	BreakFlags  uint32
+	DataOffset  uint32
+	State       uint8
+	_pad        uint8 //nolint:unused // padding
+	IndexSize   uint16
+	Capacity    uint64
+	EntryLength uint64
+	EntryNum    uint32
+	IndexOffset uint16
+}
+
+func LoadHeader(f *os.File) (hdr Header, err error) {
+	var buf [headerSize]byte
+	if _, err = f.ReadAt(buf[:], 0); err != nil {
+		return
+	}
+
+	magic := binary.LittleEndian.Uint32(buf[magicOffset:])
+	if magic != FormatMagic {
+		return hdr, raw.ErrInvalidFormat
+	}
+
+	breakFlags := binary.LittleEndian.Uint32(buf[breakFlagsOffset:])
+	if breakFlags != 0 {
+		return hdr, errIncomplete
+	}
+
+	hdr.DataOffset = binary.LittleEndian.Uint32(buf[dataOffsetOffset:])
+	hdr.State = buf[stateOffset]
+	hdr.IndexSize = binary.LittleEndian.Uint16(buf[indexSizeOffset:])
+	hdr.Capacity = binary.LittleEndian.Uint64(buf[capacityOffset:])
+	hdr.EntryLength = binary.LittleEndian.Uint64(buf[entryLengthOffset:])
+	hdr.EntryNum = binary.LittleEndian.Uint32(buf[entryNumOffset:])
+
+	origin := binary.LittleEndian.Uint32(buf[crcOffset:])
+	crc := crc32.Checksum(buf[flagsOffset:], crc32q)
+	crc = crc32.Update(crc, crc32q, emptyHeader[headerSize:])
+	if origin != crc {
+		return hdr, errCorrupted
+	}
+
+	return hdr, nil
+}
+
 func (b *vsBlock) persistHeader(_ context.Context, m meta) error {
 	var buf [headerSize]byte
 	binary.LittleEndian.PutUint32(buf[magicOffset:], FormatMagic)               // magic
@@ -79,34 +128,17 @@ func (b *vsBlock) persistHeader(_ context.Context, m meta) error {
 }
 
 func (b *vsBlock) loadHeader(_ context.Context) error {
-	var buf [headerSize]byte
-	if _, err := b.f.ReadAt(buf[:], 0); err != nil {
+	hdr, err := LoadHeader(b.f)
+	if err != nil {
 		return err
 	}
 
-	magic := binary.LittleEndian.Uint32(buf[magicOffset:])
-	if magic != FormatMagic {
-		return raw.ErrInvalidFormat
-	}
-
-	breakFlags := binary.LittleEndian.Uint32(buf[breakFlagsOffset:])
-	if breakFlags != 0 {
-		return errIncomplete
-	}
-
-	b.dataOffset = int64(binary.LittleEndian.Uint32(buf[dataOffsetOffset:]))      // data offset
-	b.fm.archived = buf[stateOffset] != 0                                         // state
-	b.indexSize = binary.LittleEndian.Uint16(buf[indexSizeOffset:])               // index size
-	b.capacity = int64(binary.LittleEndian.Uint64(buf[capacityOffset:]))          // capacity
-	b.fm.entryLength = int64(binary.LittleEndian.Uint64(buf[entryLengthOffset:])) // entry length
-	b.fm.entryNum = int64(binary.LittleEndian.Uint32(buf[entryNumOffset:]))       // entry number
-
-	origin := binary.LittleEndian.Uint32(buf[crcOffset:])
-	crc := crc32.Checksum(buf[flagsOffset:], crc32q)
-	crc = crc32.Update(crc, crc32q, emptyHeader[headerSize:])
-	if origin != crc {
-		return errCorrupted
-	}
+	b.dataOffset = int64(hdr.DataOffset)
+	b.fm.archived = hdr.State != 0
+	b.indexSize = hdr.IndexSize
+	b.capacity = int64(hdr.Capacity)
+	b.fm.entryLength = int64(hdr.EntryLength)
+	b.fm.entryNum = int64(hdr.EntryNum)
 
 	return nil
 }
diff --git a/internal/store/vsb/engine_open.go b/internal/store/vsb/engine_open.go
index 3ec18be..3ebe84d 100644
--- a/internal/store/vsb/engine_open.go
+++ b/internal/store/vsb/engine_open.go
@@ -114,5 +114,9 @@ func (e *engine) Open(ctx context.Context, id vanus.ID) (block.Raw, error) {
 }
 
 func (e *engine) resolvePath(id vanus.ID) string {
-	return filepath.Join(e.dir, fmt.Sprintf("%s%s", id.String(), vsbExt))
+	return BlockPath(e.dir, id)
+}
+
+func BlockPath(dir string, id vanus.ID) string {
+	return filepath.Join(dir, fmt.Sprintf("%s%s", id.String(), vsbExt))
 }
diff --git a/internal/store/wal/config.go b/internal/store/wal/config.go
index 1539610..1ac8cb0 100644
--- a/internal/store/wal/config.go
+++ b/internal/store/wal/config.go
@@ -38,6 +38,7 @@ type config struct {
 	fileSize       int64
 	flushDelayTime time.Duration // default: 3 * time.Millisecond
 	engine         ioengine.Interface
+	readOnly       bool
 }
 
 func (cfg *config) segmentedFileOptions() []segmentedfile.Option {
@@ -115,3 +116,9 @@ func WithIOEngine(engine ioengine.Interface) Option {
 		cfg.engine = engine
 	}
 }
+
+func WithReadOnly() Option {
+	return func(cfg *config) {
+		cfg.readOnly = true
+	}
+}
diff --git a/internal/store/wal/wal.go b/internal/store/wal/wal.go
index 720f11b..f0d3c1c 100644
--- a/internal/store/wal/wal.go
+++ b/internal/store/wal/wal.go
@@ -108,8 +108,12 @@ func open(ctx context.Context, dir string, cfg config) (*WAL, error) {
 		doneC: make(chan struct{}),
 	}
 
-	w.appendQ.Init(false)
-	go w.runAppend()
+	if !cfg.readOnly {
+		w.appendQ.Init(false)
+		go w.runAppend()
+	} else {
+		close(w.doneC)
+	}
 
 	return w, nil
 }
diff --git a/vsrepair/block/vsb.go b/vsrepair/block/vsb.go
new file mode 100644
index 0000000..469e671
--- /dev/null
+++ b/vsrepair/block/vsb.go
@@ -0,0 +1,39 @@
+// Copyright 2023 Linkall Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package block
+
+import (
+	// standard libraries.
+	"os"
+	"path/filepath"
+
+	// first-party libraries.
+	"github.com/vanus-labs/vanus/internal/primitive/vanus"
+	"github.com/vanus-labs/vanus/internal/store/vsb"
+)
+
+func VSBDetail(volumeDir string, id uint64) (vsb.Header, error) {
+	blockID := vanus.NewIDFromUint64(id)
+	blockPath := vsb.BlockPath(filepath.Join(volumeDir, "block"), blockID)
+
+	f, err := os.OpenFile(blockPath, os.O_RDONLY, 0)
+	if err != nil {
+		return vsb.Header{}, err
+	}
+
+	defer f.Close()
+
+	return vsb.LoadHeader(f)
+}
diff --git a/vsrepair/command/apply/flags.go b/vsrepair/command/apply/flags.go
new file mode 100644
index 0000000..2fc0cae
--- /dev/null
+++ b/vsrepair/command/apply/flags.go
@@ -0,0 +1,17 @@
+// Copyright 2022 Linkall Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package apply
+
+var volumePath string
diff --git a/vsrepair/command/apply/get.go b/vsrepair/command/apply/get.go
new file mode 100644
index 0000000..9b25345
--- /dev/null
+++ b/vsrepair/command/apply/get.go
@@ -0,0 +1,73 @@
+// Copyright 2022 Linkall Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package apply
+
+import (
+	// standard libraries.
+	"encoding/json"
+	"fmt"
+	"strconv"
+
+	// third-party libraries.
+	"github.com/spf13/cobra"
+
+	// this project.
+	"github.com/vanus-labs/vanus/vsrepair/meta"
+)
+
+func GetCommand() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "apply BlockID",
+		Short: "Get Apply of a specific Block.",
+		Run:   get,
+	}
+	cmd.Flags().StringVar(&volumePath, "volume", "", "volume path")
+	return cmd
+}
+
+type getResult struct {
+	BlockID uint64 `json:"BlockID"`
+	Apply   uint64 `json:"Apply"`
+}
+
+func get(cmd *cobra.Command, args []string) {
+	db, err := meta.Open(volumePath, meta.ReadOnly(), meta.SkipMetaStore())
+	if err != nil {
+		panic(err)
+	}
+	defer db.Close()
+
+	for _, arg := range args {
+		id, err := strconv.ParseUint(arg, 0, 0)
+		if err != nil {
+			panic(err)
+		}
+
+		app, err := db.GetApply(id)
+		if err != nil {
+			panic(err)
+		}
+
+		jsonResult, err := json.MarshalIndent(getResult{
+			BlockID: id,
+			Apply:   app,
+		}, "", "  ")
+		if err != nil {
+			panic(err)
+		}
+
+		fmt.Println(string(jsonResult))
+	}
+}
diff --git a/vsrepair/command/apply/modify.go b/vsrepair/command/apply/modify.go
new file mode 100644
index 0000000..54a183e
--- /dev/null
+++ b/vsrepair/command/apply/modify.go
@@ -0,0 +1,62 @@
+// Copyright 2022 Linkall Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package apply
+
+import (
+	// standard libraries.
+	"strconv"
+
+	// third-party libraries.
+	"github.com/spf13/cobra"
+
+	// this project.
+	"github.com/vanus-labs/vanus/vsrepair/meta"
+)
+
+func ModifyCommand() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "apply [--volume <volume path>] nodeID offset",
+		Short: "modify apply offset",
+		Run:   modify,
+	}
+	cmd.Flags().StringVar(&volumePath, "volume", "", "volume path")
+	return cmd
+}
+
+func modify(cmd *cobra.Command, args []string) {
+	if len(args) != 2 {
+		panic("invalid args")
+	}
+
+	db, err := meta.Open(volumePath)
+	if err != nil {
+		panic(err)
+	}
+	defer db.Close()
+
+	node, err := strconv.ParseUint(args[0], 0, 0)
+	if err != nil {
+		panic(err)
+	}
+
+	app, err := strconv.ParseUint(args[1], 10, 0)
+	if err != nil {
+		panic(err)
+	}
+
+	if err := db.PutApply(node, app); err != nil {
+		panic(err)
+	}
+}
diff --git a/vsrepair/command/block/describe.go b/vsrepair/command/block/describe.go
new file mode 100644
index 0000000..92f8e5c
--- /dev/null
+++ b/vsrepair/command/block/describe.go
@@ -0,0 +1,146 @@
+// Copyright 2023 Linkall Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package block
+
+import (
+	// standard libraries.
+	"context"
+	"encoding/json"
+	"fmt"
+	"strconv"
+	"time"
+
+	// third-party libraries.
+	"github.com/spf13/cobra"
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/credentials/insecure"
+
+	// first-party libraries.
+	metapb "github.com/vanus-labs/vanus/proto/pkg/meta"
+	segmentpb "github.com/vanus-labs/vanus/proto/pkg/segment"
+
+	// this project.
+	"github.com/vanus-labs/vanus/internal/store/vsb"
+	"github.com/vanus-labs/vanus/vsrepair/block"
+	"github.com/vanus-labs/vanus/vsrepair/meta"
+)
+
+const (
+	defaultTimeout = 10 * time.Second
+)
+
+func DescribeCommand() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "block ID",
+		Short: "Show details of a specific Block.",
+		Args:  cobra.ExactArgs(1),
+		RunE:  describe,
+	}
+	cmd.Flags().StringVar(&volumePath, "volume", "", "volume path")
+	cmd.Flags().StringVar(&storeEndpoint, "store", "", "store endpoint")
+	cmd.Flags().BoolVar(&noVSB, "no-vsb", false, "not describe vsb detail")
+	cmd.Flags().BoolVar(&noRaft, "no-raft", false, "not describe raft detail")
+	cmd.Flags().BoolVar(&withStatus, "with-status", false, "describe block status")
+	if err := cobra.MarkFlagDirname(cmd.Flags(), "volume"); err != nil {
+		panic(err)
+	}
+	return cmd
+}
+
+type blockDetail struct {
+	ID     uint64                    `json:"ID"`
+	VSB    *vsb.Header               `json:"VSB,omitempty"`
+	Raft   *meta.RaftDetail          `json:"Raft,omitempty"`
+	Status *metapb.SegmentHealthInfo `json:"Status,omitempty"`
+}
+
+func describe(cmd *cobra.Command, args []string) error {
+	id, err := strconv.ParseUint(args[0], 0, 64)
+	if err != nil {
+		return err
+	}
+
+	if (!noVSB || !noRaft) && volumePath == "" {
+		return fmt.Errorf("volume path is empty")
+	}
+	if withStatus && storeEndpoint == "" {
+		return fmt.Errorf("store endpoint is empty")
+	}
+
+	d := blockDetail{
+		ID: id,
+	}
+
+	if !noVSB {
+		vsbDetail, err2 := block.VSBDetail(volumePath, id)
+		if err2 != nil {
+			return err2
+		}
+		d.VSB = &vsbDetail
+	}
+
+	if !noRaft {
+		db, err2 := meta.Open(volumePath, meta.ReadOnly())
+		if err2 != nil {
+			return err2
+		}
+		defer db.Close()
+
+		raftDetail, err2 := db.GetRaftDetail(id)
+		if err2 == nil {
+			d.Raft = &raftDetail
+		} else if err2 != meta.ErrNotFound { //nolint:errorlint // compare to meta.ErrNotFound is ok.
+			return err2
+		}
+	}
+
+	if withStatus {
+		req := &segmentpb.DescribeBlockRequest{
+			Id: id,
+		}
+
+		ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
+		defer cancel()
+
+		opts := []grpc.DialOption{
+			grpc.WithBlock(),
+			grpc.WithTransportCredentials(insecure.NewCredentials()),
+		}
+		conn, err2 := grpc.DialContext(ctx, storeEndpoint, opts...)
+		if err2 != nil {
+			return err2
+		}
+		defer func() {
+			_ = conn.Close()
+		}()
+
+		cli := segmentpb.NewSegmentServerClient(conn)
+		resp, err2 := cli.DescribeBlock(ctx, req)
+		if err2 != nil {
+			return err2
+		}
+
+		d.Status = resp.GetInfo()
+	}
+
+	jsonDetail, err := json.MarshalIndent(d, "", "  ")
+	if err != nil {
+		return err
+	}
+
+	fmt.Println(string(jsonDetail))
+
+	return nil
+}
diff --git a/vsrepair/command/block/flags.go b/vsrepair/command/block/flags.go
new file mode 100644
index 0000000..a14f0ca
--- /dev/null
+++ b/vsrepair/command/block/flags.go
@@ -0,0 +1,23 @@
+// Copyright 2022 Linkall Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package block
+
+var (
+	volumePath    string
+	storeEndpoint string
+	noVSB         bool
+	noRaft        bool
+	withStatus    bool
+)
diff --git a/vsrepair/command/compact/flags.go b/vsrepair/command/compact/flags.go
new file mode 100644
index 0000000..fc3ae0e
--- /dev/null
+++ b/vsrepair/command/compact/flags.go
@@ -0,0 +1,21 @@
+// Copyright 2022 Linkall Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package compact
+
+var (
+	volumePath string
+	compact    uint64
+	term       uint64
+)
diff --git a/vsrepair/command/compact/get.go b/vsrepair/command/compact/get.go
new file mode 100644
index 0000000..79d345f
--- /dev/null
+++ b/vsrepair/command/compact/get.go
@@ -0,0 +1,73 @@
+// Copyright 2023 Linkall Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package compact
+
+import (
+	// standard libraries.
+	"encoding/json"
+	"fmt"
+	"strconv"
+
+	// third-party libraries.
+	"github.com/spf13/cobra"
+
+	// this project.
+	"github.com/vanus-labs/vanus/vsrepair/meta"
+)
+
+func GetCommand() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "compact BlockID",
+		Short: "Get Compact of a specific Block.",
+		Run:   get,
+	}
+	cmd.Flags().StringVar(&volumePath, "volume", "", "volume path")
+	return cmd
+}
+
+type getResult struct {
+	BlockID uint64           `json:"BlockID"`
+	Compact meta.CompactInfo `json:"Compact"`
+}
+
+func get(cmd *cobra.Command, args []string) {
+	db, err := meta.Open(volumePath, meta.ReadOnly())
+	if err != nil {
+		panic(err)
+	}
+	defer db.Close()
+
+	for _, arg := range args {
+		id, err := strconv.ParseUint(arg, 0, 0)
+		if err != nil {
+			panic(err)
+		}
+
+		compact, err := db.GetCompact(id)
+		if err != nil {
+			panic(err)
+		}
+
+		jsonResult, err := json.MarshalIndent(getResult{
+			BlockID: id,
+			Compact: compact,
+		}, "", "  ")
+		if err != nil {
+			panic(err)
+		}
+
+		fmt.Println(string(jsonResult))
+	}
+}
diff --git a/vsrepair/command/compact/modify.go b/vsrepair/command/compact/modify.go
new file mode 100644
index 0000000..dcf6cfa
--- /dev/null
+++ b/vsrepair/command/compact/modify.go
@@ -0,0 +1,71 @@
+// Copyright 2022 Linkall Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package compact
+
+import (
+	// standard libraries.
+	"strconv"
+
+	// third-party libraries.
+	"github.com/spf13/cobra"
+
+	// this project.
+	"github.com/vanus-labs/vanus/vsrepair/meta"
+)
+
+func ModifyCommand() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "compact nodeID compact",
+		Short: "modify compact",
+		Run:   modify,
+	}
+	cmd.Flags().StringVar(&volumePath, "volume", "", "volume path")
+	cmd.Flags().Uint64Var(&compact, "compact", 0, "compact")
+	cmd.Flags().Uint64Var(&term, "term", 0, "term")
+	return cmd
+}
+
+func modify(cmd *cobra.Command, args []string) {
+	if len(args) != 1 {
+		panic("invalid args")
+	}
+
+	db, err := meta.Open(volumePath)
+	if err != nil {
+		panic(err)
+	}
+	defer db.Close()
+
+	node, err := strconv.ParseUint(args[0], 0, 0)
+	if err != nil {
+		panic(err)
+	}
+
+	info, err := db.GetCompact(node)
+	if err != nil {
+		panic(err)
+	}
+
+	if compact != 0 {
+		info.Index = compact
+	}
+	if term != 0 {
+		info.Term = term
+	}
+
+	if err := db.PutCompact(node, info); err != nil {
+		panic(err)
+	}
+}
diff --git a/vsrepair/command/compact/review.go b/vsrepair/command/compact/review.go
new file mode 100644
index 0000000..d7e9304
--- /dev/null
+++ b/vsrepair/command/compact/review.go
@@ -0,0 +1,66 @@
+// Copyright 2023 Linkall Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package compact
+
+import (
+	// standard libraries.
+	"encoding/json"
+	"fmt"
+	"strconv"
+
+	// third-party libraries.
+	"github.com/spf13/cobra"
+
+	// this project.
+	"github.com/vanus-labs/vanus/vsrepair/meta"
+)
+
+func ReviewCommand() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "compact ID",
+		Short: "Review compact history of a specific Block",
+		Run:   review,
+	}
+	cmd.Flags().StringVar(&volumePath, "volume", "", "volume path")
+	return cmd
+}
+
+type reviewResult struct {
+	Version int64             `json:"version"`
+	Compact *meta.CompactInfo `json:"Compact"`
+}
+
+func review(cmd *cobra.Command, args []string) {
+	for _, arg := range args {
+		id, err := strconv.ParseUint(arg, 0, 64)
+		if err != nil {
+			panic(err)
+		}
+
+		err = meta.ReviewCompact(volumePath, id, func(compact *meta.CompactInfo, version int64) {
+			jsonResult, err2 := json.MarshalIndent(reviewResult{
+				Version: version,
+				Compact: compact,
+			}, "", "  ")
+			if err2 != nil {
+				panic(err2)
+			}
+			fmt.Println(string(jsonResult))
+		})
+		if err != nil {
+			panic(err)
+		}
+	}
+}
diff --git a/vsrepair/command/create.go b/vsrepair/command/create.go
new file mode 100644
index 0000000..9b3280c
--- /dev/null
+++ b/vsrepair/command/create.go
@@ -0,0 +1,32 @@
+// Copyright 2022 Linkall Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package command
+
+import (
+	// third-party libraries.
+	"github.com/spf13/cobra"
+
+	// this project.
+	"github.com/vanus-labs/vanus/vsrepair/command/event"
+)
+
+func CreateCommand() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "create SUB-COMMAND",
+		Short: "create resources",
+	}
+	cmd.AddCommand(event.CreateCommand())
+	return cmd
+}
diff --git a/vsrepair/command/cs/flags.go b/vsrepair/command/cs/flags.go
new file mode 100644
index 0000000..0721ec2
--- /dev/null
+++ b/vsrepair/command/cs/flags.go
@@ -0,0 +1,17 @@
+// Copyright 2022 Linkall Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cs
+
+var volumePath string
diff --git a/vsrepair/command/cs/get.go b/vsrepair/command/cs/get.go
new file mode 100644
index 0000000..11722f6
--- /dev/null
+++ b/vsrepair/command/cs/get.go
@@ -0,0 +1,74 @@
+// Copyright 2022 Linkall Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cs
+
+import (
+	// standard libraries.
+	"encoding/json"
+	"fmt"
+	"strconv"
+
+	// third-party libraries.
+	"github.com/spf13/cobra"
+
+	// this project.
+	"github.com/vanus-labs/vanus/raft/raftpb"
+	"github.com/vanus-labs/vanus/vsrepair/meta"
+)
+
+func GetCommand() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "cs BlockID",
+		Short: "Get ConfState of a specific Block.",
+		Run:   get,
+	}
+	cmd.Flags().StringVar(&volumePath, "volume", "", "volume path")
+	return cmd
+}
+
+type getResult struct {
+	BlockID   uint64           `json:"BlockID"`
+	ConfState raftpb.ConfState `json:"ConfState"`
+}
+
+func get(cmd *cobra.Command, args []string) {
+	db, err := meta.Open(volumePath, meta.ReadOnly())
+	if err != nil {
+		panic(err)
+	}
+	defer db.Close()
+
+	for _, arg := range args {
+		id, err := strconv.ParseUint(arg, 0, 0)
+		if err != nil {
+			panic(err)
+		}
+
+		cs, err := db.GetConfState(id)
+		if err != nil {
+			panic(err)
+		}
+
+		jsonResult, err := json.MarshalIndent(getResult{
+			BlockID:   id,
+			ConfState: cs,
+		}, "", "  ")
+		if err != nil {
+			panic(err)
+		}
+
+		fmt.Println(string(jsonResult))
+	}
+}
diff --git a/vsrepair/command/describe.go b/vsrepair/command/describe.go
new file mode 100644
index 0000000..d26d5dc
--- /dev/null
+++ b/vsrepair/command/describe.go
@@ -0,0 +1,32 @@
+// Copyright 2023 Linkall Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package command
+
+import (
+	// third-party libraries.
+	"github.com/spf13/cobra"
+
+	// this project.
+	"github.com/vanus-labs/vanus/vsrepair/command/block"
+)
+
+func DescribeCommand() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "describe TYPE ID",
+		Short: "Show details of a specific resource.",
+	}
+	cmd.AddCommand(block.DescribeCommand())
+	return cmd
+}
diff --git a/vsrepair/command/event/create.go b/vsrepair/command/event/create.go
new file mode 100644
index 0000000..f1441af
--- /dev/null
+++ b/vsrepair/command/event/create.go
@@ -0,0 +1,107 @@
+// Copyright 2022 Linkall Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package event
+
+import (
+	// standard libraries.
+	"context"
+	"encoding/json"
+	"fmt"
+	"strconv"
+	"time"
+
+	// third-party libraries.
+	"github.com/google/uuid"
+	"github.com/spf13/cobra"
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/credentials/insecure"
+
+	// first-party libraries.
+	cepb "github.com/vanus-labs/vanus/proto/pkg/cloudevents"
+	segmentpb "github.com/vanus-labs/vanus/proto/pkg/segment"
+)
+
+func CreateCommand() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "event",
+		Short: "create event",
+		Run:   create,
+	}
+	cmd.Flags().StringVar(&storeEndpoint, "store", "", "store endpoint")
+	cmd.Flags().StringVar(&blockID, "block", "", "block id")
+	cmd.Flags().StringVar(&eventID, "id", "", "event id")
+	cmd.Flags().StringVar(&eventSource, "source", "https://github.com/vanus-labs/vanus/vsrepair", "event source")
+	cmd.Flags().StringVar(&eventType, "type", "ai.vanus.core.vsrepair.event.create", "event type")
+	cmd.Flags().StringVar(&eventData, "data", "", "event data")
+	return cmd
+}
+
+func create(cmd *cobra.Command, args []string) {
+	id, err := strconv.ParseUint(blockID, 0, 0)
+	if err != nil {
+		panic(err)
+	}
+
+	event := &cepb.CloudEvent{
+		SpecVersion: "1.0",
+		Source:      eventSource,
+		Type:        eventType,
+	}
+	if eventID != "" {
+		event.Id = eventID
+	} else {
+		event.Id = uuid.NewString()
+	}
+	if eventData != "" {
+		event.Data = &cepb.CloudEvent_TextData{
+			TextData: eventData,
+		}
+	}
+
+	req := &segmentpb.AppendToBlockRequest{
+		BlockId: id,
+		Events: &cepb.CloudEventBatch{
+			Events: []*cepb.CloudEvent{event},
+		},
+	}
+
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	defer cancel()
+
+	opts := []grpc.DialOption{
+		grpc.WithBlock(),
+		grpc.WithTransportCredentials(insecure.NewCredentials()),
+	}
+	conn, err := grpc.DialContext(ctx, storeEndpoint, opts...)
+	if err != nil {
+		panic(err)
+	}
+	defer func() {
+		_ = conn.Close()
+	}()
+
+	cli := segmentpb.NewSegmentServerClient(conn)
+	resp, err := cli.AppendToBlock(ctx, req)
+	if err != nil {
+		panic(err)
+	}
+
+	jsonOffsets, err := json.MarshalIndent(resp.GetOffsets(), "", "  ")
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Println(string(jsonOffsets))
+}
diff --git a/vsrepair/command/event/flags.go b/vsrepair/command/event/flags.go
new file mode 100644
index 0000000..da3bf87
--- /dev/null
+++ b/vsrepair/command/event/flags.go
@@ -0,0 +1,26 @@
+// Copyright 2023 Linkall Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package event
+
+var (
+	storeEndpoint string
+	blockID       string
+	offset        uint64
+	number        uint64
+	eventID       string
+	eventSource   string
+	eventType     string
+	eventData     string
+)
diff --git a/vsrepair/command/event/get.go b/vsrepair/command/event/get.go
new file mode 100644
index 0000000..c0d027d
--- /dev/null
+++ b/vsrepair/command/event/get.go
@@ -0,0 +1,92 @@
+// Copyright 2022 Linkall Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package event
+
+import (
+	// standard libraries.
+	"context"
+	"encoding/json"
+	"fmt"
+	"strconv"
+	"time"
+
+	// third-party libraries.
+	"github.com/spf13/cobra"
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/credentials/insecure"
+
+	// first-party libraries.
+	segmentpb "github.com/vanus-labs/vanus/proto/pkg/segment"
+)
+
+func GetCommand() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "event",
+		Short: "get events",
+		Run:   get,
+	}
+	cmd.Flags().StringVar(&storeEndpoint, "store", "", "store endpoint")
+	cmd.Flags().StringVar(&blockID, "block", "", "block id")
+	cmd.Flags().Uint64Var(&offset, "offset", 0, "the start offset")
+	cmd.Flags().Uint64Var(&number, "num", 1, "the number of events")
+	return cmd
+}
+
+func get(cmd *cobra.Command, args []string) {
+	id, err := strconv.ParseUint(blockID, 0, 0)
+	if err != nil {
+		panic(err)
+	}
+
+	if number == 0 {
+		number = 1
+	}
+
+	req := &segmentpb.ReadFromBlockRequest{
+		BlockId: id,
+		Offset:  int64(offset),
+		Number:  int64(number),
+	}
+
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	defer cancel()
+
+	opts := []grpc.DialOption{
+		grpc.WithBlock(),
+		grpc.WithTransportCredentials(insecure.NewCredentials()),
+	}
+	conn, err := grpc.DialContext(ctx, storeEndpoint, opts...)
+	if err != nil {
+		panic(err)
+	}
+	defer func() {
+		_ = conn.Close()
+	}()
+
+	cli := segmentpb.NewSegmentServerClient(conn)
+	resp, err := cli.ReadFromBlock(ctx, req)
+	if err != nil {
+		panic(err)
+	}
+
+	for _, e := range resp.GetEvents().GetEvents() {
+		jsonEvent, err := json.MarshalIndent(e, "", "  ")
+		if err != nil {
+			panic(err)
+		}
+
+		fmt.Println(string(jsonEvent))
+	}
+}
diff --git a/vsrepair/command/get.go b/vsrepair/command/get.go
new file mode 100644
index 0000000..0ee5fb6
--- /dev/null
+++ b/vsrepair/command/get.go
@@ -0,0 +1,40 @@
+// Copyright 2022 Linkall Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package command
+
+import (
+	// third-party libraries.
+	"github.com/spf13/cobra"
+
+	// this project.
+	"github.com/vanus-labs/vanus/vsrepair/command/apply"
+	"github.com/vanus-labs/vanus/vsrepair/command/compact"
+	"github.com/vanus-labs/vanus/vsrepair/command/cs"
+	"github.com/vanus-labs/vanus/vsrepair/command/event"
+	"github.com/vanus-labs/vanus/vsrepair/command/hs"
+)
+
+func GetCommand() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "get SUB-COMMAND",
+		Short: "get metadata",
+	}
+	cmd.AddCommand(cs.GetCommand())
+	cmd.AddCommand(hs.GetCommand())
+	cmd.AddCommand(apply.GetCommand())
+	cmd.AddCommand(compact.GetCommand())
+	cmd.AddCommand(event.GetCommand())
+	return cmd
+}
diff --git a/vsrepair/command/hs/flags.go b/vsrepair/command/hs/flags.go
new file mode 100644
index 0000000..1462e6a
--- /dev/null
+++ b/vsrepair/command/hs/flags.go
@@ -0,0 +1,20 @@
+// Copyright 2022 Linkall Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package hs
+
+var (
+	volumePath string
+	commit     uint64
+)
diff --git a/vsrepair/command/hs/get.go b/vsrepair/command/hs/get.go
new file mode 100644
index 0000000..09caa21
--- /dev/null
+++ b/vsrepair/command/hs/get.go
@@ -0,0 +1,76 @@
+// Copyright 2022 Linkall Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package hs
+
+import (
+	// standard libraries.
+	"encoding/json"
+	"fmt"
+	"strconv"
+
+	// third-party libraries.
+	"github.com/spf13/cobra"
+
+	// first-party libraries.
+	"github.com/vanus-labs/vanus/raft/raftpb"
+
+	// this project.
+	"github.com/vanus-labs/vanus/vsrepair/meta"
+)
+
+func GetCommand() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "hs BlockID",
+		Short: "Get HardState of a specific Block.",
+		Run:   get,
+	}
+	cmd.Flags().StringVar(&volumePath, "volume", "", "volume path")
+	return cmd
+}
+
+type getResult struct {
+	BlockID   uint64           `json:"BlockID"`
+	HardState raftpb.HardState `json:"HardState"`
+}
+
+func get(cmd *cobra.Command, args []string) {
+	db, err := meta.Open(volumePath, meta.ReadOnly())
+	if err != nil {
+		panic(err)
+	}
+	defer db.Close()
+
+	for _, arg := range args {
+		id, err := strconv.ParseUint(arg, 0, 0)
+		if err != nil {
+			panic(err)
+		}
+
+		hs, err := db.GetHardState(id)
+		if err != nil {
+			panic(err)
+		}
+
+		jsonResult, err := json.MarshalIndent(getResult{
+			BlockID:   id,
+			HardState: hs,
+		}, "", "  ")
+		if err != nil {
+			panic(err)
+		}
+
+		fmt.Println(string(jsonResult))
+	}
+}
diff --git a/vsrepair/command/hs/modify.go b/vsrepair/command/hs/modify.go
new file mode 100644
index 0000000..42f7880
--- /dev/null
+++ b/vsrepair/command/hs/modify.go
@@ -0,0 +1,66 @@
+// Copyright 2022 Linkall Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package hs
+
+import (
+	// standard libraries.
+	"strconv"
+
+	// third-party libraries.
+	"github.com/spf13/cobra"
+
+	// this project.
+	"github.com/vanus-labs/vanus/vsrepair/meta"
+)
+
+func ModifyCommand() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "hs [--volume <volume path>] nodeID [--commit offset]",
+		Short: "modify HardState",
+		Run:   modify,
+	}
+	cmd.Flags().Uint64Var(&commit, "commit", 0, "commit offset")
+	return cmd
+}
+
+func modify(cmd *cobra.Command, args []string) {
+	if len(args) != 1 {
+		panic("invalid args")
+	}
+
+	db, err := meta.Open(volumePath)
+	if err != nil {
+		panic(err)
+	}
+	defer db.Close()
+
+	node, err := strconv.ParseUint(args[0], 0, 0)
+	if err != nil {
+		panic(err)
+	}
+
+	hs, err := db.GetHardState(node)
+	if err != nil {
+		panic(err)
+	}
+
+	if commit != 0 {
+		hs.Commit = commit
+	}
+
+	if err := db.PutHardState(node, hs); err != nil {
+		panic(err)
+	}
+}
diff --git a/vsrepair/command/modify.go b/vsrepair/command/modify.go
new file mode 100644
index 0000000..58cb137
--- /dev/null
+++ b/vsrepair/command/modify.go
@@ -0,0 +1,36 @@
+// Copyright 2022 Linkall Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package command
+
+import (
+	// third-party libraries.
+	"github.com/spf13/cobra"
+
+	// this project.
+	"github.com/vanus-labs/vanus/vsrepair/command/apply"
+	"github.com/vanus-labs/vanus/vsrepair/command/compact"
+	"github.com/vanus-labs/vanus/vsrepair/command/hs"
+)
+
+func ModifyCommand() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "modify SUB-COMMAND",
+		Short: "modify metadata",
+	}
+	cmd.AddCommand(hs.ModifyCommand())
+	cmd.AddCommand(apply.ModifyCommand())
+	cmd.AddCommand(compact.ModifyCommand())
+	return cmd
+}
diff --git a/vsrepair/command/review.go b/vsrepair/command/review.go
new file mode 100644
index 0000000..6e90198
--- /dev/null
+++ b/vsrepair/command/review.go
@@ -0,0 +1,32 @@
+// Copyright 2023 Linkall Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package command
+
+import (
+	// third-party libraries.
+	"github.com/spf13/cobra"
+
+	// this project.
+	"github.com/vanus-labs/vanus/vsrepair/command/compact"
+)
+
+func ReviewCommand() *cobra.Command {
+	cmd := &cobra.Command{
+		Use:   "review TYPE ID",
+		Short: "Review history of a specific resource.",
+	}
+	cmd.AddCommand(compact.ReviewCommand())
+	return cmd
+}
diff --git a/vsrepair/main.go b/vsrepair/main.go
new file mode 100644
index 0000000..32b194b
--- /dev/null
+++ b/vsrepair/main.go
@@ -0,0 +1,45 @@
+// Copyright 2022 Linkall Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package main
+
+import (
+	// standard libraries.
+	"os"
+
+	// third-party libraries.
+	"github.com/fatih/color"
+	"github.com/spf13/cobra"
+
+	// this project.
+	"github.com/vanus-labs/vanus/vsrepair/command"
+)
+
+var rootCmd = &cobra.Command{
+	Use:   "vsrepair",
+	Short: "The meta repair tool of vanus.",
+}
+
+func main() {
+	rootCmd.AddCommand(command.CreateCommand())
+	rootCmd.AddCommand(command.GetCommand())
+	rootCmd.AddCommand(command.ModifyCommand())
+	rootCmd.AddCommand(command.DescribeCommand())
+	rootCmd.AddCommand(command.ReviewCommand())
+
+	if err := rootCmd.Execute(); err != nil {
+		color.Red("vsrepair run error: %s", err)
+		os.Exit(-1)
+	}
+}
diff --git a/vsrepair/meta/db.go b/vsrepair/meta/db.go
new file mode 100644
index 0000000..c4dcbc2
--- /dev/null
+++ b/vsrepair/meta/db.go
@@ -0,0 +1,262 @@
+// Copyright 2022 Linkall Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package meta
+
+import (
+	// standard libraries.
+	"context"
+	"encoding/binary"
+	"errors"
+	"path/filepath"
+
+	// first-party libraries.
+	"github.com/vanus-labs/vanus/internal/store/meta"
+	"github.com/vanus-labs/vanus/internal/store/raft/storage"
+	walog "github.com/vanus-labs/vanus/internal/store/wal"
+	"github.com/vanus-labs/vanus/raft/raftpb"
+)
+
+var ErrNotFound = errors.New("not found")
+
+type DB struct {
+	metaStore   *meta.SyncStore
+	offsetStore *meta.AsyncStore
+}
+
+type config struct {
+	skipMetaStore bool
+	readOnly      bool
+}
+
+type Option func(*config)
+
+func SkipMetaStore() Option {
+	return func(c *config) {
+		c.skipMetaStore = true
+	}
+}
+
+func ReadOnly() Option {
+	return func(c *config) {
+		c.readOnly = true
+	}
+}
+
+func Open(volumeDir string, opts ...Option) (*DB, error) {
+	ctx := context.Background()
+
+	cfg := config{}
+	for _, opt := range opts {
+		opt(&cfg)
+	}
+
+	var metaStore *meta.SyncStore
+	if !cfg.skipMetaStore {
+		var err error
+		var metaOpts []walog.Option
+		if cfg.readOnly {
+			metaOpts = append(metaOpts, walog.WithReadOnly())
+		}
+		metaStore, err = meta.RecoverSyncStore(ctx, filepath.Join(volumeDir, "meta"), metaOpts...)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	var offsetOpts []walog.Option
+	if cfg.readOnly {
+		offsetOpts = append(offsetOpts, walog.WithReadOnly())
+	}
+	offsetStore, err := meta.RecoverAsyncStore(ctx, filepath.Join(volumeDir, "offset"), offsetOpts...)
+	if err != nil {
+		if !cfg.skipMetaStore {
+			metaStore.Close(ctx)
+		}
+		return nil, err
+	}
+
+	return &DB{
+		metaStore,
+		offsetStore,
+	}, nil
+}
+
+func (db *DB) Close() {
+	if db.metaStore != nil {
+		db.metaStore.Close(context.Background())
+	}
+	if db.offsetStore != nil {
+		db.offsetStore.Close()
+	}
+}
+
+type CompactInfo struct {
+	Index uint64 `json:"Index"`
+	Term  uint64 `json:"Term"`
+}
+
+type RaftDetail struct {
+	ConfState raftpb.ConfState `json:"ConfState"`
+	HardState raftpb.HardState `json:"HardState"`
+	Commit    uint64           `json:"Commit"`
+	Apply     uint64           `json:"Apply"`
+	Compact   CompactInfo      `json:"Compact"`
+}
+
+func (db *DB) GetRaftDetail(node uint64) (d RaftDetail, err error) {
+	d.Compact, err = db.GetCompact(node)
+	if err != nil {
+		return
+	}
+
+	d.ConfState, err = db.GetConfState(node)
+	if err != nil && err != ErrNotFound { //nolint:errorlint // compare to ErrNotFound is ok.
+		return
+	}
+
+	d.HardState, err = db.GetHardState(node)
+	if err != nil && err != ErrNotFound { //nolint:errorlint // compare to ErrNotFound is ok.
+		return
+	}
+
+	d.Commit, err = db.GetCommit(node)
+	if err != nil && err != ErrNotFound { //nolint:errorlint // compare to ErrNotFound is ok.
+		return
+	}
+
+	d.Apply, err = db.GetApply(node)
+	if err != nil && err != ErrNotFound { //nolint:errorlint // compare to ErrNotFound is ok.
+		return
+	}
+
+	return d, nil
+}
+
+func (db *DB) GetConfState(node uint64) (raftpb.ConfState, error) {
+	csKey := []byte(storage.ConfStateKey(node))
+
+	var cs raftpb.ConfState
+	if v, exist := db.metaStore.Load(csKey); exist {
+		b, ok := v.([]byte)
+		if !ok {
+			panic("confState is not []byte")
+		}
+		if err := cs.Unmarshal(b); err != nil {
+			return raftpb.ConfState{}, err
+		}
+	} else {
+		return raftpb.ConfState{}, ErrNotFound
+	}
+	return cs, nil
+}
+
+func (db *DB) GetHardState(node uint64) (raftpb.HardState, error) {
+	hsKey := []byte(storage.HardStateKey(node))
+
+	var hs raftpb.HardState
+	if v, exist := db.metaStore.Load(hsKey); exist {
+		b, ok := v.([]byte)
+		if !ok {
+			panic("hardState is not []byte")
+		}
+		if err := hs.Unmarshal(b); err != nil {
+			return raftpb.HardState{}, err
+		}
+	} else {
+		return raftpb.HardState{}, ErrNotFound
+	}
+
+	// clear commit
+	hs.Commit = 0
+
+	return hs, nil
+}
+
+func (db *DB) PutHardState(node uint64, hs raftpb.HardState) error {
+	hsKey := []byte(storage.HardStateKey(node))
+	offKey := []byte(storage.CommitKey(node))
+
+	data, err := hs.Marshal()
+	if err != nil {
+		return err
+	}
+	if err := meta.Store(context.Background(), db.metaStore, hsKey, data); err != nil {
+		return err
+	}
+	db.offsetStore.Delete(offKey)
+	return nil
+}
+
+func (db *DB) GetCommit(node uint64) (uint64, error) {
+	offKey := []byte(storage.CommitKey(node))
+
+	if v, exist := db.offsetStore.Load(offKey); exist {
+		off, ok := v.(uint64)
+		if !ok {
+			panic("commit is not uint64")
+		}
+		return off, nil
+	}
+	return 0, ErrNotFound
+}
+
+func (db *DB) GetApply(node uint64) (uint64, error) {
+	appKey := []byte(storage.ApplyKey(node))
+
+	if v, exist := db.offsetStore.Load(appKey); exist {
+		app, ok := v.(uint64)
+		if !ok {
+			panic("applied is not uint64")
+		}
+		return app, nil
+	}
+	return 0, ErrNotFound
+}
+
+func (db *DB) PutApply(node uint64, app uint64) error {
+	appKey := []byte(storage.ApplyKey(node))
+
+	db.offsetStore.Store(context.Background(), appKey, app)
+	return nil
+}
+
+func (db *DB) GetCompact(node uint64) (CompactInfo, error) {
+	comKey := []byte(storage.CompactKey(node))
+
+	if v, exist := db.metaStore.Load(comKey); exist {
+		com, ok := v.([]byte)
+		if !ok {
+			panic("compacted is not []byte")
+		}
+		info := CompactInfo{
+			Index: binary.BigEndian.Uint64(com[0:8]),
+			Term:  binary.BigEndian.Uint64(com[8:16]),
+		}
+		return info, nil
+	}
+	return CompactInfo{}, ErrNotFound
+}
+
+func (db *DB) PutCompact(node uint64, info CompactInfo) error {
+	comKey := []byte(storage.CompactKey(node))
+
+	var value [16]byte
+	binary.BigEndian.PutUint64(value[0:8], info.Index)
+	binary.BigEndian.PutUint64(value[8:16], info.Term)
+	if err := meta.Store(context.Background(), db.metaStore, comKey, value[:]); err != nil {
+		return err
+	}
+	return nil
+}
diff --git a/vsrepair/meta/review.go b/vsrepair/meta/review.go
new file mode 100644
index 0000000..430b216
--- /dev/null
+++ b/vsrepair/meta/review.go
@@ -0,0 +1,49 @@
+// Copyright 2023 Linkall Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package meta
+
+import (
+	// standard libraries.
+	"context"
+	"encoding/binary"
+	"path/filepath"
+
+	// this project.
+	"github.com/vanus-labs/vanus/internal/store/meta"
+	"github.com/vanus-labs/vanus/internal/store/raft/storage"
+	walog "github.com/vanus-labs/vanus/internal/store/wal"
+)
+
+func ReviewCompact(volumeDir string, node uint64, watcher func(*CompactInfo, int64)) error {
+	ctx := context.Background()
+	dir := filepath.Join(volumeDir, "meta")
+	comKey := []byte(storage.CompactKey(node))
+
+	return meta.ReviewSyncStore(ctx, dir, comKey, func(v interface{}, ver int64) {
+		if v == meta.DeletedMark {
+			watcher(nil, ver)
+		} else {
+			com, ok := v.([]byte)
+			if !ok {
+				panic("compacted is not []byte")
+			}
+			info := CompactInfo{
+				Index: binary.BigEndian.Uint64(com[0:8]),
+				Term:  binary.BigEndian.Uint64(com[8:16]),
+			}
+			watcher(&info, ver)
+		}
+	}, walog.WithReadOnly())
+}


Test patch:
diff --git a/internal/store/meta/codec_test.go b/internal/store/meta/codec_test.go
index 79b4dd3..3e875cc 100644
--- a/internal/store/meta/codec_test.go
+++ b/internal/store/meta/codec_test.go
@@ -27,7 +27,7 @@ import (
 
 func TestCodec_AppendValue(t *testing.T) {
 	Convey("append deletedMark", t, func() {
-		out, err := appendValue(nil, deletedMark)
+		out, err := appendValue(nil, DeletedMark)
 		So(err, ShouldBeNil)
 		So(bytes.Equal(out, []byte{byte(Deleted)}), ShouldBeTrue)
 	})