mirror of
				https://github.com/taigrr/bitcask
				synced 2025-01-18 04:03:17 -08:00 
			
		
		
		
	Add support for keys with ttl (#177)
* ttl support first commit * imports fix * put api args correction * put options added * upgrade method added * upgrade log added * v0 to v1 migration script added * error assertion added * temp migration dir fix Co-authored-by: yash <yash.chandra@grabpay.com>
This commit is contained in:
		
							parent
							
								
									f397bec88f
								
							
						
					
					
						commit
						5c6ceadac1
					
				
							
								
								
									
										1
									
								
								AUTHORS
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								AUTHORS
									
									
									
									
									
								
							| @ -13,6 +13,7 @@ Kebert Xela kebertxela | |||||||
| panyun panyun  | panyun panyun  | ||||||
| shiniao <zhuzhezhe95@gmail.com> | shiniao <zhuzhezhe95@gmail.com> | ||||||
| Whemoon Jang <palindrom615@gmail.com> | Whemoon Jang <palindrom615@gmail.com> | ||||||
|  | Yash Chandra <yashschandra@gmail.com> | ||||||
| Yury Fedorov orlangure | Yury Fedorov orlangure | ||||||
| o2gy84 <o2gy84@gmail.com> | o2gy84 <o2gy84@gmail.com> | ||||||
| garsue <labs.garsue@gmail.com> | garsue <labs.garsue@gmail.com> | ||||||
|  | |||||||
							
								
								
									
										138
									
								
								bitcask.go
									
									
									
									
									
								
							
							
						
						
									
										138
									
								
								bitcask.go
									
									
									
									
									
								
							| @ -11,6 +11,7 @@ import ( | |||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
| 	"sort" | 	"sort" | ||||||
| 	"sync" | 	"sync" | ||||||
|  | 	"time" | ||||||
| 
 | 
 | ||||||
| 	art "github.com/plar/go-adaptive-radix-tree" | 	art "github.com/plar/go-adaptive-radix-tree" | ||||||
| 	"github.com/prologic/bitcask/flock" | 	"github.com/prologic/bitcask/flock" | ||||||
| @ -20,6 +21,8 @@ import ( | |||||||
| 	"github.com/prologic/bitcask/internal/data/codec" | 	"github.com/prologic/bitcask/internal/data/codec" | ||||||
| 	"github.com/prologic/bitcask/internal/index" | 	"github.com/prologic/bitcask/internal/index" | ||||||
| 	"github.com/prologic/bitcask/internal/metadata" | 	"github.com/prologic/bitcask/internal/metadata" | ||||||
|  | 	"github.com/prologic/bitcask/scripts/migrations" | ||||||
|  | 	log "github.com/sirupsen/logrus" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| const ( | const ( | ||||||
| @ -34,6 +37,10 @@ var ( | |||||||
| 	// maximum allowed key size (configured with WithMaxKeySize). | 	// maximum allowed key size (configured with WithMaxKeySize). | ||||||
| 	ErrKeyTooLarge = errors.New("error: key too large") | 	ErrKeyTooLarge = errors.New("error: key too large") | ||||||
| 
 | 
 | ||||||
|  | 	// ErrKeyExpired is the error returned when a key is queried which has | ||||||
|  | 	// already expired (due to ttl) | ||||||
|  | 	ErrKeyExpired = errors.New("error: key expired") | ||||||
|  | 
 | ||||||
| 	// ErrEmptyKey is the error returned for a value with an empty key. | 	// ErrEmptyKey is the error returned for a value with an empty key. | ||||||
| 	ErrEmptyKey = errors.New("error: empty key") | 	ErrEmptyKey = errors.New("error: empty key") | ||||||
| 
 | 
 | ||||||
| @ -49,6 +56,8 @@ var ( | |||||||
| 	// (typically opened by another process) | 	// (typically opened by another process) | ||||||
| 	ErrDatabaseLocked = errors.New("error: database locked") | 	ErrDatabaseLocked = errors.New("error: database locked") | ||||||
| 
 | 
 | ||||||
|  | 	ErrInvalidVersion = errors.New("error: invalid db version") | ||||||
|  | 
 | ||||||
| 	// ErrMergeInProgress is the error returned if merge is called when already a merge | 	// ErrMergeInProgress is the error returned if merge is called when already a merge | ||||||
| 	// is in progress | 	// is in progress | ||||||
| 	ErrMergeInProgress = errors.New("error: merge already in progress") | 	ErrMergeInProgress = errors.New("error: merge already in progress") | ||||||
| @ -139,42 +148,14 @@ func (b *Bitcask) Sync() error { | |||||||
| 	return b.curr.Sync() | 	return b.curr.Sync() | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Get fetches value for given key | // Get fetches value for a key | ||||||
| func (b *Bitcask) Get(key []byte) ([]byte, error) { | func (b *Bitcask) Get(key []byte) ([]byte, error) { | ||||||
| 	b.mu.RLock() | 	b.mu.RLock() | ||||||
| 	defer b.mu.RUnlock() | 	defer b.mu.RUnlock() | ||||||
| 
 | 	e, err := b.get(key) | ||||||
| 	return b.get(key) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // get retrieves the value of the given key. If the key is not found or an/I/O |  | ||||||
| // error occurs a null byte slice is returned along with the error. |  | ||||||
| func (b *Bitcask) get(key []byte) ([]byte, error) { |  | ||||||
| 	var df data.Datafile |  | ||||||
| 
 |  | ||||||
| 	value, found := b.trie.Search(key) |  | ||||||
| 	if !found { |  | ||||||
| 		return nil, ErrKeyNotFound |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	item := value.(internal.Item) |  | ||||||
| 
 |  | ||||||
| 	if item.FileID == b.curr.FileID() { |  | ||||||
| 		df = b.curr |  | ||||||
| 	} else { |  | ||||||
| 		df = b.datafiles[item.FileID] |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	e, err := df.ReadAt(item.Offset, item.Size) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 
 |  | ||||||
| 	checksum := crc32.ChecksumIEEE(e.Value) |  | ||||||
| 	if checksum != e.Checksum { |  | ||||||
| 		return nil, ErrChecksumFailed |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return e.Value, nil | 	return e.Value, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -187,7 +168,7 @@ func (b *Bitcask) Has(key []byte) bool { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Put stores the key and value in the database. | // Put stores the key and value in the database. | ||||||
| func (b *Bitcask) Put(key, value []byte) error { | func (b *Bitcask) Put(key, value []byte, options ...PutOptions) error { | ||||||
| 	if len(key) == 0 { | 	if len(key) == 0 { | ||||||
| 		return ErrEmptyKey | 		return ErrEmptyKey | ||||||
| 	} | 	} | ||||||
| @ -197,10 +178,16 @@ func (b *Bitcask) Put(key, value []byte) error { | |||||||
| 	if b.config.MaxValueSize > 0 && uint64(len(value)) > b.config.MaxValueSize { | 	if b.config.MaxValueSize > 0 && uint64(len(value)) > b.config.MaxValueSize { | ||||||
| 		return ErrValueTooLarge | 		return ErrValueTooLarge | ||||||
| 	} | 	} | ||||||
|  | 	var feature Feature | ||||||
|  | 	for _, opt := range options { | ||||||
|  | 		if err := opt(&feature); err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	b.mu.Lock() | 	b.mu.Lock() | ||||||
| 	defer b.mu.Unlock() | 	defer b.mu.Unlock() | ||||||
| 	offset, n, err := b.put(key, value) | 	offset, n, err := b.put(key, value, feature) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| @ -224,20 +211,24 @@ func (b *Bitcask) Put(key, value []byte) error { | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Delete deletes the named key. If the key doesn't exist or an I/O error | // Delete deletes the named key. | ||||||
| // occurs the error is returned. |  | ||||||
| func (b *Bitcask) Delete(key []byte) error { | func (b *Bitcask) Delete(key []byte) error { | ||||||
| 	b.mu.Lock() | 	b.mu.Lock() | ||||||
| 	_, _, err := b.put(key, []byte{}) | 	defer b.mu.Unlock() | ||||||
|  | 	return b.delete(key) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // delete deletes the named key. If the key doesn't exist or an I/O error | ||||||
|  | // occurs the error is returned. | ||||||
|  | func (b *Bitcask) delete(key []byte) error { | ||||||
|  | 	_, _, err := b.put(key, []byte{}, Feature{}) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		b.mu.Unlock() |  | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	if item, found := b.trie.Search(key); found { | 	if item, found := b.trie.Search(key); found { | ||||||
| 		b.metadata.ReclaimableSpace += item.(internal.Item).Size + codec.MetaInfoSize + int64(len(key)) | 		b.metadata.ReclaimableSpace += item.(internal.Item).Size + codec.MetaInfoSize + int64(len(key)) | ||||||
| 	} | 	} | ||||||
| 	b.trie.Delete(key) | 	b.trie.Delete(key) | ||||||
| 	b.mu.Unlock() |  | ||||||
| 
 | 
 | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| @ -248,7 +239,7 @@ func (b *Bitcask) DeleteAll() (err error) { | |||||||
| 	defer b.mu.RUnlock() | 	defer b.mu.RUnlock() | ||||||
| 
 | 
 | ||||||
| 	b.trie.ForEach(func(node art.Node) bool { | 	b.trie.ForEach(func(node art.Node) bool { | ||||||
| 		_, _, err = b.put(node.Key(), []byte{}) | 		_, _, err = b.put(node.Key(), []byte{}, Feature{}) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return false | 			return false | ||||||
| 		} | 		} | ||||||
| @ -320,8 +311,44 @@ func (b *Bitcask) Fold(f func(key []byte) error) (err error) { | |||||||
| 	return | 	return | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // get retrieves the value of the given key. If the key is not found or an/I/O | ||||||
|  | // error occurs a null byte slice is returned along with the error. | ||||||
|  | func (b *Bitcask) get(key []byte) (internal.Entry, error) { | ||||||
|  | 	var df data.Datafile | ||||||
|  | 
 | ||||||
|  | 	value, found := b.trie.Search(key) | ||||||
|  | 	if !found { | ||||||
|  | 		return internal.Entry{}, ErrKeyNotFound | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	item := value.(internal.Item) | ||||||
|  | 
 | ||||||
|  | 	if item.FileID == b.curr.FileID() { | ||||||
|  | 		df = b.curr | ||||||
|  | 	} else { | ||||||
|  | 		df = b.datafiles[item.FileID] | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	e, err := df.ReadAt(item.Offset, item.Size) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return internal.Entry{}, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if e.Expiry != nil && e.Expiry.Before(time.Now().UTC()) { | ||||||
|  | 		_ = b.delete(key) // we don't care if it doesnt succeed | ||||||
|  | 		return internal.Entry{}, ErrKeyExpired | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	checksum := crc32.ChecksumIEEE(e.Value) | ||||||
|  | 	if checksum != e.Checksum { | ||||||
|  | 		return internal.Entry{}, ErrChecksumFailed | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return e, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // put inserts a new (key, value). Both key and value are valid inputs. | // put inserts a new (key, value). Both key and value are valid inputs. | ||||||
| func (b *Bitcask) put(key, value []byte) (int64, int64, error) { | func (b *Bitcask) put(key, value []byte, feature Feature) (int64, int64, error) { | ||||||
| 	size := b.curr.Size() | 	size := b.curr.Size() | ||||||
| 	if size >= int64(b.config.MaxDatafileSize) { | 	if size >= int64(b.config.MaxDatafileSize) { | ||||||
| 		err := b.curr.Close() | 		err := b.curr.Close() | ||||||
| @ -350,7 +377,7 @@ func (b *Bitcask) put(key, value []byte) (int64, int64, error) { | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	e := internal.NewEntry(key, value) | 	e := internal.NewEntry(key, value, feature.Expiry) | ||||||
| 	return b.curr.Write(e) | 	return b.curr.Write(e) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -466,12 +493,17 @@ func (b *Bitcask) Merge() error { | |||||||
| 		if item.(internal.Item).FileID > filesToMerge[len(filesToMerge)-1] { | 		if item.(internal.Item).FileID > filesToMerge[len(filesToMerge)-1] { | ||||||
| 			return nil | 			return nil | ||||||
| 		} | 		} | ||||||
| 		value, err := b.get(key) | 		e, err := b.get(key) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
|  | 		// prepare entry options | ||||||
|  | 		var opts []PutOptions | ||||||
|  | 		if e.Expiry != nil { | ||||||
|  | 			opts = append(opts, WithExpiry(*(e.Expiry))) | ||||||
|  | 		} | ||||||
| 
 | 
 | ||||||
| 		if err := mdb.Put(key, value); err != nil { | 		if err := mdb.Put(key, e.Value, opts...); err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| @ -553,6 +585,10 @@ func Open(path string, options ...Option) (*Bitcask, error) { | |||||||
| 		cfg = newDefaultConfig() | 		cfg = newDefaultConfig() | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	if err := checkAndUpgrade(cfg, configPath); err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	for _, opt := range options { | 	for _, opt := range options { | ||||||
| 		if err := opt(cfg); err != nil { | 		if err := opt(cfg); err != nil { | ||||||
| 			return nil, err | 			return nil, err | ||||||
| @ -602,6 +638,24 @@ func Open(path string, options ...Option) (*Bitcask, error) { | |||||||
| 	return bitcask, nil | 	return bitcask, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // checkAndUpgrade checks if DB upgrade is required | ||||||
|  | // if yes, then applies version upgrade and saves updated config | ||||||
|  | func checkAndUpgrade(cfg *config.Config, configPath string) error { | ||||||
|  | 	if cfg.DBVersion == CurrentDBVersion { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 	if cfg.DBVersion > CurrentDBVersion { | ||||||
|  | 		return ErrInvalidVersion | ||||||
|  | 	} | ||||||
|  | 	// for v0 to v1 upgrade, we need to append 8 null bytes after each encoded entry in datafiles | ||||||
|  | 	if cfg.DBVersion == uint32(0) && CurrentDBVersion == uint32(1) { | ||||||
|  | 		log.Warn("upgrading db version, might take some time....") | ||||||
|  | 		cfg.DBVersion = CurrentDBVersion | ||||||
|  | 		return migrations.ApplyV0ToV1(filepath.Dir(configPath), cfg.MaxDatafileSize) | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // Backup copies db directory to given path | // Backup copies db directory to given path | ||||||
| // it creates path if it does not exist | // it creates path if it does not exist | ||||||
| func (b *Bitcask) Backup(path string) error { | func (b *Bitcask) Backup(path string) error { | ||||||
|  | |||||||
| @ -13,6 +13,7 @@ import ( | |||||||
| 	"strings" | 	"strings" | ||||||
| 	"sync" | 	"sync" | ||||||
| 	"testing" | 	"testing" | ||||||
|  | 	"time" | ||||||
| 
 | 
 | ||||||
| 	"github.com/stretchr/testify/assert" | 	"github.com/stretchr/testify/assert" | ||||||
| 	"github.com/stretchr/testify/require" | 	"github.com/stretchr/testify/require" | ||||||
| @ -70,7 +71,7 @@ func TestAll(t *testing.T) { | |||||||
| 	}) | 	}) | ||||||
| 
 | 
 | ||||||
| 	t.Run("Put", func(t *testing.T) { | 	t.Run("Put", func(t *testing.T) { | ||||||
| 		err = db.Put([]byte([]byte("foo")), []byte("bar")) | 		err = db.Put([]byte("foo"), []byte("bar")) | ||||||
| 		assert.NoError(err) | 		assert.NoError(err) | ||||||
| 	}) | 	}) | ||||||
| 
 | 
 | ||||||
| @ -84,6 +85,18 @@ func TestAll(t *testing.T) { | |||||||
| 		assert.Equal(1, db.Len()) | 		assert.Equal(1, db.Len()) | ||||||
| 	}) | 	}) | ||||||
| 
 | 
 | ||||||
|  | 	t.Run("PutWithExpiry", func(t *testing.T) { | ||||||
|  | 		err = db.Put([]byte("bar"), []byte("baz"), WithExpiry(time.Now())) | ||||||
|  | 		assert.NoError(err) | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | 	t.Run("GetExpiredKey", func(t *testing.T) { | ||||||
|  | 		time.Sleep(time.Millisecond) | ||||||
|  | 		_, err := db.Get([]byte("bar")) | ||||||
|  | 		assert.Error(err) | ||||||
|  | 		assert.Equal(ErrKeyExpired, err) | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
| 	t.Run("Has", func(t *testing.T) { | 	t.Run("Has", func(t *testing.T) { | ||||||
| 		assert.True(db.Has([]byte("foo"))) | 		assert.True(db.Has([]byte("foo"))) | ||||||
| 	}) | 	}) | ||||||
| @ -338,19 +351,19 @@ func TestMetadata(t *testing.T) { | |||||||
| 	}) | 	}) | ||||||
| 	t.Run("ReclaimableAfterRepeatedPut", func(t *testing.T) { | 	t.Run("ReclaimableAfterRepeatedPut", func(t *testing.T) { | ||||||
| 		assert.NoError(db.Put([]byte("hello"), []byte("world"))) | 		assert.NoError(db.Put([]byte("hello"), []byte("world"))) | ||||||
| 		assert.Equal(int64(26), db.Reclaimable()) | 		assert.Equal(int64(34), db.Reclaimable()) | ||||||
| 	}) | 	}) | ||||||
| 	t.Run("ReclaimableAfterDelete", func(t *testing.T) { | 	t.Run("ReclaimableAfterDelete", func(t *testing.T) { | ||||||
| 		assert.NoError(db.Delete([]byte("hello"))) | 		assert.NoError(db.Delete([]byte("hello"))) | ||||||
| 		assert.Equal(int64(73), db.Reclaimable()) | 		assert.Equal(int64(97), db.Reclaimable()) | ||||||
| 	}) | 	}) | ||||||
| 	t.Run("ReclaimableAfterNonExistingDelete", func(t *testing.T) { | 	t.Run("ReclaimableAfterNonExistingDelete", func(t *testing.T) { | ||||||
| 		assert.NoError(db.Delete([]byte("hello1"))) | 		assert.NoError(db.Delete([]byte("hello1"))) | ||||||
| 		assert.Equal(int64(73), db.Reclaimable()) | 		assert.Equal(int64(97), db.Reclaimable()) | ||||||
| 	}) | 	}) | ||||||
| 	t.Run("ReclaimableAfterDeleteAll", func(t *testing.T) { | 	t.Run("ReclaimableAfterDeleteAll", func(t *testing.T) { | ||||||
| 		assert.NoError(db.DeleteAll()) | 		assert.NoError(db.DeleteAll()) | ||||||
| 		assert.Equal(int64(158), db.Reclaimable()) | 		assert.Equal(int64(214), db.Reclaimable()) | ||||||
| 	}) | 	}) | ||||||
| 	t.Run("ReclaimableAfterMerge", func(t *testing.T) { | 	t.Run("ReclaimableAfterMerge", func(t *testing.T) { | ||||||
| 		assert.NoError(db.Merge()) | 		assert.NoError(db.Merge()) | ||||||
| @ -1072,7 +1085,7 @@ func TestGetErrors(t *testing.T) { | |||||||
| 
 | 
 | ||||||
| 		mockDatafile := new(mocks.Datafile) | 		mockDatafile := new(mocks.Datafile) | ||||||
| 		mockDatafile.On("FileID").Return(0) | 		mockDatafile.On("FileID").Return(0) | ||||||
| 		mockDatafile.On("ReadAt", int64(0), int64(22)).Return( | 		mockDatafile.On("ReadAt", int64(0), int64(30)).Return( | ||||||
| 			internal.Entry{}, | 			internal.Entry{}, | ||||||
| 			ErrMockError, | 			ErrMockError, | ||||||
| 		) | 		) | ||||||
| @ -1088,7 +1101,7 @@ func TestGetErrors(t *testing.T) { | |||||||
| 		assert.NoError(err) | 		assert.NoError(err) | ||||||
| 		defer os.RemoveAll(testdir) | 		defer os.RemoveAll(testdir) | ||||||
| 
 | 
 | ||||||
| 		db, err := Open(testdir, WithMaxDatafileSize(32)) | 		db, err := Open(testdir, WithMaxDatafileSize(40)) | ||||||
| 		assert.NoError(err) | 		assert.NoError(err) | ||||||
| 
 | 
 | ||||||
| 		err = db.Put([]byte("foo"), []byte("bar")) | 		err = db.Put([]byte("foo"), []byte("bar")) | ||||||
| @ -1096,7 +1109,7 @@ func TestGetErrors(t *testing.T) { | |||||||
| 
 | 
 | ||||||
| 		mockDatafile := new(mocks.Datafile) | 		mockDatafile := new(mocks.Datafile) | ||||||
| 		mockDatafile.On("FileID").Return(0) | 		mockDatafile.On("FileID").Return(0) | ||||||
| 		mockDatafile.On("ReadAt", int64(0), int64(22)).Return( | 		mockDatafile.On("ReadAt", int64(0), int64(30)).Return( | ||||||
| 			internal.Entry{ | 			internal.Entry{ | ||||||
| 				Checksum: 0x0, | 				Checksum: 0x0, | ||||||
| 				Key:      []byte("foo"), | 				Key:      []byte("foo"), | ||||||
| @ -1377,7 +1390,7 @@ func TestMergeErrors(t *testing.T) { | |||||||
| 
 | 
 | ||||||
| 		mockDatafile := new(mocks.Datafile) | 		mockDatafile := new(mocks.Datafile) | ||||||
| 		mockDatafile.On("Close").Return(nil) | 		mockDatafile.On("Close").Return(nil) | ||||||
| 		mockDatafile.On("ReadAt", int64(0), int64(22)).Return( | 		mockDatafile.On("ReadAt", int64(0), int64(30)).Return( | ||||||
| 			internal.Entry{}, | 			internal.Entry{}, | ||||||
| 			ErrMockError, | 			ErrMockError, | ||||||
| 		) | 		) | ||||||
|  | |||||||
| @ -1,11 +1,13 @@ | |||||||
| package main | package main | ||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
|  | 	"encoding/binary" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"os" | 	"os" | ||||||
| 	"os/signal" | 	"os/signal" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"syscall" | 	"syscall" | ||||||
|  | 	"time" | ||||||
| 
 | 
 | ||||||
| 	log "github.com/sirupsen/logrus" | 	log "github.com/sirupsen/logrus" | ||||||
| 	"github.com/tidwall/redcon" | 	"github.com/tidwall/redcon" | ||||||
| @ -32,12 +34,18 @@ func newServer(bind, dbpath string) (*server, error) { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (s *server) handleSet(cmd redcon.Command, conn redcon.Conn) { | func (s *server) handleSet(cmd redcon.Command, conn redcon.Conn) { | ||||||
| 	if len(cmd.Args) != 3 { | 	if len(cmd.Args) != 3 && len(cmd.Args) != 4 { | ||||||
| 		conn.WriteError("ERR wrong number of arguments for '" + string(cmd.Args[0]) + "' command") | 		conn.WriteError("ERR wrong number of arguments for '" + string(cmd.Args[0]) + "' command") | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	key := cmd.Args[1] | 	key := cmd.Args[1] | ||||||
| 	value := cmd.Args[2] | 	value := cmd.Args[2] | ||||||
|  | 	var opts []bitcask.PutOptions | ||||||
|  | 	if len(cmd.Args) == 4 { | ||||||
|  | 		ttl, _ := binary.Varint(cmd.Args[3]) | ||||||
|  | 		e := time.Now().UTC().Add(time.Duration(ttl)*time.Millisecond) | ||||||
|  | 		opts = append(opts, bitcask.WithExpiry(e)) | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| 	err := s.db.Lock() | 	err := s.db.Lock() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @ -46,7 +54,7 @@ func (s *server) handleSet(cmd redcon.Command, conn redcon.Conn) { | |||||||
| 	} | 	} | ||||||
| 	defer s.db.Unlock() | 	defer s.db.Unlock() | ||||||
| 
 | 
 | ||||||
| 	if err := s.db.Put(key, value); err != nil { | 	if err := s.db.Put(key, value, opts...); err != nil { | ||||||
| 		conn.WriteString(fmt.Sprintf("ERR: %s", err)) | 		conn.WriteString(fmt.Sprintf("ERR: %s", err)) | ||||||
| 	} else { | 	} else { | ||||||
| 		conn.WriteString("OK") | 		conn.WriteString("OK") | ||||||
|  | |||||||
| @ -13,6 +13,7 @@ type Config struct { | |||||||
| 	MaxValueSize            uint64 `json:"max_value_size"` | 	MaxValueSize            uint64 `json:"max_value_size"` | ||||||
| 	Sync                    bool   `json:"sync"` | 	Sync                    bool   `json:"sync"` | ||||||
| 	AutoRecovery            bool   `json:"autorecovery"` | 	AutoRecovery            bool   `json:"autorecovery"` | ||||||
|  | 	DBVersion               uint32 `json:"db_version"` | ||||||
| 	DirFileModeBeforeUmask  os.FileMode | 	DirFileModeBeforeUmask  os.FileMode | ||||||
| 	FileFileModeBeforeUmask os.FileMode | 	FileFileModeBeforeUmask os.FileMode | ||||||
| } | } | ||||||
|  | |||||||
| @ -3,6 +3,7 @@ package codec | |||||||
| import ( | import ( | ||||||
| 	"encoding/binary" | 	"encoding/binary" | ||||||
| 	"io" | 	"io" | ||||||
|  | 	"time" | ||||||
| 
 | 
 | ||||||
| 	"github.com/pkg/errors" | 	"github.com/pkg/errors" | ||||||
| 	"github.com/prologic/bitcask/internal" | 	"github.com/prologic/bitcask/internal" | ||||||
| @ -49,13 +50,13 @@ func (d *Decoder) Decode(v *internal.Entry) (int64, error) { | |||||||
| 		return 0, err | 		return 0, err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	buf := make([]byte, uint64(actualKeySize)+actualValueSize+checksumSize) | 	buf := make([]byte, uint64(actualKeySize)+actualValueSize+checksumSize+ttlSize) | ||||||
| 	if _, err = io.ReadFull(d.r, buf); err != nil { | 	if _, err = io.ReadFull(d.r, buf); err != nil { | ||||||
| 		return 0, errTruncatedData | 		return 0, errTruncatedData | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	decodeWithoutPrefix(buf, actualKeySize, v) | 	decodeWithoutPrefix(buf, actualKeySize, v) | ||||||
| 	return int64(keySize + valueSize + uint64(actualKeySize) + actualValueSize + checksumSize), nil | 	return int64(keySize + valueSize + uint64(actualKeySize) + actualValueSize + checksumSize + ttlSize), nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // DecodeEntry decodes a serialized entry | // DecodeEntry decodes a serialized entry | ||||||
| @ -84,8 +85,18 @@ func getKeyValueSizes(buf []byte, maxKeySize uint32, maxValueSize uint64) (uint3 | |||||||
| 
 | 
 | ||||||
| func decodeWithoutPrefix(buf []byte, valueOffset uint32, v *internal.Entry) { | func decodeWithoutPrefix(buf []byte, valueOffset uint32, v *internal.Entry) { | ||||||
| 	v.Key = buf[:valueOffset] | 	v.Key = buf[:valueOffset] | ||||||
| 	v.Value = buf[valueOffset : len(buf)-checksumSize] | 	v.Value = buf[valueOffset : len(buf)-checksumSize-ttlSize] | ||||||
| 	v.Checksum = binary.BigEndian.Uint32(buf[len(buf)-checksumSize:]) | 	v.Checksum = binary.BigEndian.Uint32(buf[len(buf)-checksumSize-ttlSize : len(buf)-ttlSize]) | ||||||
|  | 	v.Expiry = getKeyExpiry(buf) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func getKeyExpiry(buf []byte) *time.Time { | ||||||
|  | 	expiry := binary.BigEndian.Uint64(buf[len(buf)-ttlSize:]) | ||||||
|  | 	if expiry == uint64(0) { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 	t := time.Unix(int64(expiry), 0).UTC() | ||||||
|  | 	return &t | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // IsCorruptedData indicates if the error correspondes to possible data corruption | // IsCorruptedData indicates if the error correspondes to possible data corruption | ||||||
|  | |||||||
| @ -5,6 +5,7 @@ import ( | |||||||
| 	"encoding/binary" | 	"encoding/binary" | ||||||
| 	"io" | 	"io" | ||||||
| 	"testing" | 	"testing" | ||||||
|  | 	"time" | ||||||
| 
 | 
 | ||||||
| 	"github.com/prologic/bitcask/internal" | 	"github.com/prologic/bitcask/internal" | ||||||
| 	"github.com/stretchr/testify/assert" | 	"github.com/stretchr/testify/assert" | ||||||
| @ -107,3 +108,23 @@ func TestTruncatedData(t *testing.T) { | |||||||
| 		}) | 		}) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func TestDecodeWithoutPrefix(t *testing.T) { | ||||||
|  | 	assert := assert.New(t) | ||||||
|  | 	e := internal.Entry{} | ||||||
|  | 	buf := []byte{0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 7, 109, 121, 107, 101, 121, 109, 121, 118, 97, 108, 117, 101, 0, 6, 81, 189, 0, 0, 0, 0, 95, 117, 28, 0} | ||||||
|  | 	valueOffset := uint32(5) | ||||||
|  | 	mockTime := time.Date(2020, 10, 1, 0, 0, 0, 0, time.UTC) | ||||||
|  | 	expectedEntry := internal.Entry{ | ||||||
|  | 		Key:      []byte("mykey"), | ||||||
|  | 		Value:    []byte("myvalue"), | ||||||
|  | 		Checksum: 414141, | ||||||
|  | 		Expiry:   &mockTime, | ||||||
|  | 	} | ||||||
|  | 	decodeWithoutPrefix(buf[keySize+valueSize:], valueOffset, &e) | ||||||
|  | 	assert.Equal(expectedEntry.Key, e.Key) | ||||||
|  | 	assert.Equal(expectedEntry.Value, e.Value) | ||||||
|  | 	assert.Equal(expectedEntry.Checksum, e.Checksum) | ||||||
|  | 	assert.Equal(expectedEntry.Offset, e.Offset) | ||||||
|  | 	assert.Equal(*expectedEntry.Expiry, *e.Expiry) | ||||||
|  | } | ||||||
|  | |||||||
| @ -13,7 +13,8 @@ const ( | |||||||
| 	keySize      = 4 | 	keySize      = 4 | ||||||
| 	valueSize    = 8 | 	valueSize    = 8 | ||||||
| 	checksumSize = 4 | 	checksumSize = 4 | ||||||
| 	MetaInfoSize = keySize + valueSize + checksumSize | 	ttlSize      = 8 | ||||||
|  | 	MetaInfoSize = keySize + valueSize + checksumSize + ttlSize | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // NewEncoder creates a streaming Entry encoder. | // NewEncoder creates a streaming Entry encoder. | ||||||
| @ -50,9 +51,19 @@ func (e *Encoder) Encode(msg internal.Entry) (int64, error) { | |||||||
| 		return 0, errors.Wrap(err, "failed writing checksum data") | 		return 0, errors.Wrap(err, "failed writing checksum data") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	bufTTL := bufKeyValue[:ttlSize] | ||||||
|  | 	if msg.Expiry == nil { | ||||||
|  | 		binary.BigEndian.PutUint64(bufTTL, uint64(0)) | ||||||
|  | 	} else { | ||||||
|  | 		binary.BigEndian.PutUint64(bufTTL, uint64(msg.Expiry.Unix())) | ||||||
|  | 	} | ||||||
|  | 	if _, err := e.w.Write(bufTTL); err != nil { | ||||||
|  | 		return 0, errors.Wrap(err, "failed writing ttl data") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	if err := e.w.Flush(); err != nil { | 	if err := e.w.Flush(); err != nil { | ||||||
| 		return 0, errors.Wrap(err, "failed flushing data") | 		return 0, errors.Wrap(err, "failed flushing data") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return int64(keySize + valueSize + len(msg.Key) + len(msg.Value) + checksumSize), nil | 	return int64(keySize + valueSize + len(msg.Key) + len(msg.Value) + checksumSize + ttlSize), nil | ||||||
| } | } | ||||||
|  | |||||||
| @ -4,6 +4,7 @@ import ( | |||||||
| 	"bytes" | 	"bytes" | ||||||
| 	"encoding/hex" | 	"encoding/hex" | ||||||
| 	"testing" | 	"testing" | ||||||
|  | 	"time" | ||||||
| 
 | 
 | ||||||
| 	"github.com/prologic/bitcask/internal" | 	"github.com/prologic/bitcask/internal" | ||||||
| 	"github.com/stretchr/testify/assert" | 	"github.com/stretchr/testify/assert" | ||||||
| @ -14,15 +15,17 @@ func TestEncode(t *testing.T) { | |||||||
| 	assert := assert.New(t) | 	assert := assert.New(t) | ||||||
| 
 | 
 | ||||||
| 	var buf bytes.Buffer | 	var buf bytes.Buffer | ||||||
|  | 	mockTime := time.Date(2020, 10, 1, 0, 0, 0, 0, time.UTC) | ||||||
| 	encoder := NewEncoder(&buf) | 	encoder := NewEncoder(&buf) | ||||||
| 	_, err := encoder.Encode(internal.Entry{ | 	_, err := encoder.Encode(internal.Entry{ | ||||||
| 		Key:      []byte("mykey"), | 		Key:      []byte("mykey"), | ||||||
| 		Value:    []byte("myvalue"), | 		Value:    []byte("myvalue"), | ||||||
| 		Checksum: 414141, | 		Checksum: 414141, | ||||||
| 		Offset:   424242, | 		Offset:   424242, | ||||||
|  | 		Expiry:   &mockTime, | ||||||
| 	}) | 	}) | ||||||
| 
 | 
 | ||||||
| 	expectedHex := "0000000500000000000000076d796b65796d7976616c7565000651bd" | 	expectedHex := "0000000500000000000000076d796b65796d7976616c7565000651bd000000005f751c00" | ||||||
| 	if assert.NoError(err) { | 	if assert.NoError(err) { | ||||||
| 		assert.Equal(expectedHex, hex.EncodeToString(buf.Bytes())) | 		assert.Equal(expectedHex, hex.EncodeToString(buf.Bytes())) | ||||||
| 	} | 	} | ||||||
|  | |||||||
| @ -2,6 +2,7 @@ package internal | |||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"hash/crc32" | 	"hash/crc32" | ||||||
|  | 	"time" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // Entry represents a key/value in the database | // Entry represents a key/value in the database | ||||||
| @ -10,15 +11,17 @@ type Entry struct { | |||||||
| 	Key      []byte | 	Key      []byte | ||||||
| 	Offset   int64 | 	Offset   int64 | ||||||
| 	Value    []byte | 	Value    []byte | ||||||
|  | 	Expiry   *time.Time | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // NewEntry creates a new `Entry` with the given `key` and `value` | // NewEntry creates a new `Entry` with the given `key` and `value` | ||||||
| func NewEntry(key, value []byte) Entry { | func NewEntry(key, value []byte, expiry *time.Time) Entry { | ||||||
| 	checksum := crc32.ChecksumIEEE(value) | 	checksum := crc32.ChecksumIEEE(value) | ||||||
| 
 | 
 | ||||||
| 	return Entry{ | 	return Entry{ | ||||||
| 		Checksum: checksum, | 		Checksum: checksum, | ||||||
| 		Key:      key, | 		Key:      key, | ||||||
| 		Value:    value, | 		Value:    value, | ||||||
|  | 		Expiry:   expiry, | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										17
									
								
								options.go
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								options.go
									
									
									
									
									
								
							| @ -2,6 +2,7 @@ package bitcask | |||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"os" | 	"os" | ||||||
|  | 	"time" | ||||||
| 
 | 
 | ||||||
| 	"github.com/prologic/bitcask/internal/config" | 	"github.com/prologic/bitcask/internal/config" | ||||||
| ) | ) | ||||||
| @ -26,6 +27,8 @@ const ( | |||||||
| 	DefaultSync = false | 	DefaultSync = false | ||||||
| 
 | 
 | ||||||
| 	// DefaultAutoRecovery is the default auto-recovery action. | 	// DefaultAutoRecovery is the default auto-recovery action. | ||||||
|  | 
 | ||||||
|  | 	CurrentDBVersion = uint32(1) | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // Option is a function that takes a config struct and modifies it | // Option is a function that takes a config struct and modifies it | ||||||
| @ -111,5 +114,19 @@ func newDefaultConfig() *config.Config { | |||||||
| 		Sync:                    DefaultSync, | 		Sync:                    DefaultSync, | ||||||
| 		DirFileModeBeforeUmask:  DefaultDirFileModeBeforeUmask, | 		DirFileModeBeforeUmask:  DefaultDirFileModeBeforeUmask, | ||||||
| 		FileFileModeBeforeUmask: DefaultFileFileModeBeforeUmask, | 		FileFileModeBeforeUmask: DefaultFileFileModeBeforeUmask, | ||||||
|  | 		DBVersion:               CurrentDBVersion, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type Feature struct { | ||||||
|  | 	Expiry *time.Time | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type PutOptions func(*Feature) error | ||||||
|  | 
 | ||||||
|  | func WithExpiry(expiry time.Time) PutOptions { | ||||||
|  | 	return func(f *Feature) error { | ||||||
|  | 		f.Expiry = &expiry | ||||||
|  | 		return nil | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										159
									
								
								scripts/migrations/v0_to_v1.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										159
									
								
								scripts/migrations/v0_to_v1.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,159 @@ | |||||||
|  | package migrations | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"encoding/binary" | ||||||
|  | 	"fmt" | ||||||
|  | 	"io" | ||||||
|  | 	"io/ioutil" | ||||||
|  | 	"os" | ||||||
|  | 	"path" | ||||||
|  | 	"path/filepath" | ||||||
|  | 
 | ||||||
|  | 	"github.com/prologic/bitcask/internal" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
|  | 	keySize                 = 4 | ||||||
|  | 	valueSize               = 8 | ||||||
|  | 	checksumSize            = 4 | ||||||
|  | 	ttlSize                 = 8 | ||||||
|  | 	defaultDatafileFilename = "%09d.data" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func ApplyV0ToV1(dir string, maxDatafileSize int) error { | ||||||
|  | 	temp, err := prepare(dir) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	defer os.RemoveAll(temp) | ||||||
|  | 	err = apply(dir, temp, maxDatafileSize) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	return cleanup(dir, temp) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func prepare(dir string) (string, error) { | ||||||
|  | 	return ioutil.TempDir(dir, "migration") | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func apply(dir, temp string, maxDatafileSize int) error { | ||||||
|  | 	datafilesPath, err := internal.GetDatafiles(dir) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	var id, newOffset int | ||||||
|  | 	datafile, err := getNewDatafile(temp, id) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	id++ | ||||||
|  | 	for _, p := range datafilesPath { | ||||||
|  | 		df, err := os.Open(p) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 		var off int64 | ||||||
|  | 		for { | ||||||
|  | 			entry, err := getSingleEntry(df, off) | ||||||
|  | 			if err == io.EOF { | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 			if err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 			if newOffset+len(entry) > maxDatafileSize { | ||||||
|  | 				err = datafile.Sync() | ||||||
|  | 				if err != nil { | ||||||
|  | 					return err | ||||||
|  | 				} | ||||||
|  | 				datafile, err = getNewDatafile(temp, id) | ||||||
|  | 				if err != nil { | ||||||
|  | 					return err | ||||||
|  | 				} | ||||||
|  | 				id++ | ||||||
|  | 				newOffset = 0 | ||||||
|  | 			} | ||||||
|  | 			newEntry := make([]byte, len(entry)+ttlSize) | ||||||
|  | 			copy(newEntry[:len(entry)], entry) | ||||||
|  | 			n, err := datafile.Write(newEntry) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 			newOffset += n | ||||||
|  | 			off += int64(len(entry)) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return datafile.Sync() | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func cleanup(dir, temp string) error { | ||||||
|  | 	files, err := ioutil.ReadDir(dir) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	for _, file := range files { | ||||||
|  | 		if !file.IsDir() { | ||||||
|  | 			err := os.RemoveAll(path.Join([]string{dir, file.Name()}...)) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	files, err = ioutil.ReadDir(temp) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	for _, file := range files { | ||||||
|  | 		err := os.Rename( | ||||||
|  | 			path.Join([]string{temp, file.Name()}...), | ||||||
|  | 			path.Join([]string{dir, file.Name()}...), | ||||||
|  | 		) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func getNewDatafile(path string, id int) (*os.File, error) { | ||||||
|  | 	fn := filepath.Join(path, fmt.Sprintf(defaultDatafileFilename, id)) | ||||||
|  | 	return os.OpenFile(fn, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0640) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func getSingleEntry(f *os.File, offset int64) ([]byte, error) { | ||||||
|  | 	prefixBuf, err := readPrefix(f, offset) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	actualKeySize, actualValueSize := getKeyValueSize(prefixBuf) | ||||||
|  | 	entryBuf, err := read(f, uint64(actualKeySize)+actualValueSize+checksumSize, offset+keySize+valueSize) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return append(prefixBuf, entryBuf...), nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func readPrefix(f *os.File, offset int64) ([]byte, error) { | ||||||
|  | 	prefixBuf := make([]byte, keySize+valueSize) | ||||||
|  | 	_, err := f.ReadAt(prefixBuf, offset) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return prefixBuf, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func read(f *os.File, bufSize uint64, offset int64) ([]byte, error) { | ||||||
|  | 	buf := make([]byte, bufSize) | ||||||
|  | 	_, err := f.ReadAt(buf, offset) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return buf, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func getKeyValueSize(buf []byte) (uint32, uint64) { | ||||||
|  | 	actualKeySize := binary.BigEndian.Uint32(buf[:keySize]) | ||||||
|  | 	actualValueSize := binary.BigEndian.Uint64(buf[keySize:]) | ||||||
|  | 	return actualKeySize, actualValueSize | ||||||
|  | } | ||||||
							
								
								
									
										58
									
								
								scripts/migrations/v0_to_v1_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								scripts/migrations/v0_to_v1_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,58 @@ | |||||||
|  | package migrations | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"encoding/binary" | ||||||
|  | 	"encoding/hex" | ||||||
|  | 	"io" | ||||||
|  | 	"io/ioutil" | ||||||
|  | 	"os" | ||||||
|  | 	"path/filepath" | ||||||
|  | 	"testing" | ||||||
|  | 
 | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func Test_ApplyV0ToV1(t *testing.T) { | ||||||
|  | 	assert := assert.New(t) | ||||||
|  | 	testdir, err := ioutil.TempDir("/tmp", "bitcask") | ||||||
|  | 	assert.NoError(err) | ||||||
|  | 	defer os.RemoveAll(testdir) | ||||||
|  | 	w0, err := os.OpenFile(filepath.Join(testdir, "000000000.data"), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0640) | ||||||
|  | 	assert.NoError(err) | ||||||
|  | 	w1, err := os.OpenFile(filepath.Join(testdir, "000000001.data"), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0640) | ||||||
|  | 	assert.NoError(err) | ||||||
|  | 	w2, err := os.OpenFile(filepath.Join(testdir, "000000002.data"), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0640) | ||||||
|  | 	assert.NoError(err) | ||||||
|  | 	defer w0.Close() | ||||||
|  | 	defer w1.Close() | ||||||
|  | 	defer w2.Close() | ||||||
|  | 	buf := make([]byte, 104) | ||||||
|  | 	binary.BigEndian.PutUint32(buf[:4], 5) | ||||||
|  | 	binary.BigEndian.PutUint64(buf[4:12], 7) | ||||||
|  | 	copy(buf[12:28], "mykeymyvalue0AAA") | ||||||
|  | 	binary.BigEndian.PutUint32(buf[28:32], 3) | ||||||
|  | 	binary.BigEndian.PutUint64(buf[32:40], 5) | ||||||
|  | 	copy(buf[40:52], "keyvalue0BBB") | ||||||
|  | 	_, err = w0.Write(buf[:52]) | ||||||
|  | 	assert.NoError(err) | ||||||
|  | 	_, err = w1.Write(buf[:52]) | ||||||
|  | 	assert.NoError(err) | ||||||
|  | 	_, err = w2.Write(buf[:52]) | ||||||
|  | 	assert.NoError(err) | ||||||
|  | 	err = ApplyV0ToV1(testdir, 104) | ||||||
|  | 	assert.NoError(err) | ||||||
|  | 	r0, err := os.Open(filepath.Join(testdir, "000000000.data")) | ||||||
|  | 	assert.NoError(err) | ||||||
|  | 	defer r0.Close() | ||||||
|  | 	n, err := io.ReadFull(r0, buf) | ||||||
|  | 	assert.NoError(err) | ||||||
|  | 	assert.Equal(104, n) | ||||||
|  | 	assert.Equal("0000000500000000000000076d796b65796d7976616c75653041414100000000000000000000000300000000000000056b657976616c75653042424200000000000000000000000500000000000000076d796b65796d7976616c7565304141410000000000000000", hex.EncodeToString(buf)) | ||||||
|  | 	r1, err := os.Open(filepath.Join(testdir, "000000001.data")) | ||||||
|  | 	assert.NoError(err) | ||||||
|  | 	defer r1.Close() | ||||||
|  | 	n, err = io.ReadFull(r1, buf[:100]) | ||||||
|  | 	assert.NoError(err) | ||||||
|  | 	assert.Equal(100, n) | ||||||
|  | 	assert.Equal("0000000300000000000000056b657976616c75653042424200000000000000000000000500000000000000076d796b65796d7976616c75653041414100000000000000000000000300000000000000056b657976616c7565304242420000000000000000", hex.EncodeToString(buf[:100])) | ||||||
|  | } | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user