mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-08-28 02:44:59 +00:00
* Implement Schemar.SetDatabaseOption(option, value string) This replaces the temporary `SetDatabaseOptions()` method, which replaced the entire DatabaseOptions struct, with `SetDatabaseOption` which takes an option/value pair of strings to set. * Add SetDatabaseOption to controller http handler and client This commit also: - renames some `writeLog` to `writelog` - updates ApplyDirective to call resource.Unlock() on any resources being removed from the local worker * Add Database related methods to SchemaAPI interface Currently all implementations of this interface are implemented with "unimplemented" errors on those methods. Next will be to implement the necessary methods. * SQL: CREATE DATABASE and SHOW DATABASES * SQL: DROP DATABASE * SQL: Add UNITS option to CREATE DATABASE * SQL: ALTER DATABASE * User serverlessStorage.Remove[*]Resource instead of resource.Unlock() * Add WITH keyword to CREATE/ALTER DATABASE * fix some WITH logic * linter fixes * WITH on CREATE DATABASE is not required
468 lines
15 KiB
Go
468 lines
15 KiB
Go
package storage
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"path"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/featurebasedb/featurebase/v3/dax"
|
|
"github.com/featurebasedb/featurebase/v3/dax/computer"
|
|
"github.com/featurebasedb/featurebase/v3/errors"
|
|
"github.com/featurebasedb/featurebase/v3/logger"
|
|
)
|
|
|
|
// ResourceManager holds all the various Resources each of which is
|
|
// specific to a particular shard, table key partition or field, but
|
|
// all of which use the same underlying snapshotter and writelogger.
|
|
type ResourceManager struct {
|
|
Snapshotter computer.SnapshotService
|
|
Writelogger computer.WritelogService
|
|
Logger logger.Logger
|
|
|
|
mu sync.Mutex
|
|
shardResources map[shardK]*Resource
|
|
tableKeyResources map[tableKeyK]*Resource
|
|
fieldKeyResources map[fieldKeyK]*Resource
|
|
}
|
|
|
|
func NewResourceManager(s computer.SnapshotService, w computer.WritelogService, l logger.Logger) *ResourceManager {
|
|
return &ResourceManager{
|
|
Snapshotter: s,
|
|
Writelogger: w,
|
|
Logger: l,
|
|
|
|
shardResources: make(map[shardK]*Resource),
|
|
tableKeyResources: make(map[tableKeyK]*Resource),
|
|
fieldKeyResources: make(map[fieldKeyK]*Resource),
|
|
}
|
|
}
|
|
|
|
// compound map keys
|
|
|
|
type shardK struct {
|
|
qtid dax.QualifiedTableID
|
|
partition dax.PartitionNum
|
|
shard dax.ShardNum
|
|
}
|
|
|
|
type tableKeyK struct {
|
|
qtid dax.QualifiedTableID
|
|
partition dax.PartitionNum
|
|
}
|
|
|
|
type fieldKeyK struct {
|
|
qtid dax.QualifiedTableID
|
|
field dax.FieldName
|
|
}
|
|
|
|
func (mm *ResourceManager) GetShardResource(qtid dax.QualifiedTableID, partition dax.PartitionNum, shard dax.ShardNum) *Resource {
|
|
mm.mu.Lock()
|
|
defer mm.mu.Unlock()
|
|
key := shardK{qtid: qtid, partition: partition, shard: shard}
|
|
if m, ok := mm.shardResources[key]; ok {
|
|
return m
|
|
}
|
|
mm.shardResources[key] = (&Resource{
|
|
snapshotter: mm.Snapshotter,
|
|
writelogger: mm.Writelogger,
|
|
bucket: partitionBucket(qtid.Key(), partition),
|
|
key: shardKey(shard),
|
|
log: mm.Logger,
|
|
}).initialize()
|
|
|
|
return mm.shardResources[key]
|
|
}
|
|
|
|
func (mm *ResourceManager) RemoveShardResource(qtid dax.QualifiedTableID, partition dax.PartitionNum, shard dax.ShardNum) {
|
|
mm.mu.Lock()
|
|
defer mm.mu.Unlock()
|
|
key := shardK{qtid: qtid, partition: partition, shard: shard}
|
|
if m, ok := mm.shardResources[key]; ok {
|
|
err := m.Unlock()
|
|
if err != nil {
|
|
mm.Logger.Printf("unlocking shard resource during removal: %v", err)
|
|
}
|
|
delete(mm.shardResources, key)
|
|
}
|
|
}
|
|
|
|
func (mm *ResourceManager) GetTableKeyResource(qtid dax.QualifiedTableID, partition dax.PartitionNum) *Resource {
|
|
mm.mu.Lock()
|
|
defer mm.mu.Unlock()
|
|
key := tableKeyK{qtid: qtid, partition: partition}
|
|
if m, ok := mm.tableKeyResources[key]; ok {
|
|
return m
|
|
}
|
|
mm.tableKeyResources[key] = (&Resource{
|
|
snapshotter: mm.Snapshotter,
|
|
writelogger: mm.Writelogger,
|
|
bucket: partitionBucket(qtid.Key(), partition),
|
|
key: keysFileName,
|
|
log: mm.Logger,
|
|
}).initialize()
|
|
return mm.tableKeyResources[key]
|
|
}
|
|
|
|
func (mm *ResourceManager) RemoveTableKeyResource(qtid dax.QualifiedTableID, partition dax.PartitionNum) {
|
|
mm.mu.Lock()
|
|
defer mm.mu.Unlock()
|
|
key := tableKeyK{qtid: qtid, partition: partition}
|
|
if m, ok := mm.tableKeyResources[key]; ok {
|
|
err := m.Unlock()
|
|
if err != nil {
|
|
mm.Logger.Printf("unlocking table key resource during removal: %v", err)
|
|
}
|
|
delete(mm.tableKeyResources, key)
|
|
}
|
|
}
|
|
|
|
func (mm *ResourceManager) GetFieldKeyResource(qtid dax.QualifiedTableID, field dax.FieldName) *Resource {
|
|
mm.mu.Lock()
|
|
defer mm.mu.Unlock()
|
|
key := fieldKeyK{qtid: qtid, field: field}
|
|
if m, ok := mm.fieldKeyResources[key]; ok {
|
|
return m
|
|
}
|
|
mm.fieldKeyResources[key] = (&Resource{
|
|
snapshotter: mm.Snapshotter,
|
|
writelogger: mm.Writelogger,
|
|
bucket: fieldBucket(qtid.Key(), field),
|
|
key: keysFileName,
|
|
log: mm.Logger,
|
|
}).initialize()
|
|
return mm.fieldKeyResources[key]
|
|
}
|
|
|
|
func (mm *ResourceManager) RemoveFieldKeyResource(qtid dax.QualifiedTableID, field dax.FieldName) {
|
|
mm.mu.Lock()
|
|
defer mm.mu.Unlock()
|
|
key := fieldKeyK{qtid: qtid, field: field}
|
|
if m, ok := mm.fieldKeyResources[key]; ok {
|
|
err := m.Unlock()
|
|
if err != nil {
|
|
mm.Logger.Printf("unlocking field key resource during removal: %v", err)
|
|
}
|
|
delete(mm.fieldKeyResources, key)
|
|
}
|
|
}
|
|
|
|
// RemoveAll unlocks and deletes all resources held within this
|
|
// ResourceManager. To be clear, the underlying resources are *not*
|
|
// deleted, they are simply no longer under this resource manager's
|
|
// purview. If there's a problem removing any resource, we'll save the
|
|
// error, but keep going... this is only called on shutdown, so we do
|
|
// our best to unlock everything.
|
|
func (mm *ResourceManager) RemoveAll() error {
|
|
mm.mu.Lock()
|
|
defer mm.mu.Unlock()
|
|
|
|
errList := make([]error, 0)
|
|
for k, resource := range mm.shardResources {
|
|
err := resource.Unlock()
|
|
if err != nil && !strings.Contains(err.Error(), "resource was not locked") {
|
|
errList = append(errList, err)
|
|
}
|
|
delete(mm.shardResources, k)
|
|
}
|
|
for k, resource := range mm.tableKeyResources {
|
|
err := resource.Unlock()
|
|
if err != nil && !strings.Contains(err.Error(), "resource was not locked") {
|
|
errList = append(errList, err)
|
|
}
|
|
delete(mm.tableKeyResources, k)
|
|
}
|
|
for k, resource := range mm.fieldKeyResources {
|
|
err := resource.Unlock()
|
|
if err != nil && !strings.Contains(err.Error(), "resource was not locked") {
|
|
errList = append(errList, err)
|
|
}
|
|
delete(mm.fieldKeyResources, k)
|
|
}
|
|
if len(errList) > 0 {
|
|
return errors.Errorf("%v", errList)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RemoveTable, unlocks and removes all resources related to the given
|
|
// table from this ResourceManager. The underlying files are not
|
|
// deleted. (If the table is being dropped, deleting the files is
|
|
// under the purview of metadata services). In this case (in contrast
|
|
// to RemoveAll), we'll stop on any error and return it. Not sure it
|
|
// actually matters in either of these cases.
|
|
func (mm *ResourceManager) RemoveTable(qtid dax.QualifiedTableID) error {
|
|
mm.mu.Lock()
|
|
defer mm.mu.Unlock()
|
|
|
|
for k, resource := range mm.shardResources {
|
|
if k.qtid == qtid {
|
|
if err := resource.Unlock(); err != nil {
|
|
return errors.Wrap(err, "unlocking shard")
|
|
}
|
|
}
|
|
}
|
|
for k, resource := range mm.tableKeyResources {
|
|
if k.qtid == qtid {
|
|
if err := resource.Unlock(); err != nil {
|
|
return errors.Wrap(err, "unlocking shard")
|
|
}
|
|
}
|
|
}
|
|
for k, resource := range mm.fieldKeyResources {
|
|
if k.qtid == qtid {
|
|
if err := resource.Unlock(); err != nil {
|
|
return errors.Wrap(err, "deleting field key resource %s")
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Resource wraps the snapshotter and writelogger to maintain messy
|
|
// state between calls. Resource is *not* threadsafe, care should be
|
|
// taken that concurrent calls are not made to Resource methods. The
|
|
// exception being that Snapshot and Append are safe to call
|
|
// concurrently.
|
|
type Resource struct {
|
|
snapshotter computer.SnapshotService
|
|
writelogger computer.WritelogService
|
|
bucket string
|
|
key string
|
|
|
|
log logger.Logger
|
|
|
|
loadWLsPastVersion int
|
|
latestWLVersion int
|
|
lastWLPos int
|
|
|
|
locked bool
|
|
|
|
dirty bool
|
|
}
|
|
|
|
func (m *Resource) initialize() *Resource {
|
|
m.loadWLsPastVersion = -2
|
|
m.latestWLVersion = -1
|
|
m.lastWLPos = -1
|
|
return m
|
|
}
|
|
|
|
// IsLocked checks to see if this particular instance of the resource
|
|
// believes it holds the lock. It does not look at the state of
|
|
// underlying storage to verify the lock.
|
|
func (m *Resource) IsLocked() bool {
|
|
return m.locked
|
|
}
|
|
|
|
// LoadLatestSnapshot finds the most recent snapshot for this resource
|
|
// and returns a ReadCloser for that snapshot data. If there is no
|
|
// snapshot for this resource it returns nil, nil.
|
|
func (m *Resource) LoadLatestSnapshot() (data io.ReadCloser, err error) {
|
|
snaps, err := m.snapshotter.List(m.bucket, m.key)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "listing snapshots")
|
|
}
|
|
m.log.Debugf("LoadLatestSnapshot %s/%s: list: %v", m.bucket, m.key, snaps)
|
|
m.lastWLPos = 0
|
|
|
|
if len(snaps) == 0 {
|
|
m.loadWLsPastVersion = -1
|
|
return nil, nil
|
|
}
|
|
// assuming snapshots come back in sorted order
|
|
latest := snaps[len(snaps)-1]
|
|
m.loadWLsPastVersion = latest.Version
|
|
|
|
// TODO(jaffee): whatever is using the snapshot may discover that
|
|
// it is corrupted/incomplete. We don't want to separately check
|
|
// the checksum in here because then we'd have to read the whole
|
|
// snapshot twice. Need a way to catch the checksum error and tell
|
|
// Resource to mark that version as bad and remove it, then try
|
|
// LoadLatestSnapshot again.
|
|
return m.snapshotter.Read(m.bucket, m.key, latest.Version)
|
|
}
|
|
|
|
// // Potential future methods to support getting older versions. SnapInfo would have timestamp information as well.
|
|
//
|
|
// ListSnapshots() []SnapInfo
|
|
// LoadSnapshot(version int) (data io.ReadCloser, err error)
|
|
|
|
// LoadWriteLog can be called after LoadLatestSnapshot. It loads any
|
|
// writelog data which has been written since the latest
|
|
// snapshot. Subsequent calls to LoadWriteLog will only return new
|
|
// data that hasn't previously been returned from LoadWriteLog. If
|
|
// there is no writelog, it returns nil, nil.
|
|
func (m *Resource) LoadWriteLog() (data io.ReadCloser, err error) {
|
|
if m.loadWLsPastVersion == -2 {
|
|
return nil, errors.New(errors.ErrUncoded, "LoadWriteLog called in inconsistent state, can't tell what version to load from")
|
|
}
|
|
wLogs, err := m.writelogger.List(m.bucket, m.key)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "listing write logs")
|
|
}
|
|
|
|
m.log.Debugf("LoadWriteLog %s/%s: list: %v", m.bucket, m.key, wLogs)
|
|
|
|
versions := make([]int, 0, len(wLogs))
|
|
for _, log := range wLogs {
|
|
if log.Version > m.loadWLsPastVersion {
|
|
versions = append(versions, log.Version)
|
|
}
|
|
}
|
|
|
|
if len(versions) > 1 {
|
|
// TODO(jaffee) This can happen if there's a failure writing a
|
|
// snapshot. Need to implement a MultiReadCloser or similar
|
|
// that wraps all the latest write logs into one ReadCloser.
|
|
// It should only wrap the last one in a trackingReader.
|
|
return nil, errors.New(dax.ErrUnimplemented, "UNIMPLEMENTED: multiple write log versions ahead of latest snapshot.")
|
|
}
|
|
|
|
if len(versions) == 0 {
|
|
m.log.Debugf("LoadWriteLog: no logs after snapshot: %d on %s", m.loadWLsPastVersion, path.Join(m.bucket, m.key))
|
|
m.latestWLVersion = m.loadWLsPastVersion + 1
|
|
return nil, nil
|
|
}
|
|
|
|
if m.locked && m.latestWLVersion != versions[0] {
|
|
return nil, errors.New(errors.ErrUncoded, "write log version gone since locking")
|
|
}
|
|
m.latestWLVersion = versions[0]
|
|
m.dirty = true
|
|
|
|
r, err := m.writelogger.LogReaderFrom(m.bucket, m.key, versions[0], m.lastWLPos)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "getting writelog")
|
|
}
|
|
return &trackingReader{
|
|
r: r,
|
|
update: func(n int, err error) {
|
|
m.lastWLPos += n
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// Lock acquires an advisory lock for this resource which grants
|
|
// us exclusive access to write to it. The normal pattern is to
|
|
// call:
|
|
//
|
|
// 1. LoadLatestSnapshot
|
|
// 2. LoadWriteLog
|
|
// 3. Lock
|
|
// 4. LoadWriteLog
|
|
//
|
|
// The second call to LoadWriteLog is necessary in case any writes
|
|
// occurred between the last load and acquiring the lock. Once the
|
|
// lock is acquired it should not be possible for any more writes
|
|
// to occur. Lock will error if (a) we fail to acquire the lock or
|
|
// (b) the state of the snapshot store for this resource is not
|
|
// identical to what is was before the lock was acquired. Case (b)
|
|
// means that quite a lot has happened in between LoadWriteLog and
|
|
// Lock, and we should probably just die and start over.
|
|
func (m *Resource) Lock() error {
|
|
m.log.Debugf("Lock %s/%s", m.bucket, m.key)
|
|
// lock is sort of arbitrarily on the write log interface
|
|
if err := m.writelogger.Lock(m.bucket, m.key); err != nil {
|
|
return errors.Wrap(err, "acquiring lock")
|
|
}
|
|
m.locked = true
|
|
return nil
|
|
}
|
|
|
|
// Append appends the msg to the write log. It will fail if we
|
|
// haven't properly loaded and gotten a lock for the resource
|
|
// we're writing to.
|
|
func (m *Resource) Append(msg []byte) error {
|
|
m.log.Debugf("Append %s/%s", m.bucket, m.key)
|
|
if m.latestWLVersion < 0 {
|
|
return errors.New(errors.ErrUncoded, "can't call append before loading and locking write log")
|
|
}
|
|
m.dirty = true
|
|
return m.writelogger.AppendMessage(m.bucket, m.key, m.latestWLVersion, msg)
|
|
}
|
|
|
|
// IncrementWLVersion should be called during snapshotting with a
|
|
// write Tx held on the local resource. This ensures that any writes
|
|
// which completed prior to the snapshot are in the prior WL and any
|
|
// that complete after the snapshot are in the incremented WL. If
|
|
// there have been no writes since the latest snapshot, this returns
|
|
// false and does nothing. In this case, Snapshot should *not* be
|
|
// called.
|
|
func (m *Resource) IncrementWLVersion() (bool, error) {
|
|
if !m.dirty {
|
|
return false, nil
|
|
}
|
|
m.log.Debugf("IncrementWLVersion %s/%s", m.bucket, m.key)
|
|
m.latestWLVersion++
|
|
m.lastWLPos = -1
|
|
m.loadWLsPastVersion = -1
|
|
m.dirty = false
|
|
return true, nil
|
|
}
|
|
|
|
// Snapshot takes a ReadCloser which has the contents of the resource
|
|
// being tracked at a particular point in time and writes them to the
|
|
// Snapshot Store. Upon a successful write it will truncate any write
|
|
// logs which are now incorporated into the snapshot. Do not call
|
|
// until after calling IncrementWLVersion, and only if that method
|
|
// returns "true".
|
|
func (m *Resource) Snapshot(rc io.ReadCloser) error {
|
|
m.log.Debugf("Snapshot %s/%s", m.bucket, m.key)
|
|
// latestWLVersion has already been incremented at this point, so
|
|
// we write that version minus 1.
|
|
err := m.snapshotter.Write(m.bucket, m.key, m.latestWLVersion-1, rc)
|
|
if err != nil {
|
|
return errors.Wrap(err, "writing snapshot")
|
|
}
|
|
err = m.writelogger.DeleteLog(m.bucket, m.key, m.latestWLVersion-1)
|
|
return errors.Wrap(err, "deleting old write log")
|
|
}
|
|
|
|
// SnapshotTo is Snapshot's ugly stepsister supporting the weirdness
|
|
// of reading from translate stores who we're hoping to off in the
|
|
// next season.
|
|
func (m *Resource) SnapshotTo(wt io.WriterTo) error {
|
|
m.log.Debugf("SnapshotTo %s/%s", m.bucket, m.key)
|
|
err := m.snapshotter.WriteTo(m.bucket, m.key, m.latestWLVersion-1, wt)
|
|
if err != nil {
|
|
return errors.Wrap(err, "writing snapshot SnapshotTo")
|
|
}
|
|
err = m.writelogger.DeleteLog(m.bucket, m.key, m.latestWLVersion-1)
|
|
return errors.Wrap(err, "deleting old write log snapshotTo")
|
|
}
|
|
|
|
// Unlock releases the lock. This should be called if control of
|
|
// the underlying resource is being transitioned to another
|
|
// node. Ideally it's also called if the process crashes (e.g. via
|
|
// a defer), but an implementation based on filesystem locks
|
|
// should have those removed by the operating system when the
|
|
// process exits anyway.
|
|
func (m *Resource) Unlock() error {
|
|
m.log.Debugf("Unlock %s/%s", m.bucket, m.key)
|
|
if !m.locked {
|
|
return errors.New(errors.ErrUncoded, "resource was not locked")
|
|
}
|
|
if err := m.writelogger.Unlock(m.bucket, m.key); err != nil {
|
|
return errors.Wrap(err, "unlocking")
|
|
}
|
|
m.locked = false
|
|
return nil
|
|
}
|
|
|
|
const (
|
|
keysFileName = "keys"
|
|
)
|
|
|
|
func partitionBucket(table dax.TableKey, partition dax.PartitionNum) string {
|
|
return path.Join(string(table), "partition", fmt.Sprintf("%d", partition))
|
|
}
|
|
|
|
func shardKey(shard dax.ShardNum) string {
|
|
return path.Join("shard", fmt.Sprintf("%d", shard))
|
|
}
|
|
|
|
func fieldBucket(table dax.TableKey, field dax.FieldName) string {
|
|
return path.Join(string(table), "field", string(field))
|
|
}
|