-
Notifications
You must be signed in to change notification settings - Fork 470
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
RFC Allow repeat calls to connect()
to resolve
#934
Comments
I think there's a further issue here. I think in your middle code example, the one mitigating the error thrown when the pool is already So one might try and fix that with: function runQuery(query) {
return Promise
.resolve(pool.connected ? pool : pool.connecting ? undefined : pool.connect())
// above, if the Pool was in 'connecting' state, resolves with an undefined value,
// otherwise resolves with the promise of a connected Pool,
.then(connectedPool => {
// thus, connectedPool can be undefined here (ie. Pool was in 'connecting' state) or
// an actual connected Pool (ie. Pool was not connected and also not in connecting state):
if (connectedPool) return connectedPool.request().query("SELECT 1"); // return query result.
return undefined; // return undefined because the Pool was not connected when we tried to query.
},
);
} But this ends up in a query that might not run. The developer would now have to write retry logic. I'm thinking of another solution which I'll post soon. |
Here's the other solution I was talking about. This one I actually tested. I'm using Typescript because the example is from a project of mine. It's not part of a let millisecondsPassed = 0;
// Basically let's write a recursive executor with a timeout
// and 'passed time' threshold so it doesn't recurse forever:
const waitForConnectionPoolToBeReadyExecutor = (
resolve: (value?: PromiseLike<undefined> | undefined) => void,
reject: (reason?: any) => void,
) => {
setTimeout(() => {
if (millisecondsPassed < 30000) {
millisecondsPassed += 2000;
// This variable defaults to false and is asynchronously set by the
// pool init callback set on the pool constructor:
// ie.: new(options, callback) where if not error, this._connectionPoolInitialized = true.
//
// Could as well be replaced with `this._connectionPool.connected`.
if (this._connectionPoolInitialized) {
resolve();
} else {
waitForConnectionPoolToBeReadyExecutor(resolve, reject);
}
} else
reject(
"Aborting start because the connection pool failed to initialize in the last ~30s.",
);
}, 2000);
};
// This simply now waits for like 30 seconds, for the pool to become connected.
// Since this is also an async function, if this fails after 30 seconds, execution stops
// and an error is thrown.
await new Promise(waitForConnectionPoolToBeReadyExecutor);
// use the pool: await this._connectionPool.request().query()... etc. |
By the way, I don't know if this can be fixed internally by simply delaying execution until the pool is connected and also triggering a connection attempt if it isn't, without throwing an error back to the user. It might change query execution order which might break some (if not eventually all) apps. Throwing an error (instant feedback) is actually good here. The best possible solution I see is handling errors in your app. For example, when you initialise your app and before running any queries, you can call This way you don't get any errors thrown because before calling You can then await that connection attempt, which according to the source code, creates just just one connection in the pool just for testing that the pool is correctly configured. In fact, this seems to be the only use case for the And then you can use the pool but that doesn't mean it can't become disconnected for some other reasons. So in the end, requests, queries, pools, etc., they will all inevitably throw one error or another . So then why not build retry logic / error handling logic inside your app? The requirement for that would be for you to be able to distinguish between the different errors that can be thrown so you can take different actions and that's already possible so... maybe we're using node-mssql wrong here? BTW, I just started using this last night, up till 2 AM! 😪 Sorry if I'm overly verbose. Maybe we don't have similar dilemmas after all. |
Yes - there is also the problem of when the pool is in a connecting state. Ideally the requests would be queued until the connection had been resolved. If we fired an event when the pool was connected, queued queries could then execute. The problem with "managing" it all in your app by calling As I imagine it, we could have some kind of promise that is held until the pool is connected: class ConnectionPool extends EventEmitter {
constructor () {
this._connecting = this._connected = false;
}
connect () {
if (!this._connectingPromise) {
this._connectingPromise = (new Promise((resolve, reject) => {
this.once('connected', resolve)
this.once('error', reject)
})).then(() => {
this.connected = true
this._connecting = false
return this
}).catch(err => {
this.connecting = false
return Promise.reject(err)
});
}
// kick off connection to db
return this._connectingPromise
}
close () {
this._connectingPromise = null
this._connecting = this.connected = false
// close pool
}
}
class Request {
constructor(pool) {
this.pool = pool || globalConnection.pool
}
query(sql) {
return this.pool.connect().then(pool => pool.query(sql))
}
} This is untested and just a rough approach, missing lots of details, but I'd imagine this would allow us to queue SQL queries up without having to worry about whether the pool has connected or not. |
I wasn't suggesting that you call So now that you know that you have a pool that can connect and more importantly has connected, all subsequent uses of the pool would go as normal, by which I mean no further calls to I really need some validation from the authors. I'm only just starting to test-implement some of the things I'm proposing. Going back to your example, it's pretty good but it might be missing some key domain details. A pool is connected if there's at least one connection in the pool (connected or disconnected). This is what I'm assuming. Your example provides some type of health-check that checks if the pool can create a connection before handing over a request to the developer. This has nothing to do with the actual pool, that might contain already connected connections or even ones that have disconnected and haven't been health-checked. In my opinion you can use the callback provided on pool creation to check if the newly created pool connected or not. You can wrap that in a promise and wait. You would do this once in the app/service init phase so you can fail early if the pool can't connect due to issues such as bad password or user name. No reason to spin up the app/service if will never be able to connect. And then the pool hands connections like normal. These connections live in the pool but can always become disconnected and need to be reconnected. It might happen with all or just one. That's why it can't handle these issues internally, I think. It can, for example, try to reconnect a broken connection, but that might fail so requests might fail as well due to that. In the end, you have to handle this situations yourself. |
@brokenthorn What you suggest in the first half of your post is basically what the library already does and doesn't require any changes to the library. On However, the reason I've proposed this change is because (going by the issues opened on GitHub) many developers seem to struggle with the concept of managing the pool themselves and don't understand why repeat calls to connect fail nor why or when they should close the pool, etc.
This is incorrect, a pool is connected if the initial probe connection was successful, there's no need for any connections to actually be in the pool at all, nor for them to be healthy. |
There are some problems to keep note of, though:
|
Actually, I don't think it's like that. The pool is created regardless of whether you call
Yes, I know, I've had the same problem for 2 days since starting to use this library but now I'm pretty sure I was using it wrong. I already described how I think the pool should be used so I won't repeat myself here.
I see! I need to check the source code again but it sounds familiar (it was very late last night but I seem to remember that logic now...). But if that's the case, that means that the way I previously described how I think the pool should be used is probably right. |
@brokenthorn with respect, I maintain this library so I know the internals of it pretty well. The "pool" as an object is created no matter what, but the underlying tarn js pool and probes to the database are not started until const pool = new sql.ConnectionPool(config)
pool.connected // false
pool.connecting // false
pool.healthy // false
const connPromise = pool.connect()
pool.connected // false
pool.connecting // true
pool.healthy // false
await connPromise
pool.connected // true
pool.connecting // false
pool.healthy // true The alternative is to use the callback pattern, and then const pool = new sql.ConnectionPool(config, (err => {
pool.connected // true
pool.connecting // false
pool.healthy // true
});
pool.connected // false
pool.connecting // true
pool.healthy // false
That's right, you accurately described how the pool should be used, but some developers struggle with this concept, and so allowing repeat calls to the DB would be helpful, it seems. Changing the pool to allow repeat calls wouldn't really change how most people use the pool, it would just allow developers to call |
That's all you had to say! I didn't know! 😨 I thought we were both just users. Now I understand you're point of view and why you opened this issue. I'm still inclined to write an MRE because I remember last night, while I was having trouble understanding how to use the library, that just by following the example for async/await from the docs, that the first call to The MRE is basically your first code example from the above reply. That call to BTW, thank you for taking your time and the respect is mutual. |
No problem. If you've got code to show for the instance where you were getting an "already connecting" error when calling connect, I could probably give some feedback as that shouldn't be happening if you're only calling I've opened a PR (#941) which settles the basic problem of calling const pool = new sql.ConnectionPool(config)
pool.connect() // note no await or then
pool.request().query('SELECT 1').then(result => {
console.log(result)
}).catch(err => {
console.error(err) // this will be called because connect hasn't resolved
}) The next step would be fore the requests to check if the pool is connected before continuing instead of throwing an error so you could call |
Shouldn't the docs and examples also be updated? |
It seems that there is a common use where developers would like to call
ConnectionPool.connect()
repeatedly to then chain on a request.For example:
At the moment, repeat calls to
connect()
in the above example will throw an error because the pool is already connected.This also has the advantage of allowing developers not to have to worry about whether the pool is connected or closed. At the moment, to achieve something similar you'd need to do the following:
This feels overly verbose and needless and should be simple to solve.
As an enhancement, we may wish to do something similar for when the pool is in a "connecting" state. At the moment if two process call
connect()
whilst the pool is connecting, we will see an error, which will mean that using thepool.connect().then()
pattern will be inherently unsafe for any parallel execution of queries on a pool that's not yet connected.It feels like we are relying on devs to do too much caretaking of the pool when the library should be able to abstract that away.
Potentially the final solution could just be a lazily invoked connection. eg:
developers can still call
connect()
to validate the connection is working but they wouldn't need to worry about theconnected
orconnecting
state of the pool at all.The text was updated successfully, but these errors were encountered: