Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
b526454
feat(parallel): per-VU iteration counter (changes 0-3, 6b prep)
scriptonist Jun 8, 2026
6a07dc3
feat(parallel): gate end-of-partition + cr block guards (changes 4, 5)
scriptonist Jun 8, 2026
87f706e
feat(parallel): full-fresh-on-reuse + late-write drop (changes 6, 6c)
scriptonist Jun 8, 2026
cf9c29e
feat(parallel): sentinel cycles + eof trigger normalization (changes …
scriptonist Jun 8, 2026
08d9b15
test(parallel): end-to-end integration tests for customParallelIterat…
scriptonist Jun 8, 2026
b0ec38a
fix(parallel): resolve lint errors and stale cookie-jar test mock
scriptonist Jul 22, 2026
2c4489c
fix(parallel): settle run completion callback in custom mode
scriptonist Jul 22, 2026
9189ecc
build(deps): vendor unreleased postman-sandbox with Infinity transform
scriptonist Jul 22, 2026
506bea4
build: exclude vendored sandbox from publish and exempt file: spec
scriptonist Jul 22, 2026
cba4338
fix: use published postman-sandbox@6.7.3 instead of the vendored per-…
saialekhya-001 Aug 4, 2026
3433ef2
feat(parallel): surface per-VU iteration data as pm.iterationData
saialekhya-001 Aug 4, 2026
91564d9
fix(parallel): seed per-VU _variables AND iterationData in custom mode
saialekhya-001 Aug 5, 2026
e457b1d
fix(parallel): isolate per-VU variables on slot reuse + generation-gu…
saialekhya-001 Aug 7, 2026
6e102b2
fix(lint): object spread over Object.assign + JSDoc @param generation
saialekhya-001 Aug 7, 2026
2557674
Merge branch 'develop' into feat/per-vu-iteration-data
saialekhya-001 Aug 11, 2026
94a83dc
Merge branch 'develop' into feat/per-vu-iteration-data
Kart03 Oct 21, 2026
54e3493
fix: Handed a nested runRequest the calling partition's data row (#1567)
Kart03 Oct 22, 2026
118bb3f
Merge remote-tracking branch 'origin/develop' into pr-1559
Kart03 Oct 22, 2026
da85c65
Merge remote-tracking branch 'origin/develop' into pr-1559
Kart03 Oct 22, 2026
e57e28d
Merge remote-tracking branch 'origin/develop' into pr-1559
Kart03 Oct 30, 2026
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
49 changes: 44 additions & 5 deletions lib/runner/extensions/event.command.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,34 @@ var _ = require('lodash'),
postProcessContext, // fn
sanitizeFiles, // fn
maskForbiddenSecrets, // fn
applySandboxCursorSentinel, // fn
applyScopeMutations; // fn

/**
* Replaces cursor.cycles with -1 when in customParallelIterations mode.
*
* Why -1 over the wire: the runtime sends cursor.cycles = Number.MAX_SAFE_INTEGER
* internally (so cursor.seek's bounds check accepts arbitrary iteration
* values), but exposing that to scripts would leak as
* pm.info.iterationCount === 9007199254740991. We use -1 as the wire
* sentinel because it survives JSON, structured-clone, and v8 IPC
* encodings intact — Infinity would be clobbered to null by
* JSON.stringify. The sandbox renders -1 as Infinity in pm.info.iterationCount

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Did we verify this?

* (postman-sandbox/lib/sandbox/pmapi.js).
*
* @param {Object} scriptCursor - cursor object about to be passed to host.execute
* @param {Boolean} isCustomParallelIterations - the Run-level gate
* @returns {Object} either a new object with cycles=-1, or the original
* reference if no transform applies
*/
applySandboxCursorSentinel = function (scriptCursor, isCustomParallelIterations) {
if (!isCustomParallelIterations) {
return scriptCursor;
}

return { ...scriptCursor, cycles: -1 };
};

/**
* Rebuilds a variable scope as the script left it, from the scope sent to the sandbox and the mutations the
* sandbox sent back. Used when the sandbox is asked (`variableMutationsOnly`) not to send the full scope back.
Expand Down Expand Up @@ -781,7 +807,7 @@ module.exports = {

// @todo: Expose this as a property in Collection SDK's Script
timeout: payload.scriptTimeout,
cursor: scriptCursor,
cursor: applySandboxCursorSentinel(scriptCursor, this.isCustomParallelIterations),
context: contextToUse,
resolvedPackages: resolvedPackages,

Expand Down Expand Up @@ -905,13 +931,22 @@ module.exports = {
result && result._variables &&
(payload.context._variables = new sdk.VariableScope(result._variables));

// persist the pm.variables for the next request
result && result._variables &&
// persist the pm.variables for the next request.
// In parallelized (per-VU) runs the run-global scope must stay the
// pristine baseline that new/recycled VUs clone from (via
// Partition#resetVariables -> _cloneVariables(state._variables));
// the live per-VU scope is persisted through updatePartitionVariables
// below. Writing each VU's mutations here would leak them across VU
// reuse and race between concurrent VUs, so skip it in parallel mode.
result && result._variables && !this.areIterationsParallelized &&
(this.state._variables = new sdk.VariableScope(result._variables));

if (this.areIterationsParallelized) {
// persist the pm.variables for the next request in the current partition
this.partitionManager.updatePartitionVariables(payload.coords.partitionIndex, result);
// persist the pm.variables for the next request in the current partition.
// Pass the generation this execution was queued under so a late
// completion from a reused slot's prior occupant is dropped.
this.partitionManager.updatePartitionVariables(payload.coords.partitionIndex, result,
payload.coords.partitionGeneration);
}

// persist the mutated request in payload context,
Expand Down Expand Up @@ -961,3 +996,7 @@ module.exports = {
}
}
};

// Exposed for unit tests — see test/unit/custom-parallel-iterations.test.js.
// Not part of the public runtime API.
module.exports._applySandboxCursorSentinel = applySandboxCursorSentinel;
52 changes: 45 additions & 7 deletions lib/runner/extensions/parallel.command.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,32 +99,70 @@ module.exports = {
// since we will never reach coords.eof for some partitions because each cursor
// contains cycles for the entire run, we are breaking off early here.
// this has been done to keep the contract of a cursor intact.
// cycles is defined as "number of iterations in the run"
if (coords.iteration === partition.startIndex + coords.partitionCycles) {
// cycles is defined as "number of iterations in the run".
//
// Gated on !isCustomParallelIterations: under custom mode the partition's
// cursor.iteration is the VU's per-loop counter (always equal to
// startIndex+partitionCycles on loop 2+ given startIndex=0, partitionCycles=1),
// so this guard would short-circuit every loop after the first and skip all
// items. The host (perftest) drives loop boundaries, not the runtime.
if (!this.isCustomParallelIterations &&
coords.iteration === partition.startIndex + coords.partitionCycles) {
this.triggers.iteration(null, payload.coords);

return next();
}

if (coords.cr) {
delay = _.get(this.options, 'delay.iteration', 0);

this.triggers.iteration(null, payload.coords);

// Custom mode: the host (perftest) is the loop driver. After
// firing the iteration trigger so the host can decide what to
// do next (continue, addUsers, removeUsers, abort), return
// and let the host queue the next iteration explicitly. Do
// NOT fire beforeIteration here — startParallelIteration's
// own parallel-command invocation carries start:true and
// fires it.
if (this.isCustomParallelIterations) {
return next();
}

this.triggers.beforeIteration(null, coords);
delay = _.get(this.options, 'delay.iteration', 0);
}


if (coords.eof) {
this.triggers.iteration(null, coords);
// Change 9: in customParallelIterations mode the iteration
// trigger carries "loop just completed" coords
// (payload.coords, pre-rollover) so the host can attribute
// the just-finished work to the correct iteration N. In
// runtime-managed mode (Newman/Postman desktop) the eof
// site has historically fired with the post-rollover
// snapshot — preserve that to avoid silent regressions in
// downstream subscribers we can't audit from here.
this.triggers.iteration(null,
this.isCustomParallelIterations ? payload.coords : coords);

return next();
}

this.queueDelay(function () {
this.queue('item', {
item: item,
coords: coords,
data: getIterationData(this.state.data, coords.iteration + partition.startIndex),
// Stamp the partition's current generation so a late completion
// from a prior occupant of a reused slot is dropped by
// PartitionManager#updatePartitionVariables (custom mode only).
coords: this.isCustomParallelIterations ?
{ ...coords, partitionGeneration: partition.generation } :
coords,
// customParallelIterations: the host injects each VU's data row
// via startParallelIteration -> partition.iterationData. Surface it
// as `data` so the sandbox exposes it as pm.iterationData (and layers
// it into _variables, so {{key}} / pm.variables.get() keep working).
data: this.isCustomParallelIterations ?
partition.iterationData :
getIterationData(this.state.data, coords.iteration + partition.startIndex),
environment: partition.variables.environment,
globals: partition.variables.globals,
vaultSecrets: this.state.vaultSecrets,
Expand Down
35 changes: 27 additions & 8 deletions lib/runner/nested-request.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,31 @@ function createNestedRequestChain (run, cursor, item) {
};
}

/**
* Finds the iteration data row of the iteration that a nested request was invoked from.
*
* @param {Run} run - the run the nested request was invoked from
* @param {Object} nestedRequest - the pm.execution.runRequest chain of the top-level script
* @returns {Object}
*/
function getParentIterationRow (run, nestedRequest) {
// A streaming source must never be passed down: the child would iterate the same object,
// stealing the parent's rows, and release the shared cursor on teardown
// while the parent still has iterations to run.
if (run.state._dataIterator) {
return run.state._dataRow;
}

// customParallelIterations has no run-level data, each partition holds the row its host injected
if (run.isCustomParallelIterations) {
return _.get(run, ['partitionManager', 'partitions', nestedRequest.rootCursor.partitionIndex,
'iterationData'], {});
}

return getIterationData(_.isEmpty(run.state.data) ? [{}] : run.state.data,
nestedRequest.rootCursor.iteration);
}

function runNestedRequest ({ executionId, isExecutionSkipped, vaultSecrets, item, nestedRequestScope }) {
// Note: Don't forget to bind this function to the runner to make sure `this` can give you the right options & state
return function (cursor, eventId, requestId, requestToRunId, runRequestOptions = {}, runContext = {}) {
Expand Down Expand Up @@ -132,14 +157,8 @@ function runNestedRequest ({ executionId, isExecutionSkipped, vaultSecrets, item
mergedCollectionVariableList = new sdk.VariableList(),

// A nested request sees the row of the iteration it was invoked from, so
// resolve it here and hand the child a one-row array. A streaming source
// must never be passed down: the child would iterate the same object,
// stealing the parent's rows, and release the shared cursor on teardown
// while the parent still has iterations to run.
parentIterationRow = self.state._dataIterator ?
self.state._dataRow :
getIterationData(_.isEmpty(self.state.data) ? [{}] : self.state.data,
nestedRequest.rootCursor.iteration),
// resolve it here and hand the child a one-row array.
parentIterationRow = getParentIterationRow(self, nestedRequest),

// A partitioned run hands its nested requests the calling partition's cookies: its own jar when
// per-partition jars are on, else the run's shared jar. An explicit jar also turns off per-partition
Expand Down
Loading
Loading