3

It sounds like a reasonable expectation that events fired from one and the same thread should be received in the order in which they were fired. However, that doesn't seem to be the case. Is this a known/documented behavior, and is there any recourse to correct it?

Below are two ready-to-run code snippets that exhibit the issue, tested with PS v5.1 under both Win7 and Win10.

(a) Events fired from a thread in a separate job (i.e. a different process).

$events = 1000
$recvd = 0
$ooseq = 0

$job = Register-EngineEvent -SourceIdentifier 'Posted' -Action {
    $global:recvd++ 
    if($global:recvd -ne $event.messageData) {
        $global:ooseq++
        ("-?- event #{0} received as #{1}" -f $event.messageData, $global:recvd)
} }

$run = Start-Job -ScriptBlock {
    Register-EngineEvent -SourceIdentifier 'Posted' -Forward
    for($n = 1; $n -le $using:events; $n++) {
        [void] (New-Event -SourceIdentifier 'Posted' -MessageData $n)
} }
Receive-Job -Job $run -Wait -AutoRemoveJob

Unregister-Event -SourceIdentifier 'Posted'
Receive-Job -Job $job -Wait -AutoRemoveJob

if($events -eq $script:recvd) {
    ("total {0} events" -f $events)
} else {
    ("total {0} events events, {1} received" -f $events, $recvd)
}
if($ooseq -ne 0) {
    ("{0} out-of-sequence events" -f $ooseq)
}

Sample output from a failure case (out of a batch of 100 consecutive runs).

   -?- event #545 received as #543
   -?- event #543 received as #544
   -?- event #546 received as #545
   -?- event #544 received as #546
   total 1000 events
   4 out-of-sequence events

(b) Events fired from a separate runspace (i.e. a different thread).

$recvd = 0
$ooseq = 0

$job = Register-EngineEvent -SourceIdentifier 'Posted' -Action {
    $global:recvd++ 
    if($recvd -ne $event.messageData) {
        $global:ooseq++
        ("-?- event #{0} received as #{1}" -f $event.messageData, $recvd)
}}

$sync = [hashTable]::Synchronized(@{})
$sync.Host = $host
$sync.events = 1000
$sync.posted = 0

$rs = [runspaceFactory]::CreateRunspace()
$rs.ApartmentState = "STA"
$rs.ThreadOptions = "ReUseThread"
$rs.Open()
$rs.SessionStateProxy.SetVariable("sync",$sync)

$ps = [powerShell]::Create().AddScript({
    for($n = 1; $n -le $sync.events; $n++) {
        $sync.Host.Runspace.Events.GenerateEvent('Posted', $null, $null, $n)
        $sync.posted++
}})
$ps.runspace = $rs
$thd = $ps.BeginInvoke()
$ret = $ps.EndInvoke($thd)
$ps.Dispose()

Unregister-Event -SourceIdentifier 'Posted'
Receive-Job -Job $job -Wait -AutoRemoveJob

if($sync.events -eq $recvd) {
    ("total {0} events" -f $sync.events)
} else {
    ("total {0} events fired, {1} posted, {2} received" -f $sync.events, $sync.posted, $recvd)
}
if($ooseq -ne 0) {
    ("{0} out-of-sequence events" -f $ooseq)
}

Failure cases resemble the sample one posted under (a) above, except a few runs also had events dropped altogether. This, however, is more likely related to the other question Action-based object events sometimes lost.

total 1000 events fired, 1000 posted, 999 received
484 out-of-sequence events


[ EDIT ]   I ran some additional tests for case (b) specifically, and confirmed that:
  • the receiving Action (where $global:recvd++) is always called on the same managed thread (this was confirmed by saving and comparing the [System.Threading.Thread]::CurrentThread.ManagedThreadId between calls);

  • the receiving Action is not re-entered during execution (this was confirmed by adding a global "nesting" counter, wrapping the Action between [System.Threading.Interlocked]::Increment/Decrement calls and checking that the counter never takes any values other than 0 and 1).

These eliminate a couple of possible race conditions, but still do not explain why the observed behavior is happening or how to correct it, so the original question remains open.

dxiv
  • 16,984
  • 2
  • 27
  • 49

1 Answers1

2

Is this a known/documented behavior?

"Normally" event handling is asynchronous by design. And this is the case in PowerShell with cmdlets like Register-EngineEvent -Action. This is indeed known and intended behaviour. You can read more about PowerShell eventing here and here. Both Microsoft sources point out, that this way of event handling is asynchronous:

PowerShell Eventing lets you respond to the asynchronous notifications that many objects support.

and

NOTE These cmdlets can only be used for asynchronous .NET events. It’s not possible to set up event handlers for synchronous events using the PowerShell eventing cmdlets. This is because synchronous events all execute on the same thread and the cmdlets expect (require) that the events will happen on another thread. Without the second thread, the PowerShell engine will simply block the main thread and nothing will ever get executed.

So that's basically what you are doing. You forward events from your background job to the event subscriber that has an action defined and perform the action without blocking your background job. As far as I can tell, there is nothing more to expect. There is no requirement specified to process the forwarded events in any special order. Even the -Forward switch does not ensure anything more, except passing the events:

Indicates that the cmdlet sends events for this subscription to the session on the local computer. Use this parameter when you are registering for events on a remote computer or in a remote session.

It is hard and maybe impossible to find any documentation on the internals of the cmdlets. Keep in mind that Microsoft does not publish any documentation about internals afaik from the past, instead it is up to the MVPs to guess what happens inside and write books about it (drastically expressed).

So as there is no requirement to process the events in a certain order, and PowerShell just has the task to perfrom actions on an event queue, it is also allowed to perform those actions in parallel to accelerate the processing of the event queue.

Test your scripts on a VM with only one vCPU. The wrong order will still occur sometimes, but way more rarely. So less (real) parallelism, less possibilities to scramble the order. Of course, you cannot prevent the logical parallelism, implemented by different threads executed on one physical core. So some "errors" remain.

Is there any recourse to correct it?

I put "normally" into quotation marks, because there are ways to implement it synchronously. You will have to implement your own event handler of type System.EventHandler. I recommend reading this article to get an example for an implementation.

Another workaround is to store the events in an own event queue and sort them after collection (runs in ISE, not yet in PS):

$events = 10000
$recvd = 0
$ooseq = 0
$myEventQueue = @()

$job = Register-EngineEvent -SourceIdentifier 'Posted' -Action {$global:myEventQueue += $event}

$run = Start-Job -ScriptBlock {
    Register-EngineEvent -SourceIdentifier 'Posted' -Forward
    for($n = 1; $n -le $using:events; $n++) {
        [void] (New-Event -SourceIdentifier 'Posted' -MessageData $n)
    }
}
Receive-Job -Job $run -Wait -AutoRemoveJob
Unregister-Event -SourceIdentifier 'Posted'
Receive-Job -Job $job -Wait -AutoRemoveJob

Write-Host "Wrong event order in unsorted queue:"
$i = 1
foreach ($event in $myEventQueue) {
    if ($i -ne $event.messageData) {
        Write-Host "Event $($event.messageData) received as event $i"
    }
    $i++
}

$myEventQueue = $myEventQueue | Sort-Object -Property EventIdentifier

Write-Host "Wrong event order in sorted queue:"
$i = 1
foreach ($event in $myEventQueue) {
    if ($i -ne $event.messageData) {
        Write-Host "Event $($event.messageData) received as event $i"
    }
    $i++
}

Archived links:

stackprotector
  • 10,498
  • 4
  • 35
  • 64
  • Thank you for the pointers. However, these (and others) refer to cases where the events are fired from multiple different threads and/or received in multiple different threads. But the posted cases have all events fired from one and the *same* thread, and also received in (another) one and the *same* thread. At face value, this would appear to fall under a rule quoted [elsewhere](https://stackoverflow.com/a/37193874) on SO "*within a single thread of execution ... side-effects and exceptions generated by a thread are visible in the order specified*". Turns out that doesn't apply here, but why? – dxiv Apr 21 '20 at 06:28
  • I still think that this is the case, because event handling does not have to be synchronous. Generated events are first added to the event queue. And then any handling may access/process them. As there is no requirement for synchronicity, software/hardware is allowed to optimize the control flow, like handling events in parallel, do pipelining, using results from speculative code execution and so on. This may result in a different processing order of your fired events. – stackprotector Apr 21 '20 at 07:12
  • Your citation states, that your events will be fired in your thread in your specified order, which will sure be the case. But the event handling is not done by this thread you start (`Start-Job`). So the handling (the action you defined) may indeed occur in another order. – stackprotector Apr 21 '20 at 07:12
  • Emphasis in my question is on "*events fired from* ***one and the same thread***, *and also received in (another)* ***one and the same thread***". I would expect the order to be preserved in this particular case. FWIW the same code rewritten in C# works flawlessly - not a single event was lost or reordered during thousands of test runs with millions of events each. So question remains why PS won't or can't do the same. – dxiv Apr 26 '20 at 19:15
  • @dxiv I would not rely on "received in (another) one and the same thread". For the event queue of 1000 events, there might be exactly 1000 other threads (for each action) fired by the event subscriber. Those threads don't have to be processed in any order as there is no connection between them, and: an "order" could still be recreated by evaluating the event IDs, so really no requirement to process them in any order. Imagine your events are processed in parallel and then one thread gets interrupted, then `$global:recvd++` will not happen in your expected sequence anymore. – stackprotector May 03 '20 at 07:51
  • That's not what's happening, though - see the *edit* I just added at the end of the question. The point however is a valid one, and thanks for playing along, though I'll leave the answer as not accepted for now. – dxiv May 03 '20 at 23:19