Skip to content

Commit

Permalink
storage.engine: add version file to store directory
Browse files Browse the repository at this point in the history
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 cockroachdb#2718.
  • Loading branch information
BramGruneir committed Mar 29, 2016
1 parent 5ba490a commit 892a4ed
Show file tree
Hide file tree
Showing 5 changed files with 249 additions and 2 deletions.
2 changes: 2 additions & 0 deletions storage/engine/mvcc.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 23 additions & 2 deletions storage/engine/rocksdb.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,21 +116,42 @@ 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),
memtable_budget: C.uint64_t(r.memtableBudget),
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() {
Expand Down
62 changes: 62 additions & 0 deletions storage/engine/rocksdb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@
package engine

import (
"encoding/json"
"io/ioutil"
"math/rand"
"os"
"strconv"
"testing"

Expand Down Expand Up @@ -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
}
87 changes: 87 additions & 0 deletions storage/engine/version.go
Original file line number Diff line number Diff line change
@@ -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)
}
75 changes: 75 additions & 0 deletions storage/engine/version_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}

0 comments on commit 892a4ed

Please sign in to comment.