Skip to content
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

Connection config provider #3403

Closed
wants to merge 22 commits into from
Closed

Connection config provider #3403

wants to merge 22 commits into from

Conversation

Ali-Dalal
Copy link

As requested in this PR #3364
@elhigu @kibertoad Can you please check? if the changes are ok then I will add unit test and update the rest of the dialects

lib/client.js Outdated Show resolved Hide resolved
lib/connection.js Outdated Show resolved Hide resolved
lib/client.js Outdated Show resolved Hide resolved
Copy link
Collaborator

@kibertoad kibertoad left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comments.

lib/client.js Outdated Show resolved Hide resolved
class ConnectionConfigProvider {
constructor(client) {
this.client = client;
this.resolvedConnectionSettings = null;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not resolve settings at this step already? At the very least we can definitely do that if it's a static config.


async resolveConnectionConfig() {
if (this.resolvedConnectionSettings) return this.resolvedConnectionSettings;
if (isFunction(this.client.config.connection)) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can do the check once in constructor and set some flag after that.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you give more details about this, please?

if we need to manage this in ConnectionConfigProvider constructor, the how-to manipulate the promise as the type of the function is async?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I mean is this:

  1. In constructor check if it's a function. If it is - set this.isConfigSourceFunction = true and do nothing else. If it is not - set this.isConfigSourceFunction = false and this.resolvedConnectionSettings = cloneDeep <...>

  2. in resolveConnectionConfig check if this.resolvedConnectionSettings is set, if it is, return, if it is not and isConfigSourceFunction, resolve from function, if neither, probably throw an error.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this line in ConnectionConfigProvider class does the same, right?

  async resolveConnectionConfig() {
    if (this.resolvedConnectionSettings) return this.resolvedConnectionSettings;

the lines after check for the type of the connection. if it is a function resolve it, other wise, deepclone it.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partially. What I'm saying is that everything that can be moved from resolveConnectionConfig to constructor should ideally be moved, resolveConnectionConfig should be as simple as possible.
Current implementation is different from what I've described.

const settings = Object.assign({}, this.connectionSettings);
const client = this;
return new Bluebird(async (resolver, rejecter) => {
const settings = await client
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason why we might want to set connectionConfigProvider each time for a new connection anew and not just set it once on a client when it is created?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in ConnectionConfigProvider class I processed config.connection resolve the connection settings for only one time. So calling

const settings = await client
        .connectionConfigProvider()
        .resolveConnectionConfig();

will return the processed object direclty without processing anything anymore.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will it?

  connectionConfigProvider() {
    return new ConnectionConfigProvider(this);
  },

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any recommendation?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I've said, you can call connectionConfigProvider once after client is created and configuration is known. And probably it should be renamed to initConnectionConfigProvider to emphasise it mutates the state.

const settings = await client
.connectionConfigProvider()
.resolveConnectionConfig();
// const settings = Object.assign({}, this.connectionSettings);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this needed?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it is needed. if the config is passed as an async function, then this will cause a problem without resolving the config.

Also, this line

const settings = Object.assign({}, this.connectionSettings);

is not needed any more as ConnectionConfigProvider is taking the responsibility for this.

I will clarify the need for this PR again in case I miss any clarification.

The main purpose is we need to fetch the config of the DB from an async store (AWS SSM as an example) and this requires passing the config as an async function in order to wait for the config to be loaded from the promise.

Travis 1st stage passes but the other stages failed and I believe it is because of oracledb dialect which fails only in one of the test cases. Can you have a look please and help?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please delete the unneeded commented out code from PR?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re: Oracle

this seems directly related:

1) Oracledb driver tests
       OracleDb externalAuth
         externalAuth and connectString should be sent to the getConnection:
     AssertionError: expected getConnection to have been called exactly once, but it was called 0 times
      at Context.<anonymous> (test/unit/dialects/oracledb.js:36:25)

I believe you'd need to update some of mocks in oracle tests, as call hierarchy has obviously changed.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re: Oracledb

I believe the issue is because of switching the bludbird callback to async in line 79

const asyncConnection = new Bluebird(async function(resolver, rejecter) {
//...res of the code
}

where the linter is showing an error at line 105

    client.driver.fetchAsString = client.fetchAsString;

error:
possible race condition client.driver.fetchAsString might be reassinged based on an outdated value of client.driver.fetchAsString

need to debug the code but the issue I can't get the tests working on my machine due to insufficient storge issue (mac 128 GB)

any ideas?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume this is still relevant?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kibertoad I've checked the tests. only Oracle dialect fails. and honestly, I have no idea why. can you please help? I will do one more round to try to find the problem

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Ali-Dalal

externalAuth and connectString should be sent to the getConnection

add await in test

-    knexInstance.client
+    await knexInstance.client
      .acquireRawConnection()
      .then(function(resolve) {}, function(reject) {});

lib/client.js Outdated Show resolved Hide resolved
@kibertoad
Copy link
Collaborator

@Ali-Dalal Do you need any help for finishing the PR?

@Ali-Dalal
Copy link
Author

Ali-Dalal commented Oct 15, 2019

@kibertoad Yes please.

I need clarification on this point

in resolveConnectionConfig check if this.resolvedConnectionSettings is set, if it is, return, if it is not and isConfigSourceFunction, resolve from function, if neither, probably throw an error.

Do we need to resolve the config if it is a function inside the constructor? remember the type of the function is async

I am a bit confused because how to resolve connectionSettings in client.js since it is async?
in a different word, where is the best place to call
ConnectionConfigProvider.resolveConnectionConfig()
currently, I am calling this line in each dialect

  acquireRawConnection() {
    const client = this;
    return new Bluebird(async function(resolver, rejecter) {
      const connectionSettings = await client
        .connectionConfigProvider()
        .resolveConnectionConfig();

and I agree with you, the dialect is not responsible for processing or taking care of the connection config. But the question, how to place this code inside client.js?

@kibertoad
Copy link
Collaborator

@Ali-Dalal As I've said before, we can't resolve config from function since it could be async. We could do it if it's a static config, though, but disregard this part, we can refactor it later. Important part at this point is to finalize the implementation :).

@kibertoad
Copy link
Collaborator

@maximelkin Any chance you could take a look at the Oracle problem to see what is wrong here? I don't have readily available instance nor configured Docker to test easily myself, unfortunately.

@kibertoad kibertoad closed this Oct 15, 2019
@kibertoad kibertoad reopened this Oct 15, 2019
@kibertoad
Copy link
Collaborator

@Ali-Dalal Can you pull changes from master, btw? I wonder if latest fixes in master would resolve the issue.

// If external authentication dont have to worry about username/password and
// if not need to set the username and password
const oracleDbConfig = client.connectionSettings.externalAuth
? { externalAuth: client.connectionSettings.externalAuth }
const connectionSettings = await client
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do not use async inside new promise callback
it will not catch errors, so you get unhandledRejection

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Ali-Dalal This needs to be addressed

@maximelkin
Copy link
Collaborator

eslint/eslint#11899
just mark rule require-atomic-updates as ignored, it will be removed shortly due to false-positives

@Ali-Dalal
Copy link
Author

@kibertoad @maximelkin guys can you help in the last error from eslint?

check travis for more information

106:5 error Possible race condition: client.driver.fetchAsStringmight be reassigned based on an outdated value ofclient.driver.fetchAsString require-atomic-updates

@kibertoad
Copy link
Collaborator

@Ali-Dalal as @maximelkin has mentioned, "just mark rule require-atomic-updates as ignored, it will be removed shortly due to false-positives*

@Ali-Dalal
Copy link
Author

@kibertoad @maximelkin done.

can you guide me where is the best place to write a unit test for testing async config provider?

@Ali-Dalal
Copy link
Author

@kibertoad I will write unit tests under unit/dialects for every one

const client = this;
return new Bluebird(async (resolver, rejecter) => {
const settings = await client
.connectionConfigProvider()
Copy link
Collaborator

@kibertoad kibertoad Oct 16, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So in this line we are still always initializing a new ConnectionConfigProvider, meaning we will resolve credentials again on each connection.
Can we adjust connectionConfigProvider() method to lazy-instantiate and not reset each time?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a new knex instance defined, a new client is attached and a new ConnectionConfoProvider as well and once we call resolve connection, for the first time it will execute the function if the connection is a function then it will mutate the state of the client and set the connection to the resolved one.

I tried it on my machine and the connection is resolved only one time and for the 2nd time it loads the config from

async resolveConnectionConfig() {
    if (this.resolvedConnectionSettings) return this.resolvedConnectionSettings;

https://github.com/knex/knex/pull/3403/files#diff-cbc1ae6887774cf34c8d4bc73dece12fR10

Can we adjust connectionConfigProvider() method to lazy-instantiate and not reset each time?

can you give some examples how I can solve this?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you are seeing connection being resolved only once because it is being reused from the connection pool and not recreated on every request - for new connections it is likely that same acquireRawConnection will be called again and you will have to resolve credentials again.

I'll take a swing at the branch sometime this week to see how to improve the situation.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Easy way to check with multiple connections is to create transactions and never commit / rollback them. In that way the old connections cannot be reused from pool.

Copy link
Contributor

@oranoran oranoran Oct 24, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Guys, sorry for coming in late in the process, but I noticed this PR when I was considering opening a similar issue, with the use case in mind being support of Amazon RDS IAM authentication.

For this use case though, the resolved configuration can't be cached indefinitely, because the password needs to be changed every 15 minutes.

How about adding an optional field to the returned configuration object, with a sync function determining whether or not the cached config can be reused?

So the connection config provider code can look something like this:

const { token, tokenExpiration } = await someCallToGetTheToken();
return {
    host : 'your_host',
    user : 'your_database_user',
    password : token,
    database : 'myapp_test',
    expired = () => {
        return tokenExpiration <= Date.now();
    }
}

expired is a synchronous function, returning a boolean determining whether the configuration has expired or or not. If it hasn't expired, it can be reused. If not specified, the previously-resolved configuration is always reused.

What do you think?

@kibertoad
Copy link
Collaborator

Superseded by #3497

@kibertoad kibertoad closed this Oct 27, 2019
@Ali-Dalal Ali-Dalal deleted the connection-config-provider branch October 29, 2019 02:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants