diff --git a/storage/engine/rocksdb.go b/storage/engine/rocksdb.go index b80c5b207bc5..87cbe6fb6f25 100644 --- a/storage/engine/rocksdb.go +++ b/storage/engine/rocksdb.go @@ -116,9 +116,28 @@ func (r *RocksDB) Open() error { humanize.IBytes(minMemtableBudget), util.IBytes(r.memtableBudget)) } + var ver int if len(r.dir) != 0 { log.Infof("opening rocksdb instance at %q", r.dir) + + // Check the version number. + var err error + if ver, err = getVersion(r.dir); err != nil { + return err + } + if ver < versionMinimum || ver > versionCurrent { + // 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", + versionCurrent, ver, versionMinimum) + } + } else { + log.Infof("opening in memory rocksdb instance") + + // In memory dbs are always current. + ver = versionCurrent } + status := C.DBOpen(&r.rdb, goToCSlice([]byte(r.dir)), C.DBOptions{ cache_size: C.uint64_t(r.cacheSize), @@ -126,11 +145,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) } + // Update or add the version file if needed. + if ver < versionCurrent { + 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..1b671853fb95 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, Version{versionCurrent}, ""}, + {true, Version{versionMinimum}, ""}, + {true, Version{-1}, "incompatible rocksdb data version, current:1, on disk:-1, minimum:0"}, + {true, Version{2}, "incompatible rocksdb data version, current:1, on disk:2, minimum:0"}, + } + + 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. +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, 0, minMemtableBudget, 0, stopper) + defer rocksdb.Close() + return rocksdb.Open() +} diff --git a/storage/engine/version.go b/storage/engine/version.go new file mode 100644 index 000000000000..60c009e83d80 --- /dev/null +++ b/storage/engine/version.go @@ -0,0 +1,78 @@ +// 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 = "COCKROACHDB_VERSION" + versionFilenameTemp = "COCKROACHDB_VERSION_TEMP" + versionNoFile = 0 + versionCurrent = 1 + versionMinimum = versionNoFile +) + +// Version stores all the version information for all stores and is used as +// the format for the version file. +type Version struct { + Version int +} + +// getVersionFilename returns the filename for the version file stored in the +// data directory. +func getVersionFilename(dir string) string { + return filepath.Join(dir, versionFilename) +} + +// getVersion returns the current on disk cockroach version from the version +// file in the passed in directory. If there is no version file yet, it +// returns 0. +func getVersion(dir string) (int, error) { + filename := getVersionFilename(dir) + b, err := ioutil.ReadFile(filename) + if err != nil { + if os.IsNotExist(err) { + return versionNoFile, nil + } + return 0, err + } + var ver Version + if err := json.Unmarshal(b, &ver); err != nil { + return 0, fmt.Errorf("version file %s is not formatted correctly; %s", filename, err) + } + return ver.Version, nil +} + +// writeVersionFile overwrites the version file to contain the latest version. +func writeVersionFile(dir string) error { + tempFilename := filepath.Join(dir, versionFilenameTemp) + filename := getVersionFilename(dir) + b, err := json.Marshal(Version{versionCurrent}) + if err != nil { + return err + } + // First write to a temp file. + if err := ioutil.WriteFile(tempFilename, b, 0644); err != nil { + return err + } + // Atomically rename the file to overwrite the version file on disk. + return os.Rename(tempFilename, filename) +} diff --git a/storage/engine/version_test.go b/storage/engine/version_test.go new file mode 100644 index 000000000000..8a3b36f8fb62 --- /dev/null +++ b/storage/engine/version_test.go @@ -0,0 +1,73 @@ +// 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" + "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 := getVersion(dir) + if err != nil { + t.Fatal(err) + } + if ver != versionNoFile { + t.Errorf("no version file version should be %d, got %d", versionNoFile, ver) + } + + // Write the current versions to the file. + if err := writeVersionFile(dir); err != nil { + t.Fatal(err) + } + ver, err = getVersion(dir) + if err != nil { + t.Fatal(err) + } + if ver != versionCurrent { + t.Errorf("current versions do not match, expected %d got %d", versionCurrent, ver) + } + + // 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 := getVersion(dir); !testutils.IsError(err, "is not formatted correctly") { + t.Errorf("expected error contains '%s', got '%s'", "is not formatted correctly", err) + } +}