-
Notifications
You must be signed in to change notification settings - Fork 151
go/oasis-node/cmd/storage: Add create and import checkpoint cmd #6454
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
martintomazic
wants to merge
5
commits into
master
Choose a base branch
from
martin/feature/create-checkpoint-cmd
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
851cc3d
go/oasis-node/cmd/storage: Add create and import checkpoint cmd
martintomazic 44005e2
go/oasis-test-runner/e2e: Add create import checkpoint test
martintomazic 7a3bee8
go/oasis-test-runner: Add wait methods to controller
martintomazic 296298b
Fixup: Fix flaky test and ensure sync from imported checkpoint
martintomazic 194a24f
Fixup: Make start and abort multipart insert version scoped
martintomazic File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Empty file.
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,323 @@ | ||
| package storage | ||
|
|
||
| import ( | ||
| "context" | ||
| "os" | ||
| "path/filepath" | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/require" | ||
|
|
||
| "github.com/oasisprotocol/oasis-core/go/common" | ||
| "github.com/oasisprotocol/oasis-core/go/storage/mkvs" | ||
| dbAPI "github.com/oasisprotocol/oasis-core/go/storage/mkvs/db/api" | ||
| "github.com/oasisprotocol/oasis-core/go/storage/mkvs/db/pathbadger" | ||
| "github.com/oasisprotocol/oasis-core/go/storage/mkvs/node" | ||
| ) | ||
|
|
||
| const testVersion uint64 = 10 | ||
|
|
||
| var testNs common.Namespace = common.NewTestNamespaceFromSeed([]byte("test namespace"), 0) | ||
|
|
||
| func TestCreateCheckpoints(t *testing.T) { | ||
| t.Run("fails on non-empty output dir", func(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| ctx := t.Context() | ||
| ns := common.Namespace{} | ||
| ndb, err := newTestNodeDB(t, ns) | ||
| require.NoError(t, err) | ||
| defer ndb.Close() | ||
|
|
||
| stateRoot := commitRoot(ctx, t, ndb, ns, testVersion, node.RootTypeState, map[string]string{ | ||
| "key": "value", | ||
| }) | ||
| require.NoError(t, ndb.Finalize([]node.Root{stateRoot})) | ||
|
|
||
| outDir := filepath.Join(t.TempDir(), "checkpoints") | ||
|
martintomazic marked this conversation as resolved.
|
||
| require.NoError(t, os.Mkdir(outDir, 0o700)) | ||
| require.NoError(t, os.WriteFile(filepath.Join(outDir, "existing"), []byte{}, 0o600)) | ||
|
|
||
| err = createCheckpoints(ctx, ndb, ns, testVersion, outDir) | ||
| require.ErrorContains(t, err, "output directory is not empty") | ||
| }) | ||
|
|
||
| t.Run("fails on empty node DB", func(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| ctx := t.Context() | ||
| ns := common.Namespace{} | ||
| ndb, err := newTestNodeDB(t, ns) | ||
| require.NoError(t, err) | ||
| defer ndb.Close() | ||
|
|
||
| cpDir := filepath.Join(t.TempDir(), "checkpoint") | ||
| err = createCheckpoints(ctx, ndb, ns, testVersion, cpDir) | ||
| require.ErrorContains(t, err, "empty state DB") | ||
| }) | ||
|
|
||
| t.Run("fails when version is before earliest", func(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| ctx := t.Context() | ||
| ns := common.Namespace{} | ||
| ndb, err := newTestNodeDB(t, ns) | ||
| require.NoError(t, err) | ||
| defer ndb.Close() | ||
|
|
||
| stateRoot := commitRoot(ctx, t, ndb, ns, testVersion, node.RootTypeState, map[string]string{ | ||
| "key": "value", | ||
| }) | ||
| require.NoError(t, ndb.Finalize([]node.Root{stateRoot})) | ||
|
|
||
| cpDir := filepath.Join(t.TempDir(), "checkpoint") | ||
| err = createCheckpoints(ctx, ndb, ns, testVersion-1, cpDir) | ||
| require.ErrorContains(t, err, "version not found") | ||
| }) | ||
|
|
||
| t.Run("fails when version is after latest", func(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| ctx := t.Context() | ||
| ns := common.Namespace{} | ||
| ndb, err := newTestNodeDB(t, ns) | ||
| require.NoError(t, err) | ||
| defer ndb.Close() | ||
|
|
||
| stateRoot := commitRoot(ctx, t, ndb, ns, testVersion, node.RootTypeState, map[string]string{ | ||
| "key": "value", | ||
| }) | ||
| require.NoError(t, ndb.Finalize([]node.Root{stateRoot})) | ||
|
|
||
| cpDir := filepath.Join(t.TempDir(), "checkpoint") | ||
| err = createCheckpoints(ctx, ndb, ns, testVersion+1, cpDir) | ||
| require.ErrorContains(t, err, "version not found") | ||
| }) | ||
| } | ||
|
|
||
| func TestImportCheckpoints(t *testing.T) { | ||
| t.Run("fails on non-empty destination DB", func(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| ctx := t.Context() | ||
| ns := common.Namespace{} | ||
|
|
||
| srcNDB, err := newTestNodeDB(t, ns) | ||
| require.NoError(t, err) | ||
| defer srcNDB.Close() | ||
|
|
||
| stateRoot := commitRoot(ctx, t, srcNDB, ns, testVersion, node.RootTypeState, map[string]string{ | ||
| "key": "value", | ||
| }) | ||
| require.NoError(t, srcNDB.Finalize([]node.Root{stateRoot})) | ||
|
|
||
| cpDir := filepath.Join(t.TempDir(), "checkpoint") | ||
| require.NoError(t, createCheckpoints(ctx, srcNDB, ns, testVersion, cpDir)) | ||
|
|
||
| dstNDB, err := newTestNodeDB(t, ns) | ||
| require.NoError(t, err) | ||
| defer dstNDB.Close() | ||
|
|
||
| // NodeDB allows importing checkpoint even if the target NodeDB is not empty, | ||
| // as long as the checkpoint is younger than the the last finalized version. | ||
| // We don't want that, thus finalizing testVersion-1 to ensure createCheckpoints | ||
| // guard is tested and that the test does not rely on mkvs version already finalized. | ||
| err = dstNDB.Finalize([]node.Root{emptyRoot(ns, testVersion-1, node.RootTypeState)}) | ||
| require.NoError(t, err) | ||
|
|
||
| err = importCheckpoints(ctx, dstNDB, ns, cpDir) | ||
| require.ErrorContains(t, err, "state db not empty") | ||
| }) | ||
|
|
||
| t.Run("fails on empty input dir", func(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| ctx := t.Context() | ||
| ns := common.Namespace{} | ||
| ndb, err := newTestNodeDB(t, ns) | ||
| require.NoError(t, err) | ||
| defer ndb.Close() | ||
|
|
||
| err = importCheckpoints(ctx, ndb, ns, t.TempDir()) | ||
| require.ErrorContains(t, err, "input directory is empty") | ||
| }) | ||
| } | ||
|
|
||
| func TestCreateImportRoundtrip(t *testing.T) { | ||
| t.Run("single non-empty checkpoint", func(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| ctx := t.Context() | ||
| ns := common.Namespace{} | ||
|
|
||
| srcNDB, err := newTestNodeDB(t, ns) | ||
| require.NoError(t, err) | ||
| defer srcNDB.Close() | ||
|
|
||
| stateRoot := commitRoot(ctx, t, srcNDB, ns, testVersion, node.RootTypeState, map[string]string{ | ||
| "key": "value", | ||
| }) | ||
| require.NoError(t, srcNDB.Finalize([]node.Root{stateRoot})) | ||
|
|
||
| cpDir := filepath.Join(t.TempDir(), "checkpoint") | ||
| require.NoError(t, createCheckpoints(ctx, srcNDB, ns, testVersion, cpDir)) | ||
|
|
||
| dstNDB, err := newTestNodeDB(t, ns) | ||
| require.NoError(t, err) | ||
| defer dstNDB.Close() | ||
|
|
||
| require.NoError(t, importCheckpoints(ctx, dstNDB, ns, cpDir)) | ||
|
|
||
| roots, err := dstNDB.GetRootsForVersion(testVersion) | ||
| require.NoError(t, err) | ||
| require.Equal(t, []node.Root{stateRoot}, roots) | ||
| require.True(t, dstNDB.HasRoot(stateRoot)) | ||
| }) | ||
|
|
||
| t.Run("empty root is imported", func(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| ctx := t.Context() | ||
| ns := common.Namespace{} | ||
| stateRoot := emptyRoot(ns, testVersion, node.RootTypeState) | ||
|
|
||
| srcNDB, err := newTestNodeDB(t, ns) | ||
| require.NoError(t, err) | ||
| defer srcNDB.Close() | ||
|
|
||
| require.NoError(t, srcNDB.Finalize([]node.Root{stateRoot})) | ||
|
|
||
| latest, ok := srcNDB.GetLatestVersion() | ||
| require.True(t, ok) | ||
| require.EqualValues(t, testVersion, latest) | ||
|
|
||
| // Empty roots are implicit yet GetRootForVersion does not return them. | ||
| roots, err := srcNDB.GetRootsForVersion(testVersion) | ||
| require.NoError(t, err) | ||
| require.Empty(t, roots) | ||
| require.True(t, srcNDB.HasRoot(stateRoot)) | ||
|
|
||
| cpDir := filepath.Join(t.TempDir(), "checkpoint") | ||
| require.NoError(t, createCheckpoints(ctx, srcNDB, ns, testVersion, cpDir)) | ||
|
|
||
| dstNDB, err := newTestNodeDB(t, ns) | ||
| require.NoError(t, err) | ||
| defer dstNDB.Close() | ||
|
|
||
| require.NoError(t, importCheckpoints(ctx, dstNDB, ns, cpDir)) | ||
|
|
||
| latest, ok = dstNDB.GetLatestVersion() | ||
| require.True(t, ok) | ||
| require.EqualValues(t, testVersion, latest) | ||
|
|
||
| roots, err = dstNDB.GetRootsForVersion(testVersion) | ||
| require.NoError(t, err) | ||
| // Via checkpoint roundtrip, empty state can be materialized as an explicit root, | ||
| // which technically makes checkpoint restoration not 1:1 match. Same issue would | ||
| // happen with checkpointer if this case would be ever triggered. | ||
| require.Contains(t, roots, stateRoot) | ||
| require.True(t, dstNDB.HasRoot(stateRoot)) | ||
| }) | ||
|
|
||
| t.Run("two non-empty roots (state and io)", func(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| ctx := t.Context() | ||
|
|
||
| srcNDB, err := newTestNodeDB(t, testNs) | ||
| require.NoError(t, err) | ||
| defer srcNDB.Close() | ||
|
|
||
| stateRoot := commitRoot(ctx, t, srcNDB, testNs, testVersion, node.RootTypeState, map[string]string{ | ||
| "state-key": "state-value", | ||
| }) | ||
| ioRoot := commitRoot(ctx, t, srcNDB, testNs, testVersion, node.RootTypeIO, map[string]string{ | ||
| "io-key": "io-value", | ||
| }) | ||
| require.NoError(t, srcNDB.Finalize([]node.Root{stateRoot, ioRoot})) | ||
|
|
||
| cpDir := filepath.Join(t.TempDir(), "checkpoint") | ||
| require.NoError(t, createCheckpoints(ctx, srcNDB, testNs, testVersion, cpDir)) | ||
|
|
||
| dstNDB, err := newTestNodeDB(t, testNs) | ||
| require.NoError(t, err) | ||
| defer dstNDB.Close() | ||
|
|
||
| require.NoError(t, importCheckpoints(ctx, dstNDB, testNs, cpDir)) | ||
|
|
||
| roots, err := dstNDB.GetRootsForVersion(testVersion) | ||
| require.NoError(t, err) | ||
| require.ElementsMatch(t, []node.Root{stateRoot, ioRoot}, roots) | ||
| }) | ||
|
|
||
| t.Run("non-empty state root and empty io root", func(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| ctx := t.Context() | ||
|
|
||
| srcNDB, err := newTestNodeDB(t, testNs) | ||
| require.NoError(t, err) | ||
| defer srcNDB.Close() | ||
|
|
||
| stateRoot := commitRoot(ctx, t, srcNDB, testNs, testVersion, node.RootTypeState, map[string]string{ | ||
| "state-key": "state-value", | ||
| }) | ||
| ioRoot := emptyRoot(testNs, testVersion, node.RootTypeIO) | ||
| require.NoError(t, srcNDB.Finalize([]node.Root{stateRoot, ioRoot})) | ||
|
|
||
| cpDir := filepath.Join(t.TempDir(), "checkpoint") | ||
| require.NoError(t, createCheckpoints(ctx, srcNDB, testNs, testVersion, cpDir)) | ||
|
|
||
| dstNDB, err := newTestNodeDB(t, testNs) | ||
| require.NoError(t, err) | ||
| defer dstNDB.Close() | ||
|
|
||
| require.NoError(t, importCheckpoints(ctx, dstNDB, testNs, cpDir)) | ||
|
|
||
| roots, err := dstNDB.GetRootsForVersion(testVersion) | ||
| require.NoError(t, err) | ||
| require.Contains(t, roots, stateRoot) | ||
| require.True(t, dstNDB.HasRoot(stateRoot)) | ||
| require.True(t, dstNDB.HasRoot(ioRoot)) | ||
| }) | ||
| } | ||
|
|
||
| func newTestNodeDB(t *testing.T, ns common.Namespace) (dbAPI.NodeDB, error) { | ||
| t.Helper() | ||
|
|
||
| return pathbadger.New(&dbAPI.Config{ | ||
| DB: filepath.Join(t.TempDir()), | ||
| Namespace: ns, | ||
| MemoryOnly: true, | ||
| }) | ||
| } | ||
|
|
||
| func commitRoot( | ||
| ctx context.Context, | ||
| t *testing.T, | ||
| ndb dbAPI.NodeDB, | ||
| ns common.Namespace, | ||
| version uint64, | ||
| rootType node.RootType, | ||
| data map[string]string, | ||
| ) node.Root { | ||
| t.Helper() | ||
|
|
||
| tree := mkvs.New(nil, ndb, rootType) | ||
| defer tree.Close() | ||
|
|
||
| for k, v := range data { | ||
| err := tree.Insert(ctx, []byte(k), []byte(v)) | ||
| require.NoError(t, err) | ||
| } | ||
|
|
||
| _, rootHash, err := tree.Commit(ctx, ns, version) | ||
| require.NoError(t, err) | ||
|
|
||
| return node.Root{ | ||
| Namespace: ns, | ||
| Version: version, | ||
| Type: rootType, | ||
| Hash: rootHash, | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.