postman-runtime
Version:
Underlying library of executing Postman Collections
325 lines (266 loc) • 10.5 kB
JavaScript
var Partition = require('./partition');
var sdk = require('postman-collection');
class PartitionManager {
constructor (runInstance) {
this.runInstance = runInstance;
}
spawn () {
this.partitions = [];
this.stopActionTriggered = false;
// we need at least one pool to start with.
// this is the pool that will be used to process the control instruction
// before we start partitioning (for abort, etc)
this.priorityPartition = this._getSinglePartition();
}
createPartitions () {
this.options = this.runInstance.options;
this.state = this.runInstance.state;
this.processingPriority = false;
this.priorityLock = false;
let { iterationCount, maxConcurrency } = this.options,
concurrency = maxConcurrency || 1,
cyclesPerPartition = Math.floor(iterationCount / concurrency),
remainingCycles = iterationCount % concurrency,
startIteration = 0; // the iteration that this partition will start with.
if (concurrency > iterationCount) {
concurrency = iterationCount;
}
// make sure we are starting afresh
this.reset();
// if customParallelIterations is true, then do not create partitions by default
if (this.options?.customParallelIterations) {
return;
}
for (let i = 0; i < concurrency; i++) {
let partitionSize = cyclesPerPartition + (i < remainingCycles ? 1 : 0);
if (partitionSize <= 0) { continue; }
// create a partition for each concurrency
this.partitions.push(this._getSinglePartition(startIteration, partitionSize, i));
startIteration += partitionSize;
}
}
_getSinglePartition (startIteration = 0, partitionSize = 1, partitionIndex = 0) {
return new Partition(this.runInstance, startIteration, partitionSize, partitionIndex);
}
/**
* @private
*
* @param {String} action -
* @param {Object} [payload] -
* @param {Array} [args] -
* @param {Boolean} [immediate] -
*/
schedule (action, payload, args, immediate) {
const coords = payload?.coords || payload?.cursor,
partitionIndex = coords?.partitionIndex;
// if the partition index is not set, we are in the priority partition.
if (action === 'abort' && this.options?.customParallelIterations) {
// eslint-disable-next-line
const instructions = this.partitions.filter((partition) => partition.hasInstructions());
// when no partition has any instructions, then we can trigger the done trigger immediately
if (instructions.length === 0) {
this.triggerStopAction();
}
}
if (immediate) {
return this.priorityPartition.schedule(action, payload, args);
}
return this.partitions[partitionIndex].schedule(action, payload, args);
}
_processPartition (partition, done) {
// If we're already processing priority items elsewhere, wait
if (this.priorityLock && partition !== this.priorityPartition) {
// Use setTimeout to recheck later without blocking
return setTimeout(() => {
this._processPartition(partition, done);
}, 10);
}
// Check if priority partition has items and we're not already processing it
if (this.priorityPartition &&
this.priorityPartition.hasInstructions() &&
partition !== this.priorityPartition &&
!this.processingPriority) {
// Set flag that we're processing priority items
this.processingPriority = true;
this.priorityLock = true;
return this._processPartition(this.priorityPartition, (err) => {
// Reset flag when done with priority items
this.processingPriority = false;
this.priorityLock = false;
if (err) {
return done(err);
}
// Continue with original partition
return this._processPartition(partition, done);
});
}
// Regular processing logic
var instruction = partition.nextInstruction();
if (!instruction) {
return done();
}
instruction.execute((err) => {
return err ? done(err) : this._processPartition(partition, done);
}, this.runInstance);
}
process (callback) {
if (this.runInstance.aborted) {
return callback();
}
let remainingPools = this.partitions.length,
completed = false;
const poolFinished = (err) => {
if (completed) {
return;
}
// If run has been aborted, complete immediately
if (this.runInstance.aborted) {
completed = true;
this.runInstance.host && this.runInstance.host.dispose();
return callback(null);
}
if (err) {
completed = true;
return callback(err);
}
remainingPools--;
if (remainingPools === 0) {
completed = true;
this.runInstance.host && this.runInstance.host.dispose();
return callback(null);
}
};
// First check if priority partition has items
if (this.priorityPartition && this.priorityPartition.hasInstructions()) {
// If yes, set the lock and process it first
this.priorityLock = true;
this._processPartition(this.priorityPartition, (err) => {
this.priorityLock = false;
if (err) {
return callback(err, this.state.cursor.current());
}
// if custom parallel iterations is true, then do not process other partitions
if (!this.options.customParallelIterations) {
// After priority is done, start processing other partitions
for (let i = 0; i < this.partitions.length; i++) {
this._processPartition(this.partitions[i], poolFinished);
}
}
});
}
else if (!this.options.customParallelIterations) {
// If no priority items initially, start all partitions
for (let i = 0; i < this.partitions.length; i++) {
this._processPartition(this.partitions[i], poolFinished);
}
}
}
/**
* Resets all partitions state
*/
reset () {
this.partitions = [];
}
/**
* Gets the total number of partitions
*
* @returns {Number} Total partition count
*/
getTotalPartitions () {
return this.partitions.length;
}
/**
* Clears all partition pools.
*/
dispose () {
// only dispose if partitions exist (i.e., spawn() was called)
if (this.partitions) {
this.partitions.forEach((partition) => {
partition.clearPool();
});
this.triggerStopAction();
}
}
/**
* Creates a single partition
* @param {Number} index - The index of the partition to create
* @returns {Partition}
*/
createSinglePartition (index) {
const START_ITERATION = 0,
PARTITION_SIZE = 1,
partition = this._getSinglePartition(START_ITERATION, PARTITION_SIZE, index);
this.partitions.push(partition);
return partition;
}
/**
* Runs a single iteration
* @param {Number} index - The index of the partition to run
* @param {Object} localVariables - Local variables for the iteration
* @param {Function} callback - The callback to call when the iteration is complete
*/
runSinglePartition (index, localVariables, callback) {
let partition;
// if the partition exists, use it, else create a new one
if (this.partitions[index]) {
partition = this.partitions[index];
// if partition is already has instructions donot do anything
if (partition.hasInstructions()) {
return callback(null);
}
}
else {
partition = this.createSinglePartition(index);
}
// Always use iteration 0 since we only have 1 iteration of data.
// and start from the 0th request position.
partition.cursor.seek(0, 0);
if (localVariables) {
partition.variables._variables = localVariables;
}
this.runInstance.queue('parallel', {
coords: partition.cursor.current(),
static: true,
start: true
});
this._processPartition(partition, callback);
}
/**
* Stops a single iteration
* @param {Number} index - The index of the partition to stop
* @param {Function} callback - The callback to call when the iteration is complete
*/
stopSinglePartition (index, callback) {
const partition = this.partitions[index];
if (partition) {
partition.clearPool();
}
return callback ? callback(null) : null;
}
/**
* Stops all iterations
*/
triggerStopAction () {
if (this.stopActionTriggered) {
return; // Prevent multiple calls
}
if (this.options && this.options.customParallelIterations && this.runInstance.triggers) {
this.stopActionTriggered = true;
this.runInstance.triggers(null);
}
}
/**
* Updates the variables for a specific partition
* Used to persist pm.variables for the next request in the current iteration
* when iterations are parallelized
*
* @param {Number} partitionIndex - The index of the partition to update
* @param {Object} result - The variables to update
*/
updatePartitionVariables (partitionIndex, result) {
if (this.partitions[partitionIndex] && result && result._variables) {
this.partitions[partitionIndex].variables._variables = new sdk.VariableScope(result._variables);
}
}
}
module.exports = PartitionManager;