PowerShell / PowerShell/PowerShell

`ForEach-Object -Parallel`: consider changing the semantics of the `-TimeoutSeconds` parameter to apply to each thread or provide a new switch / parameter that does

Open
#20,197 2 comments 4 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Issue-Enhancement Needs-Triage
Dominant language
C#
Stars
55.5k
Forks
8.5k
Avg merge
1d 2h
Merged PRs (30d)
88

Description

Summary of the new feature / enhancement

The current semantics of the -TimeoutSeconds parameter are arguably unhelpful:

  • The -TimeoutSeconds interval is applied to the runtime duration of the overall, typically throttled and therefore "batched" invocation[1] rather than to the runtime of each thread.

    • Therefore, a timeout can occur even if each individual thread completed in less than the specified timeout. A simple example:
          # This times out when the 3rd thread runs, because - due to ThrottleLimit 2 - 
          # it only starts after 1.5+ seconds, after the first 2 threads
          # have finished.
          1..4 | 
            ForEach-Object -TimeoutSeconds 2 -ThrottleLimit 2 -Parallel { 
              Start-Sleep -MilliSeconds 1500
              $_ # Pass the input object through.
            }
  • When a timeout occurs, the command terminates overall.

    • This means that threads for any remaining pipeline input then never even get to launch.
      • In the above example, only 1 and 2 print; input 4 never gets processed, because processing of 3 caused the timeout and therefore termination of the entire command.

Applying the specified timeout:

  • on a per-thread basis...
  • ... and continuing processing of the inputs even if a single thread times out

is arguably much more sensible.

Therefore, I suggest implementing the latter:

  • Either: By simply changing the meaning of the current -TimeoutSeconds parameter, assuming it amounts to a bucket 3 change.

  • Or: By introducing a separate parameter / additional switch parameter as an opt-in to the proposed semantics.


[1] More accurately, only a fixed number of threads are allowed to run at a time, based on the -ThrottleLimit arguments, which defaults to 5. If more threads are needed, they have to wait until "slots" open up, which happens when currently executing threads finish.

Proposed technical implementation details (optional)

The following shows the desired semantics expressed in PowerShell code, applied to a slightly modified version of the example above, which provokes a timeout for the 3rd input object:

# Use -AsJob to receive a job that allows monitoring the threads individually.
# Note that -AsJob cannot be combined with -TimeoutSeconds
$job = 
  1..4 | 
    ForEach-Object -AsJob -ThrottleLimit 2 -Parallel { 
      if ($_ -eq 3) {
        # Provoke a timeout error for this specific input.
        Start-Sleep -MilliSeconds 2500; $_
      } else {
        Start-Sleep -MilliSeconds 1500; $_
      }
    }

# Receive job output in a polling loop, and terminate child jobs
# that have run too long.
$timeout = 2
do {
  Start-Sleep -Milliseconds 500 # Sleep a little.
  # Get pending results.
  $job | Receive-Job
  # If any child jobs have been running for more than N seconds,
  # stop (terminate) them.
  # This will open up slots for more threads to spin up.
  foreach ($childJob in $job.ChildJobs.Where({ $_.State -eq 'Running' })) {
    if (([datetime]::now - $childJob.PSBeginTime).TotalSeconds -ge $timeout) {
      Write-Verbose -Verbose "Stopping job with ID $($childJob.Id) due to running longer than $timeout seconds..."
      $childJob | Stop-Job
    }
  }
} while ($job.ChildJobs.Where({ $_.State -in 'NotStarted', 'Running' }))

Output:

1
2
4
VERBOSE: Stopping job with ID 4 due to running longer than 2 seconds...

Note:

  • Input 4 was still processed, despite the thread for input 3 having timed out.

  • The ID value of the child job isn't really meaningful except to distinguish it from other child jobs; if you want to know what input object caused the timeout, you'll have to echo it as part of the script block (at the start, before a timeout can occur) - the job object doesn't contain this information.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reproducing the ForEach-Object -Parallel examples with -TimeoutSeconds, -ThrottleLimit, and -AsJob. Compare the current overall timeout behavior with the proposed per-thread behavior, including continued processing after one timeout. Done means either the existing parameter has the new semantics or a documented opt-in parameter provides them, with coverage for throttled inputs and timed-out threads.

Written by the indexing model from the issue text.

Assessment

Tech stack
powershell
Domain
cli
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.