ca84ba12ac
This PR modifies the server and client agents to use `go.etc.io/bbolt` as the implementation for their state stores.
425 lines
10 KiB
Go
425 lines
10 KiB
Go
// Package boltdd contains a wrapper around BBoltDB to deduplicate writes and encode
|
|
// values using mgspack. (dd stands for de-duplicate)
|
|
package boltdd
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os"
|
|
"sync"
|
|
|
|
"github.com/hashicorp/go-msgpack/codec"
|
|
"github.com/hashicorp/nomad/nomad/structs"
|
|
"go.etcd.io/bbolt"
|
|
"golang.org/x/crypto/blake2b"
|
|
)
|
|
|
|
// ErrNotFound is returned when a key is not found.
|
|
type ErrNotFound struct {
|
|
name string
|
|
}
|
|
|
|
func (e *ErrNotFound) Error() string {
|
|
return fmt.Sprintf("key not found: %s", e.name)
|
|
}
|
|
|
|
// NotFound returns a new error for a key that was not found.
|
|
func NotFound(name string) error {
|
|
return &ErrNotFound{name}
|
|
}
|
|
|
|
// IsErrNotFound returns true if the error is an ErrNotFound error.
|
|
func IsErrNotFound(e error) bool {
|
|
if e == nil {
|
|
return false
|
|
}
|
|
_, ok := e.(*ErrNotFound)
|
|
return ok
|
|
}
|
|
|
|
// DB wraps an underlying bolt.DB to create write de-duplicating buckets and
|
|
// msgpack encoded values.
|
|
type DB struct {
|
|
rootBuckets map[string]*bucketMeta
|
|
rootBucketsLock sync.Mutex
|
|
|
|
boltDB *bbolt.DB
|
|
}
|
|
|
|
// Open a bolt.DB and wrap it in a write-de-duplicating msgpack-encoding
|
|
// implementation.
|
|
func Open(path string, mode os.FileMode, options *bbolt.Options) (*DB, error) {
|
|
bdb, err := bbolt.Open(path, mode, options)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return New(bdb), nil
|
|
}
|
|
|
|
// New de-duplicating wrapper for the given bboltdb.
|
|
func New(bdb *bbolt.DB) *DB {
|
|
return &DB{
|
|
rootBuckets: make(map[string]*bucketMeta),
|
|
boltDB: bdb,
|
|
}
|
|
}
|
|
|
|
func (db *DB) bucket(btx *bbolt.Tx, name []byte) *Bucket {
|
|
bb := btx.Bucket(name)
|
|
if bb == nil {
|
|
return nil
|
|
}
|
|
|
|
db.rootBucketsLock.Lock()
|
|
defer db.rootBucketsLock.Unlock()
|
|
|
|
if db.isClosed() {
|
|
return nil
|
|
}
|
|
|
|
b, ok := db.rootBuckets[string(name)]
|
|
if !ok {
|
|
b = newBucketMeta()
|
|
db.rootBuckets[string(name)] = b
|
|
}
|
|
|
|
return newBucket(b, bb)
|
|
}
|
|
|
|
func (db *DB) createBucket(btx *bbolt.Tx, name []byte) (*Bucket, error) {
|
|
bb, err := btx.CreateBucket(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
db.rootBucketsLock.Lock()
|
|
defer db.rootBucketsLock.Unlock()
|
|
|
|
// While creating a bucket on a closed db would error, we must recheck
|
|
// after acquiring the lock to avoid races.
|
|
if db.isClosed() {
|
|
return nil, bbolt.ErrDatabaseNotOpen
|
|
}
|
|
|
|
// Always create a new Bucket since CreateBucket above fails if the
|
|
// bucket already exists.
|
|
b := newBucketMeta()
|
|
db.rootBuckets[string(name)] = b
|
|
|
|
return newBucket(b, bb), nil
|
|
}
|
|
|
|
func (db *DB) createBucketIfNotExists(btx *bbolt.Tx, name []byte) (*Bucket, error) {
|
|
bb, err := btx.CreateBucketIfNotExists(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
db.rootBucketsLock.Lock()
|
|
defer db.rootBucketsLock.Unlock()
|
|
|
|
// While creating a bucket on a closed db would error, we must recheck
|
|
// after acquiring the lock to avoid races.
|
|
if db.isClosed() {
|
|
return nil, bbolt.ErrDatabaseNotOpen
|
|
}
|
|
|
|
b, ok := db.rootBuckets[string(name)]
|
|
if !ok {
|
|
b = newBucketMeta()
|
|
db.rootBuckets[string(name)] = b
|
|
}
|
|
|
|
return newBucket(b, bb), nil
|
|
}
|
|
|
|
func (db *DB) Update(fn func(*Tx) error) error {
|
|
return db.boltDB.Update(func(btx *bbolt.Tx) error {
|
|
tx := newTx(db, btx)
|
|
return fn(tx)
|
|
})
|
|
}
|
|
|
|
func (db *DB) Batch(fn func(*Tx) error) error {
|
|
return db.boltDB.Batch(func(btx *bbolt.Tx) error {
|
|
tx := newTx(db, btx)
|
|
return fn(tx)
|
|
})
|
|
}
|
|
|
|
func (db *DB) View(fn func(*Tx) error) error {
|
|
return db.boltDB.View(func(btx *bbolt.Tx) error {
|
|
tx := newTx(db, btx)
|
|
return fn(tx)
|
|
})
|
|
}
|
|
|
|
// isClosed returns true if the database is closed and must be called while
|
|
// db.rootBucketsLock is acquired.
|
|
func (db *DB) isClosed() bool {
|
|
return db.rootBuckets == nil
|
|
}
|
|
|
|
// Close closes the underlying bolt.DB and clears all bucket hashes. DB is
|
|
// unusable after closing.
|
|
func (db *DB) Close() error {
|
|
db.rootBucketsLock.Lock()
|
|
db.rootBuckets = nil
|
|
db.rootBucketsLock.Unlock()
|
|
return db.boltDB.Close()
|
|
}
|
|
|
|
// BoltDB returns the underlying bolt.DB.
|
|
func (db *DB) BoltDB() *bbolt.DB {
|
|
return db.boltDB
|
|
}
|
|
|
|
type Tx struct {
|
|
db *DB
|
|
btx *bbolt.Tx
|
|
}
|
|
|
|
func newTx(db *DB, btx *bbolt.Tx) *Tx {
|
|
return &Tx{
|
|
db: db,
|
|
btx: btx,
|
|
}
|
|
}
|
|
|
|
// Bucket returns a root bucket or nil if it doesn't exist.
|
|
func (tx *Tx) Bucket(name []byte) *Bucket {
|
|
return tx.db.bucket(tx.btx, name)
|
|
}
|
|
|
|
func (tx *Tx) CreateBucket(name []byte) (*Bucket, error) {
|
|
return tx.db.createBucket(tx.btx, name)
|
|
}
|
|
|
|
// CreateBucketIfNotExists returns a root bucket or creates a new one if it
|
|
// doesn't already exist.
|
|
func (tx *Tx) CreateBucketIfNotExists(name []byte) (*Bucket, error) {
|
|
return tx.db.createBucketIfNotExists(tx.btx, name)
|
|
}
|
|
|
|
// Writable wraps boltdb Tx.Writable.
|
|
func (tx *Tx) Writable() bool {
|
|
return tx.btx.Writable()
|
|
}
|
|
|
|
// BoltTx returns the underlying bolt.Tx.
|
|
func (tx *Tx) BoltTx() *bbolt.Tx {
|
|
return tx.btx
|
|
}
|
|
|
|
// bucketMeta persists metadata -- such as key hashes and child buckets --
|
|
// about boltdb Buckets across transactions.
|
|
type bucketMeta struct {
|
|
// hashes holds all of the value hashes for keys in this bucket
|
|
hashes map[string][]byte
|
|
hashesLock sync.Mutex
|
|
|
|
// buckets holds all of the child buckets
|
|
buckets map[string]*bucketMeta
|
|
bucketsLock sync.Mutex
|
|
}
|
|
|
|
func newBucketMeta() *bucketMeta {
|
|
return &bucketMeta{
|
|
hashes: make(map[string][]byte),
|
|
buckets: make(map[string]*bucketMeta),
|
|
}
|
|
}
|
|
|
|
// getHash of last value written to a key or nil if no hash exists.
|
|
func (bm *bucketMeta) getHash(hashKey string) []byte {
|
|
bm.hashesLock.Lock()
|
|
lastHash := bm.hashes[hashKey]
|
|
bm.hashesLock.Unlock()
|
|
return lastHash
|
|
}
|
|
|
|
// setHash of last value written to key.
|
|
func (bm *bucketMeta) setHash(hashKey string, hashVal []byte) {
|
|
bm.hashesLock.Lock()
|
|
bm.hashes[hashKey] = hashVal
|
|
bm.hashesLock.Unlock()
|
|
}
|
|
|
|
// delHash deletes a hash value or does nothing if the hash key does not exist.
|
|
func (bm *bucketMeta) delHash(hashKey string) {
|
|
bm.hashesLock.Lock()
|
|
delete(bm.hashes, hashKey)
|
|
bm.hashesLock.Unlock()
|
|
}
|
|
|
|
// createBucket metadata entry for the given nested bucket. Overwrites any
|
|
// existing entry so caller should ensure bucket does not already exist.
|
|
func (bm *bucketMeta) createBucket(name []byte) *bucketMeta {
|
|
bm.bucketsLock.Lock()
|
|
defer bm.bucketsLock.Unlock()
|
|
|
|
// Always create a new Bucket since CreateBucket above fails if the
|
|
// bucket already exists.
|
|
b := newBucketMeta()
|
|
bm.buckets[string(name)] = b
|
|
return b
|
|
}
|
|
|
|
// deleteBucket metadata entry for the given nested bucket. Does nothing if
|
|
// nested bucket metadata does not exist.
|
|
func (bm *bucketMeta) deleteBucket(name []byte) {
|
|
bm.bucketsLock.Lock()
|
|
delete(bm.buckets, string(name))
|
|
bm.bucketsLock.Unlock()
|
|
|
|
}
|
|
|
|
// getOrCreateBucket metadata entry for the given nested bucket.
|
|
func (bm *bucketMeta) getOrCreateBucket(name []byte) *bucketMeta {
|
|
bm.bucketsLock.Lock()
|
|
defer bm.bucketsLock.Unlock()
|
|
|
|
b, ok := bm.buckets[string(name)]
|
|
if !ok {
|
|
b = newBucketMeta()
|
|
bm.buckets[string(name)] = b
|
|
}
|
|
return b
|
|
}
|
|
|
|
type Bucket struct {
|
|
bm *bucketMeta
|
|
boltBucket *bbolt.Bucket
|
|
}
|
|
|
|
// newBucket creates a new view into a bucket backed by a boltdb
|
|
// transaction.
|
|
func newBucket(b *bucketMeta, bb *bbolt.Bucket) *Bucket {
|
|
return &Bucket{
|
|
bm: b,
|
|
boltBucket: bb,
|
|
}
|
|
}
|
|
|
|
// Put into boltdb iff it has changed since the last write.
|
|
func (b *Bucket) Put(key []byte, val interface{}) error {
|
|
// buffer for writing serialized state to
|
|
var buf bytes.Buffer
|
|
|
|
// Serialize the object
|
|
if err := codec.NewEncoder(&buf, structs.MsgpackHandle).Encode(val); err != nil {
|
|
return fmt.Errorf("failed to encode passed object: %v", err)
|
|
}
|
|
|
|
// Hash for skipping unnecessary writes
|
|
hashKey := string(key)
|
|
hashVal := blake2b.Sum256(buf.Bytes())
|
|
|
|
// lastHash value or nil if it hasn't been hashed yet
|
|
lastHash := b.bm.getHash(hashKey)
|
|
|
|
// If the hashes are equal, skip the write
|
|
if bytes.Equal(hashVal[:], lastHash) {
|
|
return nil
|
|
}
|
|
|
|
// New value: write it to the underlying boltdb
|
|
if err := b.boltBucket.Put(key, buf.Bytes()); err != nil {
|
|
return fmt.Errorf("failed to write data at key %s: %v", key, err)
|
|
}
|
|
|
|
// New value written, store hash (bucket path map was created above)
|
|
b.bm.setHash(hashKey, hashVal[:])
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
// Get value by key from boltdb or return an ErrNotFound error if key not
|
|
// found.
|
|
func (b *Bucket) Get(key []byte, obj interface{}) error {
|
|
// Get the raw data from the underlying boltdb
|
|
data := b.boltBucket.Get(key)
|
|
if data == nil {
|
|
return NotFound(string(key))
|
|
}
|
|
|
|
// Deserialize the object
|
|
if err := codec.NewDecoderBytes(data, structs.MsgpackHandle).Decode(obj); err != nil {
|
|
return fmt.Errorf("failed to decode data into passed object: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Delete removes a key from the bucket. If the key does not exist then nothing
|
|
// is done and a nil error is returned. Returns an error if the bucket was
|
|
// created from a read-only transaction.
|
|
func (b *Bucket) Delete(key []byte) error {
|
|
err := b.boltBucket.Delete(key)
|
|
b.bm.delHash(string(key))
|
|
return err
|
|
}
|
|
|
|
// Bucket represents a boltdb Bucket and its associated metadata necessary for
|
|
// write deduplication. Like bolt.Buckets it is only valid for the duration of
|
|
// the transaction that created it.
|
|
func (b *Bucket) Bucket(name []byte) *Bucket {
|
|
bb := b.boltBucket.Bucket(name)
|
|
if bb == nil {
|
|
return nil
|
|
}
|
|
|
|
bmeta := b.bm.getOrCreateBucket(name)
|
|
return newBucket(bmeta, bb)
|
|
}
|
|
|
|
// CreateBucket creates a new bucket at the given key and returns the new
|
|
// bucket. Returns an error if the key already exists, if the bucket name is
|
|
// blank, or if the bucket name is too long. The bucket instance is only valid
|
|
// for the lifetime of the transaction.
|
|
func (b *Bucket) CreateBucket(name []byte) (*Bucket, error) {
|
|
bb, err := b.boltBucket.CreateBucket(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
bmeta := b.bm.createBucket(name)
|
|
return newBucket(bmeta, bb), nil
|
|
}
|
|
|
|
// CreateBucketIfNotExists creates a new bucket if it doesn't already exist and
|
|
// returns a reference to it. The bucket instance is only valid for the
|
|
// lifetime of the transaction.
|
|
func (b *Bucket) CreateBucketIfNotExists(name []byte) (*Bucket, error) {
|
|
bb, err := b.boltBucket.CreateBucketIfNotExists(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
bmeta := b.bm.getOrCreateBucket(name)
|
|
return newBucket(bmeta, bb), nil
|
|
}
|
|
|
|
// DeleteBucket deletes a child bucket. Returns an error if the bucket
|
|
// corresponds to a non-bucket key or another error is encountered. No error is
|
|
// returned if the bucket does not exist.
|
|
func (b *Bucket) DeleteBucket(name []byte) error {
|
|
// Delete the bucket from the underlying boltdb
|
|
err := b.boltBucket.DeleteBucket(name)
|
|
if err == bbolt.ErrBucketNotFound {
|
|
err = nil
|
|
}
|
|
|
|
// Remove reference to child bucket
|
|
b.bm.deleteBucket(name)
|
|
return err
|
|
}
|
|
|
|
// BoltBucket returns the internal bolt.Bucket for this Bucket. Only valid
|
|
// for the duration of the current transaction.
|
|
func (b *Bucket) BoltBucket() *bbolt.Bucket {
|
|
return b.boltBucket
|
|
}
|