retry-promise-mechanism v0.0.1
- 1. Official Documentation
- 2. Things that they have said that don't work for me
- 3. Usage
- 4. Motivation
- 5. Simple Explanation
- 6. Longer Explanation
- 7. Potential Solutions
- 8. Issue
- 9. Links describing the issue
- 10. Useful links
- 11. Extra things to check
- 12. Sample Run
There may be more I may not be considering. I will try to update this documentation periodically.
Install
npm i @toorieaa/retry-promise-mechanism
1. Official Documentation
The primary committer to node-mongodb-native says:
You open do MongoClient.connect once when your app boots up and reuse the db object. It's not a singleton connection pool each .connect creates a new connection pool. (reference : https://stackoverflow.com/questions/10656574/how-do-i-manage-mongodb-connections-in-a-node-js-web-application)
Official mongo db documentation https://mongodb.github.io/node-mongodb-native/1.4/driver-articles/mongoclient.html https://docs.mongodb.com/manual/administration/install-community/
2. Things that they have said that don't work for me
- If you can run the app after the mongo service is running, it means mongo is taking longer to start up and be connection-ready than when your app tries to connect. If your app doesn't ever connect it may not be restarting properly - you probably want it to exit if it doesn't connect and then make sure you have your Docker restart policy set accordingly ("always", "on-failure", etc.) see docs. Assuming mongo eventually starts up, your app will eventually connect.
- You can also look into using healthchecks in which case you could see if the database is actually connecting rather than starting up. The depends_on flag will only check to see that the container is up, it doesn't check to see if the apps in the container are running much less if they are working properly.
3. Usage
This module provides a method to retry any promise, and provide a callback which expects a result (the result of the mongo db connect promise). You should create an empty object to store your connection objects, and use the callback to reference this object and change the value of the connections key to the result. The callback will be called if the promise successfully resolves before the numberOfAttempts. Also note you can specify an exponential back of input as a true or valse value which will double the sleeping time on every failed attempt. You can also set numberOfAttempts to -1 so that the number of retries will be essentially infinite, and use it in combination with the exponential parameter flag. Was created for mongodb but can be used for other databases if the setup and problem is similar
const databasePoolStorage = {}; //use this in your requests, create object, call on method with param, export and import in the module you need
retryPromiseNTimes(
testPromise, //promise that returns a connection (pool) object
(result) => (databasePoolStorage.connection = result) //export databasePoolStorage, and use the value at property connection as the connection database (pool) connection object.
);
4. Motivation
Database pooling allows multithreaded/pseudo-multithreaded APIs efficient ways to use database connections. Rather than all tasks sharing a single database connection, or having the endpoints teardown and rebuild connections on every request, the session pool allows multiple session connections to a database to exist, and allow your express api / tasks to randomly access one of these session connections. This provides your application with a huge performance boost, because if each database connection is synchronized, each task will be bottle-necked by a single database connection. ATOMIC implementations, while more efficient than synchronized blocking, still impose a cost for synchronization. A volatile implementation would mean the database would have problems with consistency.
5. Simple Explanation
- The connect function promise for mongodb will return the database pool when provided correct option configurations to the API and is able to successfully connect once. The issue may become apparent when attempting to add reconnect parameters to the options object, only to realize that they rely on a successful first connect to a mongo database.
6. Longer Explanation
By default, mongoose throws an Error if the first connect fails, which crashes node.
So to reproduce this bug, you will need the following code in your app, to catch the error and prevent the crash:
db.on('error', console.error.bind(console, 'connection error:'));
Now we can reproduce this bug as follows:
- Shut down your MongoDB
- Start up your node app that uses mongoose
- Your app will log: [MongoError: failed to connect to server localhost:27017 on first connect MongoError: connect ECONNREFUSED 127.0.0.1:27017]
- Start up your MongoDB again
- Observe that mongoose does not now connect to the working MongoDB. The only way to get reconnected is to restart your app, or to use a manual workaround.
- Expected behaviour: Since autoreconnect defaults to true, I would expect mongoose to establish a connection soon after the MongoDB is accessible again.
Note: If the first connect succeeds, but the connection to MongoDB is lost during runtime, then autoreconnect works fine, as expected. The problem is the inconsistency if MongoDB is not available when the app starts up.
(If this is the desired behaviour, and developers are recommended to handle this situation by not catching the error, and letting node crash, then I can accept that, but it is worth making it clear.)
Users like fdmxfarhan who commented on Jan 3,2020 still requesting an easy to way to get around this issue.
7. Potential Solutions
- Potential Fix? Did not go deeper than looking at the solution. https://gist.github.com/asalant/4092454
- Import an async library to handle this like BlueBird. This can be overkill if all you need to provide is this specific functionality to get the pool configured and setup correctly.
- Read logs from mongo db periodically until you sniff logs that look like the database is up. This becomes difficult in a containerized environment, and the solution can tend to be clunky
8. Issue
If you start an express server and your mongo database is not ready, your first connect to mongo db will fail. Despite adding retry configuration parameters to the options object, you will realize that mongo will not auto retry to connect until it has successfully connected at least once.
8.1. Docker Issues
Despite specifying depends on on docker compose, the express service will start but will fail to connect due.
8.2. Mongo Specific
Across different database drivers, retry options work differently. This can make it hard to come up with a universal solution
9. Links describing the issue
- https://github.com/automattic/mongoose/issues/5169
- Potential Fix? Did not go deeper than looking at the solution. https://gist.github.com/asalant/4092454
- https://stackoverflow.com/questions/10656574/how-do-i-manage-mongodb-connections-in-a-node-js-web-application
- http://oak.cs.ucla.edu/classes/cs144/mongo/connection-pool.html
- 😢 The horror... https://stackoverflow.com/questions/24621940/how-to-properly-reuse-connection-to-mongodb-across-nodejs-application-and-module
- http://mongodb.github.io/node-mongodb-native/2.0/tutorials/connection_failures/
- https://team.goodeggs.com/reconnecting-to-mongodb-when-mongoose-connect-fails-at-startup-83ca8496ca02
- https://stackoverflow.com/questions/50472471/
- MongoClient failed to connect to server on first connect, but connects later mongoclient-failed-to-connect-to-server-on-first-connect-but-connects-later
- https://stackoverflow.com/questions/57608541/how-to-auto-reconnect-if-mongo-connection-fails-in-node-js-using-mongoose Have not tested this
10. Useful links
- https://www.compose.com/articles/connection-pooling-with-mongodb/
- https://github.com/docker/hub-feedback/issues/1255 Using links or health checks with docker compose
- https://github.com/mongo-express/mongo-express-docker/issues/35 Depends on should work but it does not for some as there is still a tiny delay before the database actually starts from when docker compose signals that the service is up to express
- https://dev.to/hugodias/wait-for-mongodb-to-start-on-docker-3h8b health check
- https://stackoverflow.com/questions/39785036/reliably-reconnect-to-mongodb Mentioning reconnect options which do not work if first connect fails on some driver versions of mongo
- https://medium.com/@patarkf/synchronize-your-asynchronous-code-using-javascripts-async-await-5f3fa5b1366d Working with async methods are notoriously difficult
- Checking logs with Docker https://stackoverflow.com/questions/22290143/mongodb-error-error-failed-to-connect-to-localhost27017
- https://www.youtube.com/watch?v=xgpGmi0EgcA Works but relies on a global mongo db image or a global installation somewhere, which may not be suitable. Difficult to manage. You will have to figure out how to fork the process and listen on different ports to manage multiple databases on the same computer. Not to mention that in a cloud environment, you may not have access to this configuration option which may provide no option or service to do this. If there is a configuration option or additional service, it may require hacky injection of code or managing another set of services altogether
- Production https://hackprogramming.com/how-to-configure-a-mongodb-connection-in-expressjs-for-production/
- Events for debugging https://stackoverflow.com/questions/16226472/mongoose-autoreconnect-option
11. Extra things to check
Application and Network Firewall, and anything strange in app preferences access. Check if you are on VPN or if you are not corrected to the proper wifi
Random command. May or may not help on mac
xattr -d com.apple.quarantine <exposed-alias-or-shellVisible>
12. Sample Run
> node index.js
🔄: 1
🙈RetryMechanism Waiting 3000 ms
🙈RetryMechanism Error: did not go well boss
at testPromise (file:///Users/lorainetoorie/Desktop/public%20npm%20modules%20by%20Anthony%20Toorie/Retry/index.js:13:9)
at async retryPromiseNTimes (file:///Users/lorainetoorie/Desktop/public%20npm%20modules%20by%20Anthony%20Toorie/Retry/index.js:84:22)
🔄: 2
🙈RetryMechanism Waiting 3000 ms
🙈RetryMechanism Error: did not go well boss
at testPromise (file:///Users/lorainetoorie/Desktop/public%20npm%20modules%20by%20Anthony%20Toorie/Retry/index.js:13:9)
at async retryPromiseNTimes (file:///Users/lorainetoorie/Desktop/public%20npm%20modules%20by%20Anthony%20Toorie/Retry/index.js:84:22)
🔄: 3
🙈RetryMechanism Waiting 3000 ms
🙈RetryMechanism Error: did not go well boss
at testPromise (file:///Users/lorainetoorie/Desktop/public%20npm%20modules%20by%20Anthony%20Toorie/Retry/index.js:13:9)
at async retryPromiseNTimes (file:///Users/lorainetoorie/Desktop/public%20npm%20modules%20by%20Anthony%20Toorie/Retry/index.js:84:22)
🔄: 4
🙈RetryMechanism Waiting 3000 ms
🙈RetryMechanism Error: did not go well boss
at testPromise (file:///Users/lorainetoorie/Desktop/public%20npm%20modules%20by%20Anthony%20Toorie/Retry/index.js:13:9)
at async retryPromiseNTimes (file:///Users/lorainetoorie/Desktop/public%20npm%20modules%20by%20Anthony%20Toorie/Retry/index.js:84:22)
🔄: 5
🙈RetryMechanism Waiting 3000 ms
🙈RetryMechanism Error: did not go well boss
at testPromise (file:///Users/lorainetoorie/Desktop/public%20npm%20modules%20by%20Anthony%20Toorie/Retry/index.js:13:9)
at async retryPromiseNTimes (file:///Users/lorainetoorie/Desktop/public%20npm%20modules%20by%20Anthony%20Toorie/Retry/index.js:84:22)
🔄: 6
🙈RetryMechanism Waiting 3000 ms
🙈RetryMechanism success
⏲: 18.026s