Implement ForEach-Object -Parallel feature (#10229)

This commit is contained in:
Paul Higinbotham
2019-08-13 13:40:58 -07:00
committed by Aditya Patwardhan
parent ccc791c0a3
commit fbfc57a00c
9 changed files with 2492 additions and 384 deletions
@@ -108,6 +108,9 @@ namespace System.Management.Automation
new ExperimentalFeature(
name: "PSCommandNotFoundSuggestion",
description: "Recommend potential commands based on fuzzy search on a CommandNotFoundException"),
new ExperimentalFeature(
name: "PSForEachObjectParallel",
description: "New parameter set for ForEach-Object to run script blocks in parallel")
};
EngineExperimentalFeatures = new ReadOnlyCollection<ExperimentalFeature>(engineFeatures);
File diff suppressed because it is too large Load Diff
@@ -230,13 +230,21 @@ namespace System.Management.Automation
/// </remarks>
internal sealed class DlrScriptCommandProcessor : ScriptCommandProcessorBase
{
private new ScriptBlock _scriptBlock;
private readonly ArrayList _input = new ArrayList();
private readonly object _dollarUnderbar = AutomationNull.Value;
private new ScriptBlock _scriptBlock;
private MutableTuple _localsTuple;
private bool _runOptimizedCode;
private bool _argsBound;
private FunctionContext _functionContext;
internal DlrScriptCommandProcessor(ScriptBlock scriptBlock, ExecutionContext context, bool useNewScope, CommandOrigin origin, SessionStateInternal sessionState, object dollarUnderbar)
: base(scriptBlock, context, useNewScope, origin, sessionState)
{
Init();
_dollarUnderbar = dollarUnderbar;
}
internal DlrScriptCommandProcessor(ScriptBlock scriptBlock, ExecutionContext context, bool useNewScope, CommandOrigin origin, SessionStateInternal sessionState)
: base(scriptBlock, context, useNewScope, origin, sessionState)
{
@@ -513,6 +521,10 @@ namespace System.Management.Automation
{
_localsTuple.SetAutomaticVariable(AutomaticVariable.Underbar, dollarUnderbar, _context);
}
else if (_dollarUnderbar != AutomationNull.Value)
{
_localsTuple.SetAutomaticVariable(AutomaticVariable.Underbar, _dollarUnderbar, _context);
}
if (inputToProcess != AutomationNull.Value)
{
@@ -173,6 +173,13 @@ namespace System.Management.Automation.Runspaces
get { return _useLocalScope; }
}
/// <summary>
/// Gets or sets DollarUnderbar ($_) value to be used with script command.
/// This is used by foreach-object -parallel where each piped input ($_) is associated
/// with a parallel running script block.
/// </summary>
internal object DollarUnderbar { get; set; } = AutomationNull.Value;
/// <summary>
/// Checks if the current command marks the end of a statement (see PowerShell.AddStatement())
/// </summary>
@@ -495,7 +502,8 @@ namespace System.Management.Automation.Runspaces
commandProcessorBase = new DlrScriptCommandProcessor(scriptBlock,
executionContext, _useLocalScope ?? false,
origin,
executionContext.EngineSessionState);
executionContext.EngineSessionState,
DollarUnderbar);
}
}
else
File diff suppressed because it is too large Load Diff
@@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
@@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
@@ -165,4 +165,20 @@
<data name="ValueNotSpecifiedForWhereObject" xml:space="preserve">
<value>The specified operator requires both the -Property and -Value parameters. Provide values for both parameters, and then try the command again.</value>
</data>
</root>
<data name="PSTaskStreamWriterWrongThread" xml:space="preserve">
<value>This method cannot be run on the current thread. It can only be called on the cmdlet thread.</value>
</data>
<data name="ParallelUsingVariableCannotBeScriptBlock" xml:space="preserve">
<value>A ForEach-Object -Parallel using variable cannot be a script block. Passed-in script block variables are not supported with ForEach-Object -Parallel, and can result in undefined behavior.</value>
</data>
<data name="ParallelPipedInputObjectCannotBeScriptBlock" xml:space="preserve">
<value>A ForEach-Object -Parallel piped input object cannot be a script block. Passed-in script block variables are not supported with ForEach-Object -Parallel, and can result in undefined behavior.</value>
</data>
<data name="ParallelCannotUseTimeoutWithJob" xml:space="preserve">
<value>The 'TimeoutSeconds' parameter cannot be used with the 'AsJob' parameter.</value>
</data>
<data name="ParallelCommonParametersNotSupported" xml:space="preserve">
<value>The following common parameters are not currently supported in the Parallel parameter set:
ErrorAction, WarningAction, InformationAction, PipelineVariable</value>
</data>
</root>
@@ -878,6 +878,43 @@ try
}
}
Describe "ForEach-Object -Parallel Constrained Language Tests" -Tags 'Feature','RequireAdminOnWindows' {
BeforeAll {
$skipTest = -not $EnabledExperimentalFeatures.Contains('PSForEachObjectParallel')
if ($skipTest) {
Write-Verbose "Test Suite Skipped. The test suite requires the experimental feature 'PSForEachObjectParallel' to be enabled." -Verbose
$originalDefaultParameterValues = $PSDefaultParameterValues.Clone()
$PSDefaultParameterValues["it:skip"] = $true
}
}
AfterAll {
if ($skipTest) {
$global:PSDefaultParameterValues = $originalDefaultParameterValues
}
}
It 'Foreach-Object -Parallel must run in ConstrainedLanguage mode under system lock down' {
try
{
$ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage"
Invoke-LanguageModeTestingSupportCmdlet -SetLockdownMode
$results = 1..1 | ForEach-Object -Parallel { $ExecutionContext.SessionState.LanguageMode }
}
finally
{
Invoke-LanguageModeTestingSupportCmdlet -RevertLockdownMode -EnableFullLanguageMode
}
$results | Should -BeExactly "ConstrainedLanguage"
}
}
Describe "Dot sourced script block functions from trusted script files should not run FullLanguage in ConstrainedLanguage context" -Tags 'Feature','RequireAdminOnWindows' {
BeforeAll {
@@ -0,0 +1,407 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
Describe 'ForEach-Object -Parallel Basic Tests' -Tags 'CI' {
BeforeAll {
$skipTest = -not $EnabledExperimentalFeatures.Contains('PSForEachObjectParallel')
if ($skipTest) {
Write-Verbose "Test Suite Skipped. The test suite requires the experimental feature 'PSForEachObjectParallel' to be enabled." -Verbose
$originalDefaultParameterValues = $PSDefaultParameterValues.Clone()
$PSDefaultParameterValues["it:skip"] = $true
}
else {
$sb = { "Hello!" }
}
}
AfterAll {
if ($skipTest) {
$global:PSDefaultParameterValues = $originalDefaultParameterValues
}
}
It "Verifies dollar underbar variable" {
$expected = 1..10
$result = $expected | ForEach-Object -Parallel { $_ }
$result.Count | Should -BeExactly $expected.Count
$result | Should -Contain 1
$result | Should -Contain 10
}
It 'Verifies using variables' {
$var = "Hello"
$varArray = "Hello","There"
$result = 1..1 | ForEach-Object -Parallel { $using:var; $using:varArray[1] }
$result.Count | Should -BeExactly 2
$result[0] | Should -BeExactly $var
$result[1] | Should -BeExactly $varArray[1]
}
It 'Verifies terminating error streaming' {
$result = 1..1 | ForEach-Object -Parallel { throw 'Terminating Error!'; "Hello" } 2>&1
$result.Count | Should -BeExactly 1
$result.ToString() | Should -BeExactly 'Terminating Error!'
$result.FullyQualifiedErrorId | Should -BeExactly 'PSTaskException'
}
It 'Verifies terminating error in multiple iterations' {
$results = 1..2 | ForEach-Object -Parallel {
if ($_ -eq 1) {
throw 'Terminating Error!'
"Hello!"
}
else {
"Goodbye!"
}
} 2>&1
$resultStrings = $results | ForEach-Object { $_.ToString() }
$resultStrings | Should -Not -Contain "Hello!"
$resultStrings | Should -Contain "Goodbye!"
$resultStrings | Should -Contain "Terminating Error!"
}
It 'Verifies non-terminating error streaming' {
$actualError = 1..1 | ForEach-Object -Parallel { Write-Error "Error!" } 2>&1
$actualError.ToString() | Should -BeExactly 'Error!'
$actualError.FullyQualifiedErrorId | Should -BeExactly 'Microsoft.PowerShell.Commands.WriteErrorException'
}
It 'Verifies warning data streaming' {
$actualWarning = 1..1 | ForEach-Object -Parallel { Write-Warning "Warning!" } 3>&1
$actualWarning.Message | Should -BeExactly 'Warning!'
}
It 'Verifies verbose data streaming' {
$actualVerbose = 1..1 | ForEach-Object -Parallel { Write-Verbose "Verbose!" -Verbose } -Verbose 4>&1
$actualVerbose.Message | Should -BeExactly 'Verbose!'
}
It 'Verifies debug data streaming' {
$actualDebug = 1..1 | ForEach-Object -Parallel { Write-Debug "Debug!" -Debug } -Debug 5>&1
$actualDebug.Message | Should -BeExactly 'Debug!'
}
It 'Verifies information data streaming' {
$actualInformation = 1..1 | ForEach-Object -Parallel { Write-Information "Information!" } 6>&1
$actualInformation.MessageData | Should -BeExactly 'Information!'
}
It 'Verifies error for using script block variable' {
{ 1..1 | ForEach-Object -Parallel { $using:sb } } | Should -Throw -ErrorId 'ParallelUsingVariableCannotBeScriptBlock,Microsoft.PowerShell.Commands.ForEachObjectCommand'
}
It 'Verifies error for script block piped variable' {
$actualError = $sb | ForEach-Object -Parallel { "Hello" } 2>&1
$actualError.FullyQualifiedErrorId | Should -BeExactly 'ParallelPipedInputObjectCannotBeScriptBlock,Microsoft.PowerShell.Commands.ForEachObjectCommand'
}
It 'Verifies that parallel script blocks run in FullLanguage mode by default' {
$results = 1..1 | ForEach-Object -Parallel { $ExecutionContext.SessionState.LanguageMode }
$results | Should -BeExactly 'FullLanguage'
}
}
Describe 'ForEach-Object -Parallel common parameters' -Tags 'CI' {
BeforeAll {
# Test cases
$TestCasesNotSupportedCommonParameters = @(
@{
testName = 'Verifies that ErrorAction common parameter is not supported'
scriptBlock = { 1..1 | ForEach-Object -Parallel { "Hello" } -ErrorAction Stop }
},
@{
testName = 'Verifies that WarningAction common parameter is not supported'
scriptBlock = { 1..1 | ForEach-Object -Parallel { "Hello" } -WarningAction SilentlyContinue }
},
@{
testName = 'Verifies that InformationAction common parameter is not supported'
scriptBlock = { 1..1 | ForEach-Object -Parallel { "Hello" } -InformationAction SilentlyContinue }
},
@{
testName = 'Verifies that PipelineVariable common parameter is not supported'
scriptBlock = { 1..1 | ForEach-Object -Parallel { "Hello" } -PipelineVariable pipeVar }
}
)
$TestCasesForSupportedCommonParameters = @(
@{
testName = 'Verifies ErrorVariable common parameter'
scriptBlock = { 1..1 | ForEach-Object -Parallel { Write-Error "Error:$_" } -ErrorVariable global:actualVariable }
expectedResult = 'Error:1'
},
@{
testName = 'Verifies WarningVarible common parameter'
scriptBlock = { 1..1 | ForEach-Object -Parallel { Write-Warning "Warning:$_" } -WarningVariable global:actualVariable }
expectedResult = 'Warning:1'
},
@{
testName = 'Verifies InformationVariable common parameter'
scriptBlock = { 1..1 | ForEach-Object -Parallel { Write-Information "Information:$_"} -InformationVariable global:actualVariable }
expectedResult = 'Information:1'
},
@{
testName = 'Verifies OutVariable common parameter'
scriptBlock = { 1..1 | ForEach-Object -Parallel {Write-Output "Output:$_"} -OutVariable global:actualVariable }
expectedResult = 'Output:1'
}
)
}
BeforeEach {
$global:actualVariable = $null
}
AfterAll {
$global:actualVariable = $null
}
It "<testName>" -TestCases $TestCasesNotSupportedCommonParameters {
param ($scriptBlock)
{ & $scriptBlock } | Should -Throw -ErrorId 'ParallelCommonParametersNotSupported,Microsoft.PowerShell.Commands.ForEachObjectCommand'
}
It "<testName>" -TestCases $TestCasesForSupportedCommonParameters {
param ($scriptBlock, $expectedResult)
& $scriptBlock *>$null
$global:actualVariable[0].ToString() | Should -BeExactly $expectedResult
}
}
Describe 'ForEach-Object -Parallel -AsJob Basic Tests' -Tags 'CI' {
BeforeAll {
$skipTest = -not $EnabledExperimentalFeatures.Contains('PSForEachObjectParallel')
if ($skipTest) {
Write-Verbose "Test Suite Skipped. The test suite requires the experimental feature 'PSForEachObjectParallel' to be enabled." -Verbose
$originalDefaultParameterValues = $PSDefaultParameterValues.Clone()
$PSDefaultParameterValues["it:skip"] = $true
}
}
AfterAll {
if ($skipTest) {
$global:PSDefaultParameterValues = $originalDefaultParameterValues
}
}
It 'Verifies TimeoutSeconds parameter is excluded from AsJob' {
{ 1..1 | ForEach-Object -AsJob -Parallel { "Hello" } -TimeoutSeconds 60 } | Should -Throw -ErrorId 'ParallelCannotUseTimeoutWithJob,Microsoft.PowerShell.Commands.ForEachObjectCommand'
}
It 'Verifies ForEach-Object -Parallel jobs appear in job repository' {
$job = 1..1 | ForEach-Object -AsJob -Parallel { "Hello" }
Get-Job | Should -Contain $job
$job | Wait-Job | Remove-Job
}
It 'Verifies dollar underbar variable' {
$expected = 1..10
$job = $expected | ForEach-Object -AsJob -Parallel { $_ }
$result = $job | Wait-Job | Receive-Job
$job | Remove-Job
$result.Count | Should -BeExactly $expected.Count
$result | Should -Contain 1
$result | Should -Contain 10
}
It 'Verifies using variables' {
$Var1 = "Hello"
$Var2 = "Goodbye"
$Var3 = 105
$Var4 = "One","Two","Three"
$job = 1..1 | Foreach-Object -AsJob -Parallel {
Write-Output $using:Var1
Write-Output $using:Var2
Write-Output $using:Var3
Write-Output @(,$using:Var4)
Write-Output $using:Var4[1]
}
$results = $job | Wait-Job | Receive-Job
$job | Remove-Job
$results[0] | Should -BeExactly $Var1
$results[1] | Should -BeExactly $Var2
$results[2] | Should -BeExactly $Var3
$results[3] | Should -BeExactly $Var4
$results[4] | Should -BeExactly $Var4[1]
}
It 'Verifies terminating error in single iteration' {
$job = 1..1 | ForEach-Object -AsJob -Parallel { throw "Terminating Error!"; "Hello" }
$results = $job | Wait-Job | Receive-Job 2>$null
$results.Count | Should -BeExactly 0
$job.State | Should -BeExactly 'Failed'
$job.ChildJobs[0].JobStateInfo.State | Should -BeExactly 'Failed'
$job.ChildJobs[0].JobStateInfo.Reason.Message | Should -BeExactly 'Terminating Error!'
$job | Remove-Job
}
It 'Verifies terminating error in double iteration' {
$job = 1..2 | ForEach-Object -AsJob -Parallel {
if ($_ -eq 1) {
throw "Terminating Error!"
"Goodbye!"
}
else {
"Hello!"
}
}
$results = $job | Wait-Job | Receive-Job 2>$null
$results | Should -Contain 'Hello!'
$results | Should -Not -Contain 'Goodbye!'
$job.JobStateInfo.State | Should -BeExactly 'Failed'
$job.ChildJobs[0].JobStateInfo.State | Should -BeExactly 'Failed'
$job.ChildJobs[0].JobStateInfo.Reason.Message | Should -BeExactly 'Terminating Error!'
$job.ChildJobs[1].JobStateInfo.State | Should -BeExactly 'Completed'
$job | Remove-Job
}
It 'Verifies non-terminating error' {
$job = 1..1 | ForEach-Object -AsJob -Parallel { Write-Error "Error:$_" }
$results = $job | Wait-Job | Receive-Job 2>&1
$job | Remove-Job
$results.ToString() | Should -BeExactly "Error:1"
}
It 'Verifies warning data' {
$job = 1..1 | ForEach-Object -AsJob -Parallel { Write-Warning "Warning:$_" }
$results = $job | Wait-Job | Receive-Job 3>&1
$job | Remove-Job
$results.Message | Should -BeExactly "Warning:1"
}
It 'Verifies verbose data' {
$job = 1..1 | ForEach-Object -AsJob -Parallel { Write-Verbose "Verbose:$_" -Verbose }
$results = $job | Wait-Job | Receive-Job -Verbose 4>&1
$job | Remove-Job
$results.Message | Should -BeExactly "Verbose:1"
}
It 'Verifies debug data' {
$job = 1..1 | ForEach-Object -AsJob -Parallel { Write-Debug "Debug:$_" -Debug }
$results = $job | Wait-Job | Receive-Job -Debug 5>&1
$job | Remove-Job
$results.Message | Should -BeExactly "Debug:1"
}
It 'Verifies information data' {
$job = 1..1 | ForEach-Object -AsJob -Parallel { Write-Information "Information:$_" }
$results = $job | Wait-Job | Receive-Job 6>&1
$job | Remove-Job
$results.MessageData | Should -BeExactly "Information:1"
}
It 'Verifies job Command property' {
$job = 1..1 | ForEach-Object -AsJob -Parallel {"Hello"}
$job.Command | Should -BeExactly '"Hello"'
$job.ChildJobs[0].Command | Should -BeExactly '"Hello"'
$job | Wait-Job | Remove-Job
}
}
Describe 'ForEach-Object -Parallel Functional Tests' -Tags 'Feature' {
BeforeAll {
$skipTest = -not $EnabledExperimentalFeatures.Contains('PSForEachObjectParallel')
if ($skipTest) {
Write-Verbose "Test Suite Skipped. The test suite requires the experimental feature 'PSForEachObjectParallel' to be enabled." -Verbose
$originalDefaultParameterValues = $PSDefaultParameterValues.Clone()
$PSDefaultParameterValues["it:skip"] = $true
}
}
AfterAll {
if ($skipTest) {
$global:PSDefaultParameterValues = $originalDefaultParameterValues
}
}
It 'Verifies job queuing and throttle limit' {
# Run four job tasks, two in parallel at a time.
$job = 1..4 | ForEach-Object -Parallel { Start-Sleep 60 } -AsJob -ThrottleLimit 2
# Wait for child job 2 to begin running for up to ten seconds
if ( !(Wait-UntilTrue -TimeoutInMilliseconds 10000 -IntervalInMilliseconds 250 `
{ $job.ChildJobs[1].JobStateInfo.State -eq 'Running' }))
{
throw "ForEach-Object -Parallel child job 2 did not start"
}
# Two job tasks should be running and two waiting to run
$job.ChildJobs[0].JobStateInfo.State | Should -BeExactly 'Running'
$job.ChildJobs[1].JobStateInfo.State | Should -BeExactly 'Running'
$job.ChildJobs[2].JobStateInfo.State | Should -BeExactly 'NotStarted'
$job.ChildJobs[3].JobStateInfo.State | Should -BeExactly 'NotStarted'
$job | Remove-Job -Force
}
It 'Verifies jobs work with Receive-Job -AutoRemove parameter' {
$job = 1..4 | ForEach-Object -AsJob -Parallel { "Hello:$_" }
$null = $job | Receive-Job -Wait -AutoRemoveJob
Get-Job | Should -Not -Contain $job
}
It 'Verifies parallel task queuing' {
$results = 10..1 | ForEach-Object -Parallel { Start-Sleep 1; $_ } -ThrottleLimit 5
$results[0] | Should -BeGreaterThan 5
$results[1] | Should -BeGreaterThan 5
$results[2] | Should -BeGreaterThan 5
$results[3] | Should -BeGreaterThan 5
$results[4] | Should -BeGreaterThan 5
}
It 'Verifies timeout and throttle parameters' {
# With ThrottleLimit set to 1, the two 60 second long script blocks will run sequentially,
# until the timeout in 5 seconds.
$results = 1..2 | ForEach-Object -Parallel { "Output $_"; Start-Sleep -Seconds 60 } -TimeoutSeconds 5 -ThrottleLimit 1 2>&1
$results.Count | Should -BeExactly 2
$results[0] | Should -BeExactly 'Output 1'
$results[1].FullyQualifiedErrorId | Should -BeExactly 'PSTaskException'
$results[1].Exception | Should -BeOfType [System.Management.Automation.PipelineStoppedException]
}
}
+2 -1
View File
@@ -1,6 +1,7 @@
{
"ExperimentalFeatures": {
"Microsoft.PowerShell.Utility.PSDebugRunspaceWithBreakpoints": ["test/powershell/Modules/Microsoft.PowerShell.Utility/New-PSBreakpoint.Tests.ps1"],
"ExpTest.FeatureOne": [ "test/powershell/engine/ExperimentalFeature/ExperimentalFeature.Basic.Tests.ps1" ]
"ExpTest.FeatureOne": [ "test/powershell/engine/ExperimentalFeature/ExperimentalFeature.Basic.Tests.ps1" ],
"PSForEachObjectParallel": [ "test/powershell/Modules/Microsoft.PowerShell.Utility/Foreach-Object-Parallel.Tests.ps1", "test/powershell/Modules/Microsoft.PowerShell.Security/ConstrainedLanguageRestriction.Tests.ps1" ]
}
}