Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 137 additions & 0 deletions benchmark/MongoLatencyWrapper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/**
* MongoDB Latency Wrapper
*
* Utility to inject artificial latency into MongoDB operations for performance testing.
* This wrapper temporarily wraps MongoDB Collection methods to add delays before
* database operations execute.
*
* Usage:
* const { wrapMongoDBWithLatency } = require('./MongoLatencyWrapper');
*
* // Before initializing Parse Server
* const unwrap = wrapMongoDBWithLatency(10); // 10ms delay
*
* // ... run benchmarks ...
*
* // Cleanup when done
* unwrap();
*/

const { Collection } = require('mongodb');

// Store original methods for restoration
const originalMethods = new Map();

/**
* Wrap a Collection method to add artificial latency
* @param {string} methodName - Name of the method to wrap
* @param {number} latencyMs - Delay in milliseconds
*/
function wrapMethod(methodName, latencyMs) {
if (!originalMethods.has(methodName)) {
originalMethods.set(methodName, Collection.prototype[methodName]);
}

const originalMethod = originalMethods.get(methodName);

Collection.prototype[methodName] = function (...args) {
// For methods that return cursors (like find, aggregate), we need to delay the execution
// but still return a cursor-like object
const result = originalMethod.apply(this, args);

// Check if result has cursor methods (toArray, forEach, etc.)
if (result && typeof result.toArray === 'function') {
// Wrap cursor methods that actually execute the query
const originalToArray = result.toArray.bind(result);
result.toArray = function() {
// Wait for the original promise to settle, then delay the result
return originalToArray().then(
value => new Promise(resolve => setTimeout(() => resolve(value), latencyMs)),
error => new Promise((_, reject) => setTimeout(() => reject(error), latencyMs))
);
};
return result;
}

// For promise-returning methods, wrap the promise with delay
if (result && typeof result.then === 'function') {
// Wait for the original promise to settle, then delay the result
return result.then(
value => new Promise(resolve => setTimeout(() => resolve(value), latencyMs)),
error => new Promise((_, reject) => setTimeout(() => reject(error), latencyMs))
);
}

// For synchronous methods, just add delay
return new Promise((resolve) => {
setTimeout(() => {
resolve(result);
}, latencyMs);
});
};
}
Comment on lines +30 to +72
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Multiple invocations will stack latency wrappers.

The wrapMethod function stores the original method only on the first invocation (line 31-33), but it unconditionally replaces Collection.prototype[methodName] on line 37. If wrapMongoDBWithLatency is called multiple times without calling unwrap() in between, the second invocation will wrap an already-wrapped method, causing latency to be applied multiple times (e.g., 2× latency on second call, 3× on third call).

Additionally, the cursor-wrapping logic only instruments toArray() (lines 45-52), but MongoDB cursors expose many other execution methods such as forEach, next, hasNext, map, and close. Operations using these methods will bypass the artificial latency, leading to inconsistent benchmark behavior.

Consider these fixes:

  1. Prevent multiple wrapping: Check if the method is already wrapped before applying a new wrapper, or throw an error if originalMethods already contains the method.

  2. Expand cursor coverage: Wrap all cursor execution methods, or document that only toArray() is supported and advise benchmark code to use it exclusively.

Apply this diff to prevent stacking:

 function wrapMethod(methodName, latencyMs) {
+  // Prevent wrapping an already-wrapped method
+  if (Collection.prototype[methodName].__isLatencyWrapped) {
+    throw new Error(`Method ${methodName} is already wrapped. Call unwrap() first.`);
+  }
+
   if (!originalMethods.has(methodName)) {
     originalMethods.set(methodName, Collection.prototype[methodName]);
   }

   const originalMethod = originalMethods.get(methodName);

   Collection.prototype[methodName] = function (...args) {
     // ... existing wrapper code ...
   };
+  
+  // Mark the method as wrapped
+  Collection.prototype[methodName].__isLatencyWrapped = true;
 }

And update the unwrap logic:

     originalMethods.forEach((originalMethod, methodName) => {
       Collection.prototype[methodName] = originalMethod;
+      delete Collection.prototype[methodName].__isLatencyWrapped;
     });

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In benchmark/MongoLatencyWrapper.js around lines 30 to 72, the wrapper
unconditionally replaces Collection.prototype[methodName] causing stacked
latency on repeated invocations and only instruments cursor.toArray(), missing
other execution methods; fix by: 1) before replacing, check if originalMethods
already has methodName and if so skip wrapping (or throw) to prevent
double-wrapping; store the original only once and avoid reassigning a wrapper
over a wrapper; 2) when wrapping cursor results, wrap all execution methods (at
minimum toArray, forEach, next, hasNext, map, close and any method with function
type) by saving their originals (bound) and replacing them with promises that
delay resolution/rejection by latencyMs; 3) ensure unwrap() restores the exact
originals saved in originalMethods and removes entries so later re-wraps behave
correctly.


/**
* Wrap MongoDB Collection methods with artificial latency
* @param {number} latencyMs - Delay in milliseconds to inject before each operation
* @returns {Function} unwrap - Function to restore original methods
*/
function wrapMongoDBWithLatency(latencyMs) {
if (typeof latencyMs !== 'number' || latencyMs < 0) {
throw new Error('latencyMs must be a non-negative number');
}

if (latencyMs === 0) {
// eslint-disable-next-line no-console
console.log('Latency is 0ms, skipping MongoDB wrapping');
return () => {}; // No-op unwrap function
}

// eslint-disable-next-line no-console
console.log(`Wrapping MongoDB operations with ${latencyMs}ms artificial latency`);

// List of MongoDB Collection methods to wrap
const methodsToWrap = [
'find',
'findOne',
'countDocuments',
'estimatedDocumentCount',
'distinct',
'aggregate',
'insertOne',
'insertMany',
'updateOne',
'updateMany',
'replaceOne',
'deleteOne',
'deleteMany',
'findOneAndUpdate',
'findOneAndReplace',
'findOneAndDelete',
'createIndex',
'createIndexes',
'dropIndex',
'dropIndexes',
'drop',
];

methodsToWrap.forEach(methodName => {
wrapMethod(methodName, latencyMs);
});

// Return unwrap function to restore original methods
return function unwrap() {
// eslint-disable-next-line no-console
console.log('Removing MongoDB latency wrapper, restoring original methods');

originalMethods.forEach((originalMethod, methodName) => {
Collection.prototype[methodName] = originalMethod;
});

originalMethods.clear();
};
}

module.exports = {
wrapMongoDBWithLatency,
};
Loading
Loading