mirror of
				https://github.com/taigrr/bitcask
				synced 2025-01-18 04:03:17 -08:00 
			
		
		
		
	Improves Merge() operation by also pruning old key/value pairs (#29)
* Added new API Stats() and Prune() * Improved Merge() logic to also prune old key/values and actually reclaim disk space * Added backward compat for the old Merge() function * Refactor indexing of keys to items (hints) * Remove redundant TestOpenMerge * Add unit test for Stats() * Improve TestMerge()
This commit is contained in:
		
							parent
							
								
									b7ac95d66a
								
							
						
					
					
						commit
						51bac21c0a
					
				
							
								
								
									
										341
									
								
								bitcask.go
									
									
									
									
									
								
							
							
						
						
									
										341
									
								
								bitcask.go
									
									
									
									
									
								
							| @ -6,8 +6,8 @@ import ( | ||||
| 	"io" | ||||
| 	"io/ioutil" | ||||
| 	"os" | ||||
| 	"path" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| 
 | ||||
| 	"github.com/gofrs/flock" | ||||
| @ -46,6 +46,7 @@ type Bitcask struct { | ||||
| 	*flock.Flock | ||||
| 
 | ||||
| 	config    *config | ||||
| 	options   []Option | ||||
| 	path      string | ||||
| 	curr      *internal.Datafile | ||||
| 	keydir    *internal.Keydir | ||||
| @ -53,6 +54,30 @@ type Bitcask struct { | ||||
| 	trie      *trie.Trie | ||||
| } | ||||
| 
 | ||||
| // Stats is a struct returned by Stats() on an open Bitcask instance | ||||
| type Stats struct { | ||||
| 	Datafiles int | ||||
| 	Keys      int | ||||
| 	Size      int64 | ||||
| } | ||||
| 
 | ||||
| // Stats returns statistics about the database including the number of | ||||
| // data files, keys and overall size on disk of the data | ||||
| func (b *Bitcask) Stats() (stats Stats, err error) { | ||||
| 	var size int64 | ||||
| 
 | ||||
| 	size, err = internal.DirSize(b.path) | ||||
| 	if err != nil { | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	stats.Datafiles = len(b.datafiles) | ||||
| 	stats.Keys = b.keydir.Len() | ||||
| 	stats.Size = size | ||||
| 
 | ||||
| 	return | ||||
| } | ||||
| 
 | ||||
| // Close closes the database and removes the lock. It is important to call | ||||
| // Close() as this is the only way to cleanup the lock held by the open | ||||
| // database. | ||||
| @ -62,9 +87,16 @@ func (b *Bitcask) Close() error { | ||||
| 		os.Remove(b.Flock.Path()) | ||||
| 	}() | ||||
| 
 | ||||
| 	for _, df := range b.datafiles { | ||||
| 		df.Close() | ||||
| 	if err := b.keydir.Save(path.Join(b.path, "index")); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	for _, df := range b.datafiles { | ||||
| 		if err := df.Close(); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return b.curr.Close() | ||||
| } | ||||
| 
 | ||||
| @ -207,11 +239,11 @@ func (b *Bitcask) put(key string, value []byte) (int64, int64, error) { | ||||
| 	return b.curr.Write(e) | ||||
| } | ||||
| 
 | ||||
| // Merge merges all datafiles in the database creating hint files for faster | ||||
| // startup. Old keys are squashed and deleted keys removes. Call this function | ||||
| // periodically to reclaim disk space. | ||||
| func Merge(path string, force bool) error { | ||||
| 	fns, err := internal.GetDatafiles(path) | ||||
| func (b *Bitcask) reopen() error { | ||||
| 	b.mu.Lock() | ||||
| 	defer b.mu.Unlock() | ||||
| 
 | ||||
| 	fns, err := internal.GetDatafiles(b.path) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| @ -221,162 +253,36 @@ func Merge(path string, force bool) error { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	// Do not merge if we only have 1 Datafile | ||||
| 	if len(ids) <= 1 { | ||||
| 		return nil | ||||
| 	} | ||||
| 
 | ||||
| 	// Don't merge the Active Datafile (the last one) | ||||
| 	fns = fns[:len(fns)-1] | ||||
| 	ids = ids[:len(ids)-1] | ||||
| 
 | ||||
| 	temp, err := ioutil.TempDir(path, "merge") | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	defer os.RemoveAll(temp) | ||||
| 
 | ||||
| 	for i, fn := range fns { | ||||
| 		// Don't merge Datafiles whose .hint files we've already generated | ||||
| 		// (they are already merged); unless we set the force flag to true | ||||
| 		// (forcing a re-merge). | ||||
| 		if filepath.Ext(fn) == ".hint" && !force { | ||||
| 			// Already merged | ||||
| 			continue | ||||
| 		} | ||||
| 
 | ||||
| 		id := ids[i] | ||||
| 
 | ||||
| 		keydir := internal.NewKeydir() | ||||
| 
 | ||||
| 		df, err := internal.NewDatafile(path, id, true) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		defer df.Close() | ||||
| 
 | ||||
| 		for { | ||||
| 			e, n, err := df.Read() | ||||
| 			if err != nil { | ||||
| 				if err == io.EOF { | ||||
| 					break | ||||
| 				} | ||||
| 				return err | ||||
| 			} | ||||
| 
 | ||||
| 			// Tombstone value  (deleted key) | ||||
| 			if len(e.Value) == 0 { | ||||
| 				keydir.Delete(e.Key) | ||||
| 				continue | ||||
| 			} | ||||
| 
 | ||||
| 			keydir.Add(e.Key, ids[i], e.Offset, n) | ||||
| 		} | ||||
| 
 | ||||
| 		tempdf, err := internal.NewDatafile(temp, id, false) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		defer tempdf.Close() | ||||
| 
 | ||||
| 		for key := range keydir.Keys() { | ||||
| 			item, _ := keydir.Get(key) | ||||
| 			e, err := df.ReadAt(item.Offset, item.Size) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 
 | ||||
| 			_, _, err = tempdf.Write(e) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		err = tempdf.Close() | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| 		err = df.Close() | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| 		err = os.Rename(tempdf.Name(), df.Name()) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| 		hint := strings.TrimSuffix(df.Name(), ".data") + ".hint" | ||||
| 		err = keydir.Save(hint) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // Open opens the database at the given path with optional options. | ||||
| // Options can be provided with the `WithXXX` functions that provide | ||||
| // configuration options as functions. | ||||
| func Open(path string, options ...Option) (*Bitcask, error) { | ||||
| 	if err := os.MkdirAll(path, 0755); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	err := Merge(path, false) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	fns, err := internal.GetDatafiles(path) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	ids, err := internal.ParseIds(fns) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	var datafiles []*internal.Datafile | ||||
| 
 | ||||
| 	for _, id := range ids { | ||||
| 		df, err := internal.NewDatafile(b.path, id, true) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		datafiles = append(datafiles, df) | ||||
| 	} | ||||
| 
 | ||||
| 	keydir := internal.NewKeydir() | ||||
| 	trie := trie.New() | ||||
| 
 | ||||
| 	for i, fn := range fns { | ||||
| 		df, err := internal.NewDatafile(path, ids[i], true) | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 	if internal.Exists(path.Join(b.path, "index")) { | ||||
| 		if err := keydir.Load(path.Join(b.path, "index")); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		datafiles = append(datafiles, df) | ||||
| 
 | ||||
| 		if filepath.Ext(fn) == ".hint" { | ||||
| 			f, err := os.Open(filepath.Join(path, fn)) | ||||
| 			if err != nil { | ||||
| 				return nil, err | ||||
| 			} | ||||
| 			defer f.Close() | ||||
| 
 | ||||
| 			hint, err := internal.NewKeydirFromBytes(f) | ||||
| 			if err != nil { | ||||
| 				return nil, err | ||||
| 			} | ||||
| 
 | ||||
| 			for key := range hint.Keys() { | ||||
| 				item, _ := hint.Get(key) | ||||
| 				_ = keydir.Add(key, item.FileID, item.Offset, item.Size) | ||||
| 				trie.Add(key, item) | ||||
| 			} | ||||
| 		} else { | ||||
| 		for key := range keydir.Keys() { | ||||
| 			item, _ := keydir.Get(key) | ||||
| 			trie.Add(key, item) | ||||
| 		} | ||||
| 	} else { | ||||
| 		for i, df := range datafiles { | ||||
| 			for { | ||||
| 				e, n, err := df.Read() | ||||
| 				if err != nil { | ||||
| 					if err == io.EOF { | ||||
| 						break | ||||
| 					} | ||||
| 					return nil, err | ||||
| 					return err | ||||
| 				} | ||||
| 
 | ||||
| 				// Tombstone value  (deleted key) | ||||
| @ -396,24 +302,118 @@ func Open(path string, options ...Option) (*Bitcask, error) { | ||||
| 		id = ids[(len(ids) - 1)] | ||||
| 	} | ||||
| 
 | ||||
| 	curr, err := internal.NewDatafile(path, id, false) | ||||
| 	curr, err := internal.NewDatafile(b.path, id, false) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	b.curr = curr | ||||
| 	b.datafiles = datafiles | ||||
| 
 | ||||
| 	b.keydir = keydir | ||||
| 
 | ||||
| 	b.trie = trie | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // Merge merges all datafiles in the database. Old keys are squashed | ||||
| // and deleted keys removes. Duplicate key/value pairs are also removed. | ||||
| // Call this function periodically to reclaim disk space. | ||||
| func (b *Bitcask) Merge() error { | ||||
| 	// Temporary merged database path | ||||
| 	temp, err := ioutil.TempDir(b.path, "merge") | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	defer os.RemoveAll(temp) | ||||
| 
 | ||||
| 	// Create a merged database | ||||
| 	mdb, err := Open(temp, b.options...) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	// Rewrite all key/value pairs into merged database | ||||
| 	// Doing this automatically strips deleted keys and | ||||
| 	// old key/value pairs | ||||
| 	err = b.Fold(func(key string) error { | ||||
| 		value, err := b.Get(key) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| 		if err := mdb.Put(key, value); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| 		return nil | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	err = mdb.Close() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	// Close the database | ||||
| 	err = b.Close() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	// Remove all data files | ||||
| 	files, err := ioutil.ReadDir(b.path) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	for _, file := range files { | ||||
| 		if !file.IsDir() { | ||||
| 			err := os.RemoveAll(path.Join([]string{b.path, file.Name()}...)) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// Rename all merged data files | ||||
| 	files, err = ioutil.ReadDir(mdb.path) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	for _, file := range files { | ||||
| 		err := os.Rename( | ||||
| 			path.Join([]string{mdb.path, file.Name()}...), | ||||
| 			path.Join([]string{b.path, file.Name()}...), | ||||
| 		) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// And finally reopen the database | ||||
| 	return b.reopen() | ||||
| } | ||||
| 
 | ||||
| // Open opens the database at the given path with optional options. | ||||
| // Options can be provided with the `WithXXX` functions that provide | ||||
| // configuration options as functions. | ||||
| func Open(path string, options ...Option) (*Bitcask, error) { | ||||
| 	if err := os.MkdirAll(path, 0755); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	bitcask := &Bitcask{ | ||||
| 		Flock:     flock.New(filepath.Join(path, "lock")), | ||||
| 		config:    newDefaultConfig(), | ||||
| 		path:      path, | ||||
| 		curr:      curr, | ||||
| 		keydir:    keydir, | ||||
| 		datafiles: datafiles, | ||||
| 		trie:      trie, | ||||
| 		Flock:   flock.New(filepath.Join(path, "lock")), | ||||
| 		config:  newDefaultConfig(), | ||||
| 		options: options, | ||||
| 		path:    path, | ||||
| 	} | ||||
| 
 | ||||
| 	for _, opt := range options { | ||||
| 		err = opt(bitcask.config) | ||||
| 		if err != nil { | ||||
| 		if err := opt(bitcask.config); err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 	} | ||||
| @ -427,5 +427,22 @@ func Open(path string, options ...Option) (*Bitcask, error) { | ||||
| 		return nil, ErrDatabaseLocked | ||||
| 	} | ||||
| 
 | ||||
| 	if err := bitcask.reopen(); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return bitcask, nil | ||||
| } | ||||
| 
 | ||||
| // Merge calls Bitcask.Merge() | ||||
| // XXX: Deprecated; Please use the `.Merge()` method | ||||
| // XXX: This is only kept here for backwards compatibility | ||||
| //      it will be removed in future releases at some point | ||||
| func Merge(path string, force bool) error { | ||||
| 	db, err := Open(path) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	return db.Merge() | ||||
| } | ||||
|  | ||||
							
								
								
									
										127
									
								
								bitcask_test.go
									
									
									
									
									
								
							
							
						
						
									
										127
									
								
								bitcask_test.go
									
									
									
									
									
								
							| @ -211,38 +211,39 @@ func TestMaxValueSize(t *testing.T) { | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| func TestOpenMerge(t *testing.T) { | ||||
| func TestStats(t *testing.T) { | ||||
| 	var ( | ||||
| 		db  *Bitcask | ||||
| 		err error | ||||
| 	) | ||||
| 
 | ||||
| 	assert := assert.New(t) | ||||
| 
 | ||||
| 	testdir, err := ioutil.TempDir("", "bitcask") | ||||
| 	assert.NoError(err) | ||||
| 
 | ||||
| 	t.Run("Setup", func(t *testing.T) { | ||||
| 		var ( | ||||
| 			db  *Bitcask | ||||
| 			err error | ||||
| 		) | ||||
| 
 | ||||
| 		t.Run("Open", func(t *testing.T) { | ||||
| 			db, err = Open(testdir, WithMaxDatafileSize(32)) | ||||
| 			db, err = Open(testdir) | ||||
| 			assert.NoError(err) | ||||
| 		}) | ||||
| 
 | ||||
| 		t.Run("Put", func(t *testing.T) { | ||||
| 			for i := 0; i < 1024; i++ { | ||||
| 				err = db.Put(string(i), []byte(strings.Repeat(" ", 1024))) | ||||
| 				assert.NoError(err) | ||||
| 			} | ||||
| 			err := db.Put("foo", []byte("bar")) | ||||
| 			assert.NoError(err) | ||||
| 		}) | ||||
| 
 | ||||
| 		t.Run("Get", func(t *testing.T) { | ||||
| 			for i := 0; i < 32; i++ { | ||||
| 				err = db.Put(string(i), []byte(strings.Repeat(" ", 1024))) | ||||
| 				assert.NoError(err) | ||||
| 				val, err := db.Get(string(i)) | ||||
| 				assert.NoError(err) | ||||
| 				assert.Equal([]byte(strings.Repeat(" ", 1024)), val) | ||||
| 			} | ||||
| 			val, err := db.Get("foo") | ||||
| 			assert.NoError(err) | ||||
| 			assert.Equal([]byte("bar"), val) | ||||
| 		}) | ||||
| 
 | ||||
| 		t.Run("Stats", func(t *testing.T) { | ||||
| 			stats, err := db.Stats() | ||||
| 			assert.NoError(err) | ||||
| 			assert.Equal(stats.Datafiles, 0) | ||||
| 			assert.Equal(stats.Keys, 1) | ||||
| 		}) | ||||
| 
 | ||||
| 		t.Run("Sync", func(t *testing.T) { | ||||
| @ -255,34 +256,9 @@ func TestOpenMerge(t *testing.T) { | ||||
| 			assert.NoError(err) | ||||
| 		}) | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("Merge", func(t *testing.T) { | ||||
| 		var ( | ||||
| 			db  *Bitcask | ||||
| 			err error | ||||
| 		) | ||||
| 
 | ||||
| 		t.Run("Open", func(t *testing.T) { | ||||
| 			db, err = Open(testdir) | ||||
| 			assert.NoError(err) | ||||
| 		}) | ||||
| 
 | ||||
| 		t.Run("Get", func(t *testing.T) { | ||||
| 			for i := 0; i < 32; i++ { | ||||
| 				val, err := db.Get(string(i)) | ||||
| 				assert.NoError(err) | ||||
| 				assert.Equal([]byte(strings.Repeat(" ", 1024)), val) | ||||
| 			} | ||||
| 		}) | ||||
| 
 | ||||
| 		t.Run("Close", func(t *testing.T) { | ||||
| 			err = db.Close() | ||||
| 			assert.NoError(err) | ||||
| 		}) | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| func TestMergeOpen(t *testing.T) { | ||||
| func TestMerge(t *testing.T) { | ||||
| 	var ( | ||||
| 		db  *Bitcask | ||||
| 		err error | ||||
| @ -300,22 +276,40 @@ func TestMergeOpen(t *testing.T) { | ||||
| 		}) | ||||
| 
 | ||||
| 		t.Run("Put", func(t *testing.T) { | ||||
| 			for i := 0; i < 1024; i++ { | ||||
| 				err = db.Put(string(i), []byte(strings.Repeat(" ", 1024))) | ||||
| 			err := db.Put("foo", []byte("bar")) | ||||
| 			assert.NoError(err) | ||||
| 		}) | ||||
| 
 | ||||
| 		s1, err := db.Stats() | ||||
| 		assert.NoError(err) | ||||
| 		assert.Equal(0, s1.Datafiles) | ||||
| 		assert.Equal(1, s1.Keys) | ||||
| 
 | ||||
| 		t.Run("Put", func(t *testing.T) { | ||||
| 			for i := 0; i < 10; i++ { | ||||
| 				err := db.Put("foo", []byte("bar")) | ||||
| 				assert.NoError(err) | ||||
| 			} | ||||
| 		}) | ||||
| 
 | ||||
| 		t.Run("Get", func(t *testing.T) { | ||||
| 			for i := 0; i < 32; i++ { | ||||
| 				err = db.Put(string(i), []byte(strings.Repeat(" ", 1024))) | ||||
| 				assert.NoError(err) | ||||
| 				val, err := db.Get(string(i)) | ||||
| 				assert.NoError(err) | ||||
| 				assert.Equal([]byte(strings.Repeat(" ", 1024)), val) | ||||
| 			} | ||||
| 		s2, err := db.Stats() | ||||
| 		assert.NoError(err) | ||||
| 		assert.Equal(5, s2.Datafiles) | ||||
| 		assert.Equal(1, s2.Keys) | ||||
| 		assert.True(s2.Size > s1.Size) | ||||
| 
 | ||||
| 		t.Run("Merge", func(t *testing.T) { | ||||
| 			err := db.Merge() | ||||
| 			assert.NoError(err) | ||||
| 		}) | ||||
| 
 | ||||
| 		s3, err := db.Stats() | ||||
| 		assert.NoError(err) | ||||
| 		assert.Equal(1, s3.Datafiles) | ||||
| 		assert.Equal(1, s3.Keys) | ||||
| 		assert.True(s3.Size > s1.Size) | ||||
| 		assert.True(s3.Size < s2.Size) | ||||
| 
 | ||||
| 		t.Run("Sync", func(t *testing.T) { | ||||
| 			err = db.Sync() | ||||
| 			assert.NoError(err) | ||||
| @ -326,31 +320,6 @@ func TestMergeOpen(t *testing.T) { | ||||
| 			assert.NoError(err) | ||||
| 		}) | ||||
| 	}) | ||||
| 
 | ||||
| 	t.Run("Merge", func(t *testing.T) { | ||||
| 		t.Run("Merge", func(t *testing.T) { | ||||
| 			err = Merge(testdir, true) | ||||
| 			assert.NoError(err) | ||||
| 		}) | ||||
| 
 | ||||
| 		t.Run("Open", func(t *testing.T) { | ||||
| 			db, err = Open(testdir) | ||||
| 			assert.NoError(err) | ||||
| 		}) | ||||
| 
 | ||||
| 		t.Run("Get", func(t *testing.T) { | ||||
| 			for i := 0; i < 32; i++ { | ||||
| 				val, err := db.Get(string(i)) | ||||
| 				assert.NoError(err) | ||||
| 				assert.Equal([]byte(strings.Repeat(" ", 1024)), val) | ||||
| 			} | ||||
| 		}) | ||||
| 
 | ||||
| 		t.Run("Close", func(t *testing.T) { | ||||
| 			err = db.Close() | ||||
| 			assert.NoError(err) | ||||
| 		}) | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| func TestConcurrent(t *testing.T) { | ||||
|  | ||||
| @ -20,28 +20,23 @@ keys.`, | ||||
| 	Args: cobra.ExactArgs(0), | ||||
| 	Run: func(cmd *cobra.Command, args []string) { | ||||
| 		path := viper.GetString("path") | ||||
| 		force, err := cmd.Flags().GetBool("force") | ||||
| 		if err != nil { | ||||
| 			log.WithError(err).Error("error parsing force flag") | ||||
| 			os.Exit(1) | ||||
| 		} | ||||
| 
 | ||||
| 		os.Exit(merge(path, force)) | ||||
| 		os.Exit(merge(path)) | ||||
| 	}, | ||||
| } | ||||
| 
 | ||||
| func init() { | ||||
| 	RootCmd.AddCommand(mergeCmd) | ||||
| 
 | ||||
| 	mergeCmd.Flags().BoolP( | ||||
| 		"force", "f", false, | ||||
| 		"Force a re-merge even if .hint files exist", | ||||
| 	) | ||||
| } | ||||
| 
 | ||||
| func merge(path string, force bool) int { | ||||
| 	err := bitcask.Merge(path, force) | ||||
| func merge(path string) int { | ||||
| 	db, err := bitcask.Open(path) | ||||
| 	if err != nil { | ||||
| 		log.WithError(err).Error("error opening database") | ||||
| 		return 1 | ||||
| 	} | ||||
| 
 | ||||
| 	if err = db.Merge(); err != nil { | ||||
| 		log.WithError(err).Error("error merging database") | ||||
| 		return 1 | ||||
| 	} | ||||
|  | ||||
							
								
								
									
										55
									
								
								cmd/bitcask/stats.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								cmd/bitcask/stats.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,55 @@ | ||||
| package main | ||||
| 
 | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 
 | ||||
| 	log "github.com/sirupsen/logrus" | ||||
| 	"github.com/spf13/cobra" | ||||
| 	"github.com/spf13/viper" | ||||
| 
 | ||||
| 	"github.com/prologic/bitcask" | ||||
| ) | ||||
| 
 | ||||
| var statsCmd = &cobra.Command{ | ||||
| 	Use:     "stats", | ||||
| 	Aliases: []string{}, | ||||
| 	Short:   "Display statis about the Database", | ||||
| 	Long:    `This displays statistics about the Database"`, | ||||
| 	Args:    cobra.ExactArgs(0), | ||||
| 	Run: func(cmd *cobra.Command, args []string) { | ||||
| 		path := viper.GetString("path") | ||||
| 
 | ||||
| 		os.Exit(stats(path)) | ||||
| 	}, | ||||
| } | ||||
| 
 | ||||
| func init() { | ||||
| 	RootCmd.AddCommand(statsCmd) | ||||
| } | ||||
| 
 | ||||
| func stats(path string) int { | ||||
| 	db, err := bitcask.Open(path) | ||||
| 	if err != nil { | ||||
| 		log.WithError(err).Error("error opening database") | ||||
| 		return 1 | ||||
| 	} | ||||
| 	defer db.Close() | ||||
| 
 | ||||
| 	stats, err := db.Stats() | ||||
| 	if err != nil { | ||||
| 		log.WithError(err).Error("error getting stats") | ||||
| 		return 1 | ||||
| 	} | ||||
| 
 | ||||
| 	data, err := json.MarshalIndent(stats, "", "  ") | ||||
| 	if err != nil { | ||||
| 		log.WithError(err).Error("error marshalling stats") | ||||
| 		return 1 | ||||
| 	} | ||||
| 
 | ||||
| 	fmt.Println(string(data)) | ||||
| 
 | ||||
| 	return 0 | ||||
| } | ||||
| @ -5,6 +5,7 @@ import ( | ||||
| 	"encoding/gob" | ||||
| 	"io" | ||||
| 	"io/ioutil" | ||||
| 	"os" | ||||
| 	"sync" | ||||
| ) | ||||
| 
 | ||||
| @ -81,6 +82,21 @@ func (k *Keydir) Bytes() ([]byte, error) { | ||||
| 	return buf.Bytes(), nil | ||||
| } | ||||
| 
 | ||||
| func (k *Keydir) Load(fn string) error { | ||||
| 	f, err := os.Open(fn) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	defer f.Close() | ||||
| 
 | ||||
| 	dec := gob.NewDecoder(f) | ||||
| 	if err := dec.Decode(&k.kv); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (k *Keydir) Save(fn string) error { | ||||
| 	data, err := k.Bytes() | ||||
| 	if err != nil { | ||||
|  | ||||
| @ -2,12 +2,32 @@ package internal | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"sort" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
| ) | ||||
| 
 | ||||
| func Exists(path string) bool { | ||||
| 	_, err := os.Stat(path) | ||||
| 	return err == nil | ||||
| } | ||||
| 
 | ||||
| func DirSize(path string) (int64, error) { | ||||
| 	var size int64 | ||||
| 	err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error { | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		if !info.IsDir() { | ||||
| 			size += info.Size() | ||||
| 		} | ||||
| 		return err | ||||
| 	}) | ||||
| 	return size, err | ||||
| } | ||||
| 
 | ||||
| func GetDatafiles(path string) ([]string, error) { | ||||
| 	fns, err := filepath.Glob(fmt.Sprintf("%s/*.data", path)) | ||||
| 	if err != nil { | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user