Skip to content

Commit

Permalink
Merge branch 'minor' into minor
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexanderGeere authored Nov 20, 2024
2 parents 4c1d60d + 632605d commit f8fa3bb
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 20 deletions.
2 changes: 2 additions & 0 deletions api/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ The res object represents the HTTP response that an [Express] app sends when it
@typedef {Object} env
The process.ENV object holds configuration provided to the node process from the launch environment. The environment configuration allows the provision of keys and secrets which must not be accessible from the client. All env properties are limited to string type.
@property {String} [DIR=''] The XYZ API path which concatenated with the domain for all requests.
@property {String} [DBS_=''] DBS_* values are the connections used to establish connections to pg servers with the [dbs]{@link module:/utils/dbs} module.
@property {String} [PORT='3000'] The port on which the express app listens to for requests.
@property {String} [COOKIE_TTL='36000'] The Time To Live for all cookies issued by the XYZ API.
@property {String} [TITLE='GEOLYTIX | XYZ'] The TITLE value is used to identify cookies and is provided to as a param to Application View templates.
Expand All @@ -59,6 +60,7 @@ The process.ENV object holds configuration provided to the node process from the
@property {String} [FAILED_ATTEMPTS='3'] The [user/fromACL module]{@link module:/user/fromACL} will expire user validation if failed login attempts exceed the FAILED_ATTEMPTS value.
@property {String} [PASSWORD_REGEXP='(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])^.{10,}$'] The [user/register module]{@link module:/user/register} will apply PASSWORD_REGEXP value to check the complexity of provided user passwords.
@property {String} [STATEMENT_TIMEOUT] The [utils/dbs module]{@link module:/utils/dbs} will apply the STATEMENT_TIMEOUT to the query.client.
@property {String} [RETRY_LIMIT='3'] The [utils/dbs module]{@link module:/utils/dbs} will apply the RETRY_LIMIT to the query.client.
@property {String} [WORKSPACE_AGE] The [workspace/cache module]{@link module:/mod/workspace/cache} flashes the workspace cache after the WORKSPACE_AGE is reached.
@property {String} [CUSTOM_TEMPLATES] The [workspace/cache module]{@link module:/mod/workspace/cache} caches templates defined as a src in the CUSTOM_TEMPLATES env.
@property {String} [TRANSPORT] The [utils/mailer module]{@link module:/utils/mailer} requires a TRANSPORT env.
Expand Down
127 changes: 109 additions & 18 deletions mod/utils/dbs.js
Original file line number Diff line number Diff line change
@@ -1,47 +1,138 @@
/**
@module /utils/dbs
@description
## /utils/dbs
Database connection and query management module that creates connection pools for multiple databases based on environment variables prefixed with 'DBS_'.
The [node-postgres]{@link https://www.npmjs.com/package/pg} package is required to create a [new connection Pool]{@link https://node-postgres.com/apis/pool} for DBS connections.
@requires pg
@requires /utils/logger
*/

const { Pool } = require('pg');

const logger = require('./logger');

const RETRY_LIMIT = process.env.RETRY_LIMIT ?? 3;

const INITIAL_RETRY_DELAY = 1000;

const dbs = {};

// Initialize database pools and create query functions
Object.keys(process.env)

// Filter keys which start with DBS
.filter(key => key.startsWith('DBS_'))

.forEach(key => {

const id = key.split('_')[1]

/**
@type {Pool} @private
*/
const pool = new Pool({
dbs: id,
connectionString: process.env[key],
keepAlive: true
keepAlive: true,
connectionTimeoutMillis: 5000, // 5 seconds
idleTimeoutMillis: 30000, // 30 seconds
max: 20 // Maximum number of clients in the pool
});

dbs[key.split('_')[1]] = async (query, variables, timeout) => {
// Handle pool errors
pool.on('error', (err, client) => {
logger({
err,
message: 'Unexpected error on idle client',
pool: id
});
});

// Assigning clientQuery method to dbs property.
dbs[id] = async (query, variables, timeout) =>
await clientQuery(pool, query, variables, timeout)
});

try {
// Export dbs constant
module.exports = dbs;

const client = await pool.connect()
/**
@function clientQuery
@async
@description
The clientQuery method creates a client connection from the provided Pool and executes a query on this pool.
@param {Pool} pool The node-postgres connection Pool for a Client connection.
@param {string} query SQL query to execute
@param {Array} [variables] Parameters for the SQL query
@param {number} [timeout] Statement timeout in milliseconds
@returns {Promise<Array|Error>} Query results or error object
@throws {Error} Database connection or query errors
*/
async function clientQuery(pool, query, variables, timeout) {

if (timeout || process.env.STATEMENT_TIMEOUT) {
await client.query(`SET statement_timeout = ${parseInt(timeout) || parseInt(process.env.STATEMENT_TIMEOUT)}`)
}
let retryCount = 0;
let lastError;
let client;

const { rows } = await client.query(query, variables)
while (retryCount < RETRY_LIMIT) {

client.release()
try {
client = await pool.connect();

return rows
timeout ??= process.env.STATEMENT_TIMEOUT

} catch (err) {
// Set statement timeout if specified
if (timeout) {

logger({ err, query, variables })
return err;
await client.query(`SET statement_timeout = ${parseInt(timeout)}`);
}

const { rows } = await client.query(query, variables);

return rows;

} catch (err) {

// Log the error with retry information
logger({
err,
query,
variables,
retry: retryCount + 1,
pool: pool.options.dbs
});

retryCount++;

if (retryCount < RETRY_LIMIT) {
// Exponential backoff
const delay = INITIAL_RETRY_DELAY * Math.pow(2, retryCount - 1);
await sleep(delay);
}

lastError = err

} finally {
if (client) {
client.release(true); // Force release in case of errors
}
}
})
}

// If we've exhausted all retries, return the last error
return lastError;
};

module.exports = dbs
/**
@function sleep
@description
Helper function to pause execution
@param {number} ms Time to sleep in milliseconds
@returns {Promise<void>}
*/
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"dependencies": {
"jsonwebtoken": "^9.0.2",
"nodemailer": "^6.9.7",
"pg": "^8.7.3",
"pg": "^8.13.1",
"simple-statistics": "^7.8.3"
},
"devDependencies": {
Expand All @@ -45,4 +45,4 @@
"nodemon": "^3.1.7",
"uhtml": "^3.1.0"
}
}
}

0 comments on commit f8fa3bb

Please sign in to comment.