Files
threshold/internal/db/schema/manager.go
T
Todd Knight ec6151d174 Organize DB Schema Migration Code and DB Init Checks (#842)
Create Schema Manager and propagate contexts into the DB calls downstream.
Add checks at controller startup and database init for correct schema version and schema migration dirty bit being set.
2021-01-19 10:40:41 -07:00

188 lines
5.6 KiB
Go

package schema
import (
"bytes"
"context"
"database/sql"
"fmt"
"io"
"github.com/hashicorp/boundary/internal/db/schema/postgres"
"github.com/hashicorp/boundary/internal/errors"
)
// driver provides functionality to a database.
type driver interface {
TrySharedLock(context.Context) error
TryLock(context.Context) error
Lock(context.Context) error
Unlock(context.Context) error
UnlockShared(context.Context) error
Run(context.Context, io.Reader) error
// A value of -1 indicates no version is set.
SetVersion(context.Context, int, bool) error
// A value of -1 indicates no version is set.
Version(context.Context) (int, bool, error)
}
// Manager provides a way to run operations and retrieve information regarding
// the underlying boundary database schema.
// Manager is not thread safe.
type Manager struct {
db *sql.DB
driver driver
dialect string
}
// NewManager creates a new schema manager. An error is returned
// if the provided dialect is unrecognized or if the passed in db is unreachable.
func NewManager(ctx context.Context, dialect string, db *sql.DB) (*Manager, error) {
const op = "schema.NewManager"
dbM := Manager{db: db, dialect: dialect}
switch dialect {
case "postgres":
var err error
dbM.driver, err = postgres.New(ctx, db)
if err != nil {
return nil, errors.Wrap(err, op)
}
default:
return nil, errors.New(errors.InvalidParameter, op, fmt.Sprintf("unknown dialect %q", dialect))
}
return &dbM, nil
}
// State contains information regarding the current state of a boundary database's schema.
type State struct {
// InitializationStarted indicates if the current database has already been initialized
// (successfully or not) at least once.
InitializationStarted bool
// Dirty is set to true if the database failed in a previous migration/initialization.
Dirty bool
// DatabaseSchemaVersion is the schema version that is currently running in the database.
DatabaseSchemaVersion int
// BinarySchemaVersion is the schema version which this boundary binary supports.
BinarySchemaVersion int
}
// CurrentState provides the state of the boundary schema contained in the backing database.
func (b *Manager) CurrentState(ctx context.Context) (*State, error) {
dbS := State{
BinarySchemaVersion: BinarySchemaVersion(b.dialect),
}
v, dirty, err := b.driver.Version(ctx)
if err != nil {
return nil, err
}
if v == nilVersion {
return &dbS, nil
}
dbS.InitializationStarted = true
dbS.DatabaseSchemaVersion = v
dbS.Dirty = dirty
return &dbS, nil
}
// SharedLock attempts to obtain a shared lock on the database. This can fail if
// an exclusive lock is already held with the same key. An error is returned if
// a lock was unable to be obtained.
func (b *Manager) SharedLock(ctx context.Context) error {
const op = "schema.(Manager).SharedLock"
if err := b.driver.TrySharedLock(ctx); err != nil {
return errors.Wrap(err, op)
}
return nil
}
// SharedUnlock releases a shared lock on the database. If this
// fails for whatever reason an error is returned. Unlocking a lock
// that is not held is not an error.
func (b *Manager) SharedUnlock(ctx context.Context) error {
const op = "schema.(Manager).SharedUnlock"
if err := b.driver.UnlockShared(ctx); err != nil {
return errors.Wrap(err, op)
}
return nil
}
// ExclusiveLock attempts to obtain an exclusive lock on the database.
// An error is returned if a lock was unable to be obtained.
func (b *Manager) ExclusiveLock(ctx context.Context) error {
const op = "schema.(Manager).ExclusiveLock"
if err := b.driver.TryLock(ctx); err != nil {
return errors.Wrap(err, op)
}
return nil
}
// ExclusiveUnlock releases a shared lock on the database. If this
// fails for whatever reason an error is returned. Unlocking a lock
// that is not held is not an error.
func (b *Manager) ExclusiveUnlock(ctx context.Context) error {
const op = "schema.(Manager).ExclusiveUnlock"
if err := b.driver.Unlock(ctx); err != nil {
return errors.Wrap(err, op)
}
return nil
}
// RollForward updates the database schema to match the latest version known by
// the boundary binary. An error is not returned if the database is already at
// the most recent version.
func (b *Manager) RollForward(ctx context.Context) error {
const op = "schema.(Manager).RollForward"
// Capturing a lock that this session to the db already possesses is okay.
if err := b.driver.Lock(ctx); err != nil {
return errors.Wrap(err, op)
}
defer func() {
b.driver.Unlock(ctx)
}()
curVersion, dirty, err := b.driver.Version(ctx)
if err != nil {
return errors.Wrap(err, op)
}
if dirty {
return errors.New(errors.NotSpecificIntegrity, op, fmt.Sprintf("schema is dirty with version %d", curVersion))
}
sp, err := newStatementProvider(b.dialect, curVersion)
if err != nil {
return errors.Wrap(err, op)
}
return b.runMigrations(ctx, sp)
}
// runMigrations passes migration queries to a database driver and manages
// the version and dirty bit. Cancelation or deadline/timeout is managed
// through the passed in context.
func (b *Manager) runMigrations(ctx context.Context, qp *statementProvider) error {
const op = "schema.(Manager).runMigrations"
for qp.Next() {
select {
case <-ctx.Done():
return errors.Wrap(ctx.Err(), op)
default:
// context is not done yet. Continue on to the next query to execute.
}
// set version with dirty state
if err := b.driver.SetVersion(ctx, qp.Version(), true); err != nil {
return errors.Wrap(err, op)
}
if err := b.driver.Run(ctx, bytes.NewReader(qp.ReadUp())); err != nil {
return errors.Wrap(err, op)
}
// set clean state
if err := b.driver.SetVersion(ctx, qp.Version(), false); err != nil {
return errors.Wrap(err, op)
}
}
return nil
}