-
-
Notifications
You must be signed in to change notification settings - Fork 407
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
BeforeConnect Hook #1875
base: v10
Are you sure you want to change the base?
BeforeConnect Hook #1875
Conversation
base.go
Outdated
@@ -115,6 +115,12 @@ func (db *baseDB) initConn(ctx context.Context, cn *pool.Conn) error { | |||
} | |||
} | |||
|
|||
if db.opt.BeforeConnect != nil { | |||
if err := db.opt.BeforeConnect(ctx, db.opt); err != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe we should copy the options to avoid data race (since this hooks is called concurrently):
opt := db.opt.Clone()
// use the cloned opt
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
and then reassign to db.opt?
if db.opt.BeforeConnect != nil {
opts := db.opt.Clone()
if err := opt.BeforeConnect(ctx, opt); err != nil { return err }
db.opts = opts
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you re assign to db.opt then you need to make that re assignment atomic (*thread safe?). What if this code is being executed in multiple go routines?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also if we replace the db.opt here this can lead to a few interesting issues depending on how the user is using the library. What if they change the DB address entirely? what if they add TLS? Beyond simply changing the password or the user that a connection is issued with; changing other variables available in the options object could have a lot of unintended consequences that are currently unknown.
If at all possible we might just want to limit being able to change authentication parameters if we do update the original db.opts
. Maybe limiting it to changing the user, password and TLS config.
If they for some reason changed something like the connection pool settings, how would that affect the pool that is already running?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@justcompile I don't think you need to replace db.opt
. Just clone it (when needed for BeforeConnect) and use below in startup:
opt := db.opt.Clone()
opt.BeforeConnect(ctx, opt)
db.startup(ctx, cn, opt.User, opt.Password, opt.Database, opt.ApplicationName)
That will not cover all options but is good enough for this particular use case. We can improve it later if needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if db.opt.BeforeConnect != nil {
db.opt.mux.Lock()
updateOpts := &BeforeConnectOptions{
User: db.opt.User,
Password: db.opt.Password,
TLSConfig: db.opt.TLSConfig,
}
if err := db.opt.BeforeConnect(ctx, updateOpts); err != nil {
db.opt.mux.Unlock()
return err
}
db.opt.applyUpdatedConnectionOptions(updateOpts)
db.opt.mux.Unlock()
}
// options.go
func (opt *Options) applyUpdatedConnectionOptions(bcOpts *BeforeConnectOptions) {
opt.User = bcOpts.User
opt.Password = bcOpts.Password
if bcOpts.TLSConfig != nil {
opt.TLSConfig = bcOpts.TLSConfig.Clone()
}
}
Based on the concurrency issue mentioned & the points around unintended side effects of a user changing the Addr or Pool options, I was looking at something like the above.
But if that's overkill, I can go with the opts.Clone approach.
I'm also locking the mux here rather than within applyUpdatedConnectionOptions
so that if multiple go routines are calling BeforeConnect
it's a bit more deterministic
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's keep as is BeforeConnect(ctx context.Context, o *pg.Options) error
. It will not work for all options but that is fine. It is an advanced feature. Add add a warning to the comment.
@@ -108,6 +108,30 @@ func TestDBConnectWithStartupNotice(t *testing.T) { | |||
require.NoError(t, db.Ping(context.Background()), "must successfully ping database with long application name") | |||
} | |||
|
|||
func TestBeforeConnect(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would add another test that tests the before connect with multiple go routines to make sure that a race condition is not triggered by accident. A race condition in a before connection hook for an ORM could cripple an application.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think a good way to do this would be to set the options to have a max pool of 1. And then try to send multiple queries concurrently in a few go routines.
This should trigger a concurrent call of the BeforeConnect method; and given the -race
flag should be a sufficient enough smoke test for most use cases.
Thanks for the feedback, I'll work the suggestions into this PR over the coming days |
@vmihailenco any way I'd be able to take this over? Still very interested in it |
@elliotcourant sure, go ahead 👍 |
99258be
to
4d138f1
Compare
@vmihailenco sorry for the horrendous delay in this. Before I get around to resolve the testing points from @elliotcourant, could I confirm whether the current changes align with your expectations around cloning Options? |
@justcompile I will take a look at the diff again this weekend and double check it! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still looks good to me, added some more info about how the before connect thing could be tested.
Network: opt.Network, | ||
Addr: opt.Addr, | ||
Dialer: opt.Dialer, | ||
BeforeConnect: opt.BeforeConnect, | ||
OnConnect: opt.OnConnect, | ||
User: opt.User, | ||
Password: opt.Password, | ||
Database: opt.Database, | ||
ApplicationName: opt.ApplicationName, | ||
TLSConfig: opt.TLSConfig.Clone(), | ||
DialTimeout: opt.DialTimeout, | ||
ReadTimeout: opt.ReadTimeout, | ||
WriteTimeout: opt.WriteTimeout, | ||
MaxRetries: opt.MaxRetries, | ||
RetryStatementTimeout: opt.RetryStatementTimeout, | ||
MinRetryBackoff: opt.MinRetryBackoff, | ||
MaxRetryBackoff: opt.MaxRetryBackoff, | ||
PoolSize: opt.PoolSize, | ||
MinIdleConns: opt.MinIdleConns, | ||
MaxConnAge: opt.MaxConnAge, | ||
PoolTimeout: opt.PoolTimeout, | ||
IdleTimeout: opt.IdleTimeout, | ||
IdleCheckFrequency: opt.IdleCheckFrequency, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do these need to be go fmt
ed?
@@ -108,6 +108,30 @@ func TestDBConnectWithStartupNotice(t *testing.T) { | |||
require.NoError(t, db.Ping(context.Background()), "must successfully ping database with long application name") | |||
} | |||
|
|||
func TestBeforeConnect(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think a good way to do this would be to set the options to have a max pool of 1. And then try to send multiple queries concurrently in a few go routines.
This should trigger a concurrent call of the BeforeConnect method; and given the -race
flag should be a sufficient enough smoke test for most use cases.
8925177
to
49ebb57
Compare
As per discussions in #1848
This PR adds a new hook into
Options
to facilitate the use of secret providers / dynamic / one-time passwords. It has been added asBeforeConnect
in order for the usage to be generic; whether it is for secret providers or simply logging when database connections are established.Example
some error handling removed for readability