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>
db package
Usage
Just some high-level usage highlights to get you started. Read the godocs for a complete list of capabilities and their documentation.
dialect = postgres.New(postgres.Config{
DSN: connectionUrl},
)
conn, _ := gorm.Open(dialect, &gorm.Config{})
// Db implements both the Reader and Writer interfaces
rw := Db{Tx: conn}
// There are writer methods like: Create, Update and Delete
// that will write Gorm struct to the db. These writer methods
// all support options for writing Oplog entries for the
// caller: WithOplog(yourWrapper, yourMetadata)
// the caller is responsible for the transaction life cycle of the writer
// and if an error is returned the caller must decide what to do with
// the transaction, which is almost always a rollback for the caller.
err = rw.Create(context.Background(), user)
// There are reader methods like: LookupByPublicId,
// LookupByName, SearchBy, LookupBy, etc
// which will lookup resources for you and scan them into your Gorm struct
err = rw.LookupByPublicId(context.Background(), foundUser)
// There's reader ScanRows that facilitates scanning rows from
// a query into your Gorm struct
where := "select * from test_users where name in ($1, $2)"
rows, err := rw.Query(context.Background(), where, []interface{}{"alice", "bob"})
defer rows.Close()
for rows.Next() {
user := db_test.NewTestUser()
// scan the row into your Gorm struct
if err := rw.ScanRows(rows, &user); err != nil {
return err
}
// Do something with the Gorm user struct
}
// DoTx is a writer function that wraps a TxHandler
// in a retryable transaction. You simply implement a
// TxHandler that does your writes and hand the handler
// to DoTx, which will wrap the writes in a retryable
// transaction with the retry attempts and backoff
// strategy you specify via options.
_, err = w.DoTx(
context.Background(),
10, // ten retries
ExpBackoff{}, // exponential backoff
func(w Writer) error {
// the TxHandler updates the user's friendly name
return w.Update(context.Background(), user, []string{"FriendlyName"},
// write oplogs for this update
WithOplog(
InitTestWrapper(t),
oplog.Metadata{
"key-only": nil,
"deployment": []string{"amex"},
"project": []string{"central-info-systems", "local-info-systems"},
},
),
)
})