Files
threshold/internal/db/changesafe_reader_writer.go
T
Hugo VieiraandJeff Mitchell bed6e1a0ff feat(server): Reload database on SIGHUP
If the database url is updated on Boundary's config and then a SIGHUP
triggered, we now connect to the new database, ensure it's OK to use
and use that one going forwards instead.

This allows a controller to be deployed in a more dynamic infrastructure
where, for example, database credentials could change, without having
to restart the controller. In this case only a database url config
change and a SIGHUP signal to all controllers would be required.

This uses an atomic.Pointer to store wrapped DB objects. This allows
atomically swapping to the new object. A timeout is introduced so that
if the object is swapped after the existing one has been looked up it
isn't immediately invalidated. The timeout is a maximum; a ticker once a
second that looks at stats to determine how many database connections
are still in use and then closes the driver as soon as none are in use.

This could be done with mutexes to avoid checking stats in a loop, but
that carries its own risk: if a write lock is attempted to be acquired
while a query is holding a read lock, the write lock will block, and any
subsequent read locks will also block since write locks take priority.
As a result, if a query is long-running or timing out, all other queries
in the system will become blocked. Our default user request timeout is
90 seconds and some jobs have no timeout at all, so this is not a purely
theoretical concern, even if it's unlikely.

Refs: #2435
Co-authored-by: Jeff Mitchell <jeffrey.mitchell@gmail.com>
2022-09-12 17:55:32 +01:00

106 lines
4.2 KiB
Go

package db
import (
"context"
"database/sql"
"github.com/hashicorp/go-dbw"
)
var (
_ dbw.Reader = (*changeSafeDbwReader)(nil)
_ dbw.Writer = (*changeSafeDbwWriter)(nil)
)
// changeSafeDbwReader is a type that wraps a *db.Db as a dbw.Reader and ensures
// that uses of the underlying database follows changes to the connection.
type changeSafeDbwReader struct {
db *Db
}
func NewChangeSafeDbwReader(underlying *Db) dbw.Reader {
return &changeSafeDbwReader{db: underlying}
}
func (r *changeSafeDbwReader) LookupBy(ctx context.Context, resource any, opt ...dbw.Option) error {
return dbw.New(r.db.underlying.wrapped.Load()).LookupBy(ctx, resource, opt...)
}
func (r *changeSafeDbwReader) LookupByPublicId(ctx context.Context, resource dbw.ResourcePublicIder, opt ...dbw.Option) error {
return dbw.New(r.db.underlying.wrapped.Load()).LookupByPublicId(ctx, resource, opt...)
}
func (r *changeSafeDbwReader) LookupWhere(ctx context.Context, resource any, where string, args []any, opt ...dbw.Option) error {
return dbw.New(r.db.underlying.wrapped.Load()).LookupWhere(ctx, resource, where, args, opt...)
}
func (r *changeSafeDbwReader) Query(ctx context.Context, sql string, values []any, opt ...dbw.Option) (*sql.Rows, error) {
return dbw.New(r.db.underlying.wrapped.Load()).Query(ctx, sql, values, opt...)
}
func (r *changeSafeDbwReader) ScanRows(rows *sql.Rows, result any) error {
return dbw.New(r.db.underlying.wrapped.Load()).ScanRows(rows, result)
}
func (r *changeSafeDbwReader) SearchWhere(ctx context.Context, resources any, where string, args []any, opt ...dbw.Option) error {
return dbw.New(r.db.underlying.wrapped.Load()).SearchWhere(ctx, resources, where, args, opt...)
}
// changeSafeDbwWriter is a type that wraps a *db.Db as a dbw.Writer and ensures
// that uses of the underlying database follows changes to the connection.
type changeSafeDbwWriter struct {
db *Db
}
func NewChangeSafeDbwWriter(underlying *Db) dbw.Writer {
return &changeSafeDbwWriter{db: underlying}
}
func (w *changeSafeDbwWriter) Begin(ctx context.Context) (*dbw.RW, error) {
return dbw.New(w.db.underlying.wrapped.Load()).Begin(ctx)
}
func (w *changeSafeDbwWriter) Commit(ctx context.Context) error {
return dbw.New(w.db.underlying.wrapped.Load()).Commit(ctx)
}
func (w *changeSafeDbwWriter) Create(ctx context.Context, i any, opt ...dbw.Option) error {
return dbw.New(w.db.underlying.wrapped.Load()).Create(ctx, i, opt...)
}
func (w *changeSafeDbwWriter) CreateItems(ctx context.Context, createItems []any, opt ...dbw.Option) error {
return dbw.New(w.db.underlying.wrapped.Load()).CreateItems(ctx, createItems, opt...)
}
func (w *changeSafeDbwWriter) Delete(ctx context.Context, i any, opt ...dbw.Option) (int, error) {
return dbw.New(w.db.underlying.wrapped.Load()).Delete(ctx, i, opt...)
}
func (w *changeSafeDbwWriter) DeleteItems(ctx context.Context, deleteItems []any, opt ...dbw.Option) (int, error) {
return dbw.New(w.db.underlying.wrapped.Load()).DeleteItems(ctx, deleteItems, opt...)
}
func (w *changeSafeDbwWriter) DoTx(ctx context.Context, retryErrorsMatchingFn func(error) bool, retries uint, backOff dbw.Backoff, handler dbw.TxHandler) (dbw.RetryInfo, error) {
return dbw.New(w.db.underlying.wrapped.Load()).DoTx(ctx, retryErrorsMatchingFn, retries, backOff, handler)
}
func (w *changeSafeDbwWriter) Exec(ctx context.Context, sql string, values []any, opt ...dbw.Option) (int, error) {
return dbw.New(w.db.underlying.wrapped.Load()).Exec(ctx, sql, values, opt...)
}
func (w *changeSafeDbwWriter) Query(ctx context.Context, sql string, values []any, opt ...dbw.Option) (*sql.Rows, error) {
return dbw.New(w.db.underlying.wrapped.Load()).Query(ctx, sql, values, opt...)
}
func (w *changeSafeDbwWriter) Rollback(ctx context.Context) error {
return dbw.New(w.db.underlying.wrapped.Load()).Rollback(ctx)
}
func (w *changeSafeDbwWriter) ScanRows(rows *sql.Rows, result any) error {
return dbw.New(w.db.underlying.wrapped.Load()).ScanRows(rows, result)
}
func (w *changeSafeDbwWriter) Update(ctx context.Context, i interface{}, fieldMaskPaths []string, setToNullPaths []string, opt ...dbw.Option) (int, error) {
return dbw.New(w.db.underlying.wrapped.Load()).Update(ctx, i, fieldMaskPaths, setToNullPaths, opt...)
}