From 892a4ed3d7eb9743aede7d686572784036f2a5b5 Mon Sep 17 00:00:00 2001 From: Bram Gruneir Date: Tue, 29 Mar 2016 14:54:38 -0400 Subject: [PATCH] storage.engine: add version file to store directory This adds a new file VERSION to all store directories. This has a tiny bit of future proofing to allow us to have multiple databases with different data versions as well as a minimum value we support. When needed, having this file will allow us to automatically perform migrations during the call to rocksDB.Open(). Fixes #2718. --- storage/engine/mvcc.pb.go | 2 + storage/engine/rocksdb.go | 25 +++++++++- storage/engine/rocksdb_test.go | 62 ++++++++++++++++++++++++ storage/engine/version.go | 87 ++++++++++++++++++++++++++++++++++ storage/engine/version_test.go | 75 +++++++++++++++++++++++++++++ 5 files changed, 249 insertions(+), 2 deletions(-) create mode 100644 storage/engine/version.go create mode 100644 storage/engine/version_test.go diff --git a/storage/engine/mvcc.pb.go b/storage/engine/mvcc.pb.go index bbace78d69c1..d9ae2b3fcb70 100644 --- a/storage/engine/mvcc.pb.go +++ b/storage/engine/mvcc.pb.go @@ -7,10 +7,12 @@ It is generated from these files: cockroach/storage/engine/mvcc.proto + cockroach/storage/engine/version.proto It has these top-level messages: MVCCMetadata MVCCStats + Version */ package engine diff --git a/storage/engine/rocksdb.go b/storage/engine/rocksdb.go index 8a18ccd3eb57..ab084e6ce42e 100644 --- a/storage/engine/rocksdb.go +++ b/storage/engine/rocksdb.go @@ -116,9 +116,24 @@ func (r *RocksDB) Open() error { humanize.IBytes(minMemtableBudget), util.IBytes(r.memtableBudget)) } + var ver Version if len(r.dir) != 0 { log.Infof("opening rocksdb instance at %q", r.dir) + + // Check the version number. + var err error + if ver, err = getVersions(r.dir); err != nil { + return err + } + if ver.RocksDBData != versionNoFile && (ver.RocksDBData < versionRocksDBMinimum || + ver.RocksDBData > versionRocksDBCurrent) { + // Instead of an error, we should call a migration if possible when + // one is needed immediately following the DBOpen call. + return fmt.Errorf("incompatible rocksdb data version, current:%d, on disk:%d, minimum:%d", + versionRocksDBCurrent, ver.RocksDBData, versionRocksDBMinimum) + } } + status := C.DBOpen(&r.rdb, goToCSlice([]byte(r.dir)), C.DBOptions{ cache_size: C.uint64_t(r.cacheSize), @@ -126,11 +141,17 @@ func (r *RocksDB) Open() error { allow_os_buffer: C.bool(true), logging_enabled: C.bool(log.V(3)), }) - err := statusToError(status) - if err != nil { + if err := statusToError(status); err != nil { return util.Errorf("could not open rocksdb instance: %s", err) } + // Write the version file if none exists. + if len(r.dir) > 0 && ver.RocksDBData == versionNoFile { + if err := writeVersionFile(r.dir); err != nil { + return err + } + } + // Start a goroutine that will finish when the underlying handle // is deallocated. This is used to check a leak in tests. go func() { diff --git a/storage/engine/rocksdb_test.go b/storage/engine/rocksdb_test.go index 11c9f3dbbe3a..b0ec092350dc 100644 --- a/storage/engine/rocksdb_test.go +++ b/storage/engine/rocksdb_test.go @@ -17,7 +17,10 @@ package engine import ( + "encoding/json" + "io/ioutil" "math/rand" + "os" "strconv" "testing" @@ -139,3 +142,62 @@ func benchmarkIterOnBatch(b *testing.B, writes int) { iter.Close() } } + +// TestRocksDBOpenWithVersions verifies the version checking in Open() +// functions correctly. +func TestRocksDBOpenWithVersions(t *testing.T) { + defer leaktest.AfterTest(t)() + + testCases := []struct { + hasFile bool + ver Version + expectedErr string + }{ + {false, Version{}, ""}, + {true, currentVersions, ""}, + {true, Version{0}, "incompatible rocksdb data version, current:1, on disk:0, minimum:1"}, + {true, Version{2}, "incompatible rocksdb data version, current:1, on disk:2, minimum:1"}, + } + + for i, testCase := range testCases { + err := openRocksDBWithVersion(t, testCase.hasFile, testCase.ver) + if err == nil && len(testCase.expectedErr) == 0 { + continue + } + if !testutils.IsError(err, testCase.expectedErr) { + t.Errorf("%d: expected error '%s', actual '%s'", i, testCase.expectedErr, err) + } + } +} + +// openRocksDBWithVersion attempts to open a rocks db instance, optionally with +// the supplied Version struct. It then closes the +func openRocksDBWithVersion(t *testing.T, hasVersionFile bool, ver Version) error { + stopper := stop.NewStopper() + defer stopper.Stop() + + dir, err := ioutil.TempDir("", "testing") + if err != nil { + t.Fatal(err) + } + defer func() { + if err := os.RemoveAll(dir); err != nil { + t.Fatal(err) + } + }() + + if hasVersionFile { + b, err := json.Marshal(ver) + if err != nil { + t.Fatal(err) + } + if err := ioutil.WriteFile(getVersionFilename(dir), b, 0644); err != nil { + t.Fatal(err) + } + } + + rocksdb := NewRocksDB(roachpb.Attributes{}, dir, 512<<20, minMemtableBudget, 0, stopper) + err = rocksdb.Open() + defer rocksdb.Close() + return err +} diff --git a/storage/engine/version.go b/storage/engine/version.go new file mode 100644 index 000000000000..8d70200f165f --- /dev/null +++ b/storage/engine/version.go @@ -0,0 +1,87 @@ +// Copyright 2016 The Cockroach Authors. +// +// 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 engine + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path/filepath" +) + +const ( + versionFilename = "VERSION" + versionNoFile = -1 + versionRocksDBCurrent = 1 + versionRocksDBMinimum = 1 +) + +// Version stores all the version information for all stores which is used as +// the format for the version file. +type Version struct { + RocksDBData int `json:"RocksDBData"` +} + +// currentVersions returns a Version struct with the most recent versions. +var currentVersions = Version{ + RocksDBData: versionRocksDBCurrent, +} + +// noVersionFile is a copy of Version in which all values are set to blank +var noVersionFile = Version{ + RocksDBData: versionNoFile, +} + +// getVersionFilename returns the filename for the version file stored in the +// data directory. +func getVersionFilename(dir string) string { + return filepath.Clean(dir + "/" + versionFilename) +} + +// getVersions returns the current on disk cockroach versions from the version +// file for the passed in directory. If there is no version file yet, it +// returns a versionNoFile for each version. +func getVersions(dir string) (Version, error) { + filename := getVersionFilename(dir) + b, err := ioutil.ReadFile(filename) + if err != nil { + if os.IsNotExist(err) { + return noVersionFile, nil + } + return Version{}, err + } + var ver Version + if err := json.Unmarshal(b, &ver); err != nil { + return Version{}, fmt.Errorf("version file %s is not formatted correctly; %s", filename, err) + } + return ver, nil +} + +// writeVersionFile overwrites the version file to contain the latest versions. +func writeVersionFile(dir string) error { + filename := getVersionFilename(dir) + if len(filename) == 0 { + return nil + } + if err := os.Remove(filename); err != nil && !os.IsNotExist(err) { + return err + } + b, err := json.Marshal(currentVersions) + if err != nil { + return err + } + return ioutil.WriteFile(filename, b, 0644) +} diff --git a/storage/engine/version_test.go b/storage/engine/version_test.go new file mode 100644 index 000000000000..c0e511373aa6 --- /dev/null +++ b/storage/engine/version_test.go @@ -0,0 +1,75 @@ +// Copyright 2016 The Cockroach Authors. +// +// 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 engine + +import ( + "io/ioutil" + "os" + "reflect" + "testing" + + "github.com/cockroachdb/cockroach/testutils" + "github.com/cockroachdb/cockroach/util/leaktest" +) + +// TestVersions verifies that both getVersions() and writeVersionFile work +// correctly. +func TestVersions(t *testing.T) { + defer leaktest.AfterTest(t)() + + dir, err := ioutil.TempDir("", "testing") + if err != nil { + t.Fatal(err) + } + defer func() { + if err := os.RemoveAll(dir); err != nil { + t.Fatal(err) + } + }() + + // First test when no file exists yet. + ver, err := getVersions(dir) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(ver, noVersionFile) { + t.Errorf("no file versions do not match, expected %+v got %+v", ver, noVersionFile) + } + + // Write the current versions to the file. + if err := writeVersionFile(dir); err != nil { + t.Fatal(err) + } + ver, err = getVersions(dir) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(ver, currentVersions) { + t.Errorf("current versions do not match, expected %+v got %+v", ver, currentVersions) + } + + // Write gibberish to the file. + filename := getVersionFilename(dir) + if err := os.Remove(filename); err != nil { + t.Fatal(err) + } + if err := ioutil.WriteFile(filename, []byte("cause an error please"), 0644); err != nil { + t.Fatal(err) + } + if _, err = getVersions(dir); !testutils.IsError(err, + "is not formatted correctly") { + t.Errorf("expected error contains '%s', got '%s'", "is not formatted correctly", err) + } +}