-
Notifications
You must be signed in to change notification settings - Fork 15
allow hikari password changes at runtime #5308
Conversation
This adds a method to the hikari connection manager so that the password can be changed at runtime. This is a requirement to support being able to rotate database passwords without needing to restart services. Also fixes a bug that made HikariCpConnectionManagerTest not actually run (because it was not in a suite). The test itself was actually broken because it did not wait for the DB to be ready (the suite setup correctly makes sure that it can connect to the DB before running tests).
Generate changelog in
|
984bdb4
to
1719a45
Compare
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've done a preliminary review; @jeremyk-91 will give his own pass here (I'm trying to familiarise myself with parts of the codebase I don't know, so apologies if I've misunderstood this codepath at all).
if (localState.type == StateType.NORMAL) { | ||
// pool is initialized, need to set password directly on the pool | ||
internalSetPassword(newPassword, localState.dataSourcePool); |
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'm not that familiar with this codepath, but what happens if the state changes after this if
check? Why don't you synchronise here?
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.
Equally, what happens (and can it happen?) if there is a race to change the password - is it possible that the username gets set to null
but the password is set, or vice versa?
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.
This doesn't need to be synchronized because the only other possible state change is closing the pool. That is completely fine to happen concurrently because all this does is update a volatile field on the pool (it is fine to update that concurrently/after the pool is closed).
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 it is completely fine for the username to be null while the password is not null (the password field would just be ignored in that case which means it will use the original configured password). This would only happen with a specific execution order of concurrent calls where one setPassword passes in the original password and another passes in a different password.
So consider the following two concurrent calls where the static config has the password set as "originalPassword":
- setPassword("originalPassword")
- setPassword("newPassword")
The possible outcomes of this are as follows for the username/password override fields:
- username = null; password = null; => "originalPassword" used for connections
- username = null; password = "newPassword"; => "originalPassword" used for connections
- username = "username"; password = "newPassword"; => "newPassword" used for connections (JDBC parameters copied for all new connections)
- username = "username"; password = null; => "originalPassword" used for connections (although in this case the JDBC parameters are copied for all new connections)
Regardless of the outcome, the behavior is arguably correct (you can say one of the calls "wins" and that is what the end behavior is). There is also no point in time where any password would be used other than one of the passwords from the concurrent calls.
In practice there should not be concurrent calls to this. The only caller should be a subscription on the witchcraft Runtime config object. Witchcraft uses a single task on a schedule using scheduleWithFixedDelay to check the runtime config and calls the subscribers on the same thread (so no Runtime config subscription calls will ever overlap).
The only thing we need to really worry about is other threads that may call init() or close(), but that is handled correctly by the current code which checks the state.
if (localState.type == StateType.ZERO) { | ||
synchronized (this) { | ||
// need to check again in case init just happened | ||
localState = state; | ||
if (localState.type == StateType.ZERO) { |
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.
While I believe that this is fine (since the only State
with type set to ZERO
has all nulls, and so checking the type is equivalent to checking the whole object), the method checkAndGetDataSourcePool
performs a similar operation in the sense that it switches against the possible states that we can be in, and notably it compares the whole object vs just the type.
To err on the side of caution, can we follow the structure used there? In other words:
- use a switch statement
- compare
state
againstlocalState
directly
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 thought about that, but that is actually suboptimal (and would result in needing to write duplicate code). When you examine the state again (while being synchronized) if the state happens to be the normal state, you would actually want to have the same behavior as what is outside of the synchronized block). It would also mean that you potentially need to add a third volatile read instead of what is written here where it is guaranteed a max of two volatile memory reads.
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'd hope that the perf of this method isn't that critical (though now that you've written this more efficient impl it makes sense, of course)!
* <p>Hikari checks the username field for being null to determine if it needs to copy the JDBC parameters, so | ||
* the username *must* be set for it to respect the password change. | ||
*/ | ||
private void internalSetPassword(String newPassword, HikariConfig configOrPool) { |
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.
As above, I'm a little concerned about concurrency (but I might be being paranoid here).
@@ -141,6 +130,103 @@ public void testCloseIdempotent() throws SQLException { | |||
manager.close(); // shouldn't throw | |||
} | |||
|
|||
@Test | |||
public void testRuntimePasswordChange() throws SQLException { |
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.
From looking at the base class and the tests, it seems rather difficult to write a test that confirms that attempts to concurrently edit the password don't cause an issue; still, if at all possible, it'd be quite interesting to see.
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'm not sure that a test would be able to confirm correctness unless you somehow can control the execution order on different threads and iterate through all possible execution orders.
I think it's fine to have undefined behavior as the only caller to this should be a witchcraft Runtime subscription which is guaranteed to never have multiple concurrent calls.
If someone does end up having concurrent calls and ends up in an unexpected state, either the password is correct or not. If the password is not correct, all connections would fail and they'd probably need to restart the service (or have something which triggers calling setPassword again later). For this case I would argue the bug is in the service which made concurrent calls to the setPassword method.
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.
yeah, in sync here that trying to setPassword concurrently (as long as we're not doing it) shouldn't be allowed. Maybe we could stick a @NotThreadSafe
or some javadocs there noting that that's a bad idea?
Seems reasonable in light of what you've said - thanks for providing additional context around these methods and changes. I'll leave it to @jeremyk-91 for the +2 before we merge. Thanks! |
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.
👍
Broadly looks good. I'd suggest some minor docs changes, mainly to tell people more explicitly to avoid calling setPassword()
concurrently, but the impl looks solid.
private void internalSetPassword(String newPassword, HikariConfig configOrPool) { | ||
if (newPassword.equals(connConfig.getDbPassword().unmasked())) { | ||
configOrPool.setUsername(null); | ||
configOrPool.setPassword(null); |
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.
thanks for the comment above, that explains this part well
@@ -141,6 +130,103 @@ public void testCloseIdempotent() throws SQLException { | |||
manager.close(); // shouldn't throw | |||
} | |||
|
|||
@Test | |||
public void testRuntimePasswordChange() throws SQLException { |
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.
yeah, in sync here that trying to setPassword concurrently (as long as we're not doing it) shouldn't be allowed. Maybe we could stick a @NotThreadSafe
or some javadocs there noting that that's a bad idea?
* Change the password which will be used for new connections. Existing connections are not modified in any way. | ||
* | ||
* <p>Note that if the password is changed at runtime, there is a slight negative impact on pool performance | ||
* because Hikari will copy the JDBC parameters every time a new connection is made. |
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.
yeah let's say concurrent calls to this method = UB
if (localState.type == StateType.ZERO) { | ||
synchronized (this) { | ||
// need to check again in case init just happened | ||
localState = state; | ||
if (localState.type == StateType.ZERO) { |
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'd hope that the perf of this method isn't that critical (though now that you've written this more efficient impl it makes sense, of course)!
Released 0.309.3 |
Goals (and why): Many customers have password policies that require regular password rotation for Oracle database users (often every 60 days). Currently since the password is in static config, this requires restarting services that use AtlasDB or rely on the Hikari pool. Ideally we should support being able to change user passwords without requiring services restart.
Implementation Description (bullets):
Testing (What was existing testing like? What have you done to improve it?): Added tests against postgres. Also fixed the tests (HikariCpConnectionManagerTest was never being run because it was not in a suite, and the setup for that class was broken because it didn't actually wait for the database to be available which meant that even running it manually would have failed).
Concerns (what feedback would you like?):
Where should we start reviewing?:
Priority (whenever / two weeks / yesterday): normal