Repository navigation
Description
A job that is stopped while it is executing clean{} interrupts execution of that clean{} block. This appears to contradict the documentation
The clean block is a convenient way for users to clean up resources... Resource cleanup is enforced for the following scenarios:
...
4. when the pipeline is being stopped byStopProcessing()
It seems that when StopProcessing() occurs while clean{} is executing, resource cleanup is interrupted rather than enforced.
Steps to reproduce
$log = [System.Collections.Concurrent.ConcurrentQueue[string]]::new()
$job =
Start-ThreadJob `
-ArgumentList $log `
-ScriptBlock {
param($log)
begin {}
clean {
$log.Enqueue('clean{} start')
Start-Sleep 1
$log.Enqueue('clean{} end')
}
}
Start-Sleep -Milliseconds 100
$job.StopJobAsync()
Start-Sleep -Milliseconds 100
$job | Receive-Job -Wait
$logExpected output
clean{} start
clean{} endperhaps with
OperationStopped: The pipeline has been stopped.
mentioned.
Actual output
OperationStopped: The pipeline has been stopped.
clean{} startRelated Issues
This issue arose from Provide a way to assign a returned object to a variable that is explicitly atomic when Stopping PowerShell #24658.
There are a number of other related issues as summarized in the table below. Note that none of those issues are the same as this because of the following distinctions:
- The issues in the table are only reproducible with Ctrl+C, and not by stopping a job. This issue is reproducible by stopping a job.
- None of the issues below contemplate the scenario where execution has already entered
clean{}when execution is interrupted. This issue is about interrupting execution inclean{}.
| Issue | Shortened Description | Ctrl+C repro 1. |
Job.Stoppingrepro |
|---|---|---|---|
| #23786 | Finally block isn't executed upon Ctrl+C |
yes | no |
| #19988 | Attempts to write to success in finally causes termination |
yes | no |
| #19987 | statements in finally fail with Ctrl+C and Read-Host |
OP | N/A 2. |
| #24241 | "Clean" block does not run on Ctrl+C | yes | no |
Notes:
- "yes" and "no" in the repro columns are indicated only if I was able to successfully repeat a check for the behavior reported in the issue.
Write-Hoststatements, file redirections in afinallyblock intermittently fail quietly on Windows when Ctrl+C is used to terminate aRead-Hostcall #19987 seems to hinge onRead-Hostin a way that doesn't have an obvious analog in a job where there is no console.
Environment data
Name Value
---- -----
PSVersion 7.4.6
PSEdition Core
GitCommitId 7.4.6
OS Microsoft Windows 10.0.22631
Platform Win32NT
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0…}
PSRemotingProtocolVersion 2.3
SerializationVersion 1.1.0.1
WSManStackVersion 3.0
Activity
alx9r commentedon Dec 16, 2024
Observation of
StopProcessing()andStoppingInvocation of
StopProcessing()and setting ofStoppingcan be observed occurring inclean{}per this comment:alx9r commentedon Dec 16, 2024
Workaround
Consider the following test which records the flow control of a script block whose execution is stopped at different
stop:points:test code
An analogous script block with points annotated with
stop:where the stop signal arrives follows:The following table is output by that test:
Analysis
The test demonstrates the following key observations:
clean{try{}}orclean{finally{}}or both executes to completion regardless of thestop:point.$cleanis executed either once or twice regardless of thestop:point.This implies that the interruption of
clean{}can be recovered from using the aboveclean{try{}finally{}}pattern for any cleanup that can safely be invoked multiple times.Note the following nuances about this workaround:
clean{}and assignment of$cleanmust contain only statements that are atomic 1.. Which statements are atomic seems to be incidental. That is, atomicity is not by design. This means whether a statement is (or a series of statements are) atomic is an empirical question. A statement's atomicity can be tested using one of the techniques mentioned in this answer.clean{try{}finally{}}into a PowerShell command that accepts arguments since parameter binding seems to be interrupted byStopping.A parameterless command that does not checkStoppingand thus reliably executes when it is the first command inclean{}.Out-Nullseems to be an example of such a command that reliably executes in that position. Such a call site might look likeclean {$cleanup = {...}; CleanUp;}whereCleanUpinvokes$cleanupby way ofCmdlet.SessionState.PSVariable.A .Net method that accepts$cleanupscriptblock and, possibly,$PSCmdlet. Such a call site might look like[workaround]::clean($PSCmdlet,{...})whereclean()injects the requisitetry{}finally{}to ensure complete execution in all cases.finally{}Stoppingis initiated from Ctrl+C as mentioned in the related issues in OP hereFootnotes:
1. Atomic here has the meaning from the working definition specified in this question.
alx9r commentedon Dec 16, 2024
.Net Method Workaround Proof-of-Concept
By the same doctrine as the test for the
clean{try{}finally{}}workaround, the following test records the flow control of a script block whose execution is stopped at differentstop:points:test code
The analogous script block with
stop:points annotated is as follows:The following table is output by that test:
This demonstrates the following:
Cleanup completes in all cases.Cleanup does not complete when the timing of the stop signal occurs afterclean{}has been entered but before the call toScriptBlock.Invoke().[workaround]::clean({})script block,$objhasCleanUp()invoked twice.This workaround also prevents writing to the error and success streams. That seems to abate problems associated with those streams when stopping is inititiated by Ctrl+C like in #19988.
jborean93 commentedon Dec 16, 2024
This was an explicit decision during the implementation of the
cleanblock to have it reflect the behaviour offinally. The PR that was merged #15177 statedThe original PR had some more background information on both sides of the debate but in the end having finally/clean be cancellable was desired from a shell perspective over a consistency of state perspective. I think over time I have changed my mind to be in favour of the final choice but I'm sure there are pros and cons to both sides of the debate but in the end the current behaviour of
cleanand thefinallyblock was explicitly chosen to be by design.alx9r commentedon Dec 17, 2024
Jordan Borean (@jborean93) Thank you for that incite. That is very helpful.
I see now the applicable deliberation in the RFC's Pipeline Stopping Behavior section:
Indeed there is substantial debate about those behaviors in the RFC and PR review. Evidently the current design was chosen and it led to me opening this issue. I certainly can see now how and why that decision was reached. I would say though, that given that the current behavior is an intentional design decision, the documentation for
clean{}currently overstates its abilities in this statement:As it stands, out-of-the-box, a plain-old
clean{}block cannot enforce cleanup as described by that statement without extraordinary measures like those described in this thread.I think that passage would more accurately be written as follows:
SeeminglyScience commentedon Dec 20, 2024
The wording there is still correct, with an added caveat that if
cleanis running when Ctrl + C is pressed for the first time in that command invocation, it will be interrupted. Typically that'll be quite rare as most cleanup operations are very fast, but definitely still possible.That's just my opinion though, you're welcome to see if the docs folks would be down to add more details (MicrosoftDocs/PowerShell-Docs).
alx9r commentedon Dec 20, 2024
Patrick Meinecke (@SeeminglyScience) I'll consider doing so. Could you direct me to whatever guidance there is to accomplish that? I've had some unproductive experiences attempting to improve similar documentation, so I think I must be missing something about how to do this effectively.
SeeminglyScience commentedon Dec 21, 2024
They have a new issue template, afraid I don't have any additional information to share but that's where I'd suggest starting.
iSazonov commentedon Dec 21, 2024
By-design? I wonder why did we spend a lot of time to implement
clean {}? The main intention was to clear resources when it's not possible inend {}block. In other words, Ctrl-C interrupting the work of all (begin {},process {},end {}) blocks should still ensure the execution ofclean {}blocks in the pipeline (just asfinally {}).alx9r commentedon Dec 21, 2024
Ilya (@iSazonov)
Strictly speaking, I think the behavior is by design. Arguably that design has shortcomings. But this part of PowerShell does seem to behave the way it does by design.
I do have a related quibble about tagging this issue with "Resolution-By Design", though: Adding that tag marks the issue as "By Design". That is accurate. But, it also marks this issue as "Resolved". From my perspective this issue is not resolved. Perhaps the implementation of
clean{}andfinally{}should remain as designed, but in that case the problem of unreliable cleanup from my OP remains for me and anyone else facing this issue to solve.(As an aside I've disliked the "Resolution-By Design" tag since I first saw it because it conflates two orthogonal concepts: 1. Whether the issue reflects PowerShell behaving as designed. 2. Whether the issue is resolved. I'm not sure what to do with that grievance since there doesn't seem to be a meta forum for the PowerShell project like stackoverflow, for example, has.)
Based on the historical documents I have reviewed, I think that last sentence reflects what the original intention of
clean{}seems to have been. But it looks like that intention was departed from to achieve the current behavior to make it more consistent withfinally{}. I don't think there's much point revisiting that decision if changing it would be a breaking change. Recently I've been assuming that changingclean{}behavior to address this issue would be a breaking change but I haven't put much thought into that question.For what it's worth, I think the Ctrl+C scenario is difficult to deliberate about because of how easily cleanup is curtailed by the console after interruption (per #23786). But your point, I think, applies equally to stopping jobs where there is no such complication.
For me a similar question to yours comes to mind: After so much has been invested in establishing a reliable cleanup mechanism in PowerShell, why wouldn't we want to abate these last few ways PowerShell itself deters cleanup?
I don't know yet what the best abatement for this issue would be, but I've seen no arguments made for why such an abatement shouldn't at least be explored.
iSazonov commentedon Dec 21, 2024
I wasn't a fan of the appearance of this block as in my opinion binary cmdlets are simpler and more reliable in that sense. But definitely the original request was "to release resources like dispose in C#". If it was transformed into something else and this block doesn't clear resources, why is it needed at all? If it transformed into “do as
finally”, then that highlights the problem thatfinallyalso doesn't work as well as it should (as expected as in C#). All this will work ifterminate(soft kill) is used instead ofkillsemantics (in process terms).5 remaining items
SeeminglyScience commentedon Dec 23, 2024
There are two PRs, the first was created by Rain who was the original author of the change. The second was created by Dongbo after the discussion was already completed. He decided to take over the PR, presumably in order to not force Rain to complete a lot of extra work that they didn't agree with (and it had been discussed for a large chunk of time at that point).
You can see more of the back and forth in the original PR: #9900
But yes, it was indeed understood and discussed at length.
alx9r commentedon Dec 24, 2024
Patrick Meinecke (@SeeminglyScience) Thank you for referring me to that earlier PR. I was not aware of that history. Somehow I didn't see it mentioned in the more recent records. I have reviewed #9900 as well. My understanding of that thread as it relates to this issue follows.
Two distinct scenarios requiring decisions
I see two distinct scenarios that emerged that required a decision about their respective behaviors:
clean{}Stop-Jobfor the first and only time when execution in the job runspace is inclean{}For brevity I propose referring to these as the Ctrl+C and
Stop-Jobscenarios, respectively.Ctrl+C: User presses Ctrl+C for the first or nth time when execution in the console runspace is in
clean{}This is not the scenario that corresponds to the issue described in my OP.
This is the scenario about which much of the discussion in #9900 is centered.
There are many arguments on both sides of this debate, but ultimately, for this scenario it seems to have been accepted that regardless of any other consideration when a user presses Ctrl+C while
clean{}is executing in the console runspace, the executing pipeline is de facto interrupted. I reviewed the deliberations about this as it was represented in the RFC and the latter PR #15177 in a prior comment. When I wrote that I was nearly persuaded that this behavior was accurately characterized as a design decision. But after reviewing #9900 I think it's more accurate to say that consensus about changing the behavior from that offinallycould not be reached. Socleanwas implemented to mimickfinallyfor this scenario. That's understandable and is the obvious safe course of action when consensus to make a change cannot be reached.Stop-Job: Automation invokesStop-Jobfor the first and only time when execution in the job runspace is inclean{}This is the scenario that corresponds to the issue described in my OP.
This scenario is only contemplated in passing in #9900. The first description of this scenario in #9900 seems to have been "hosted PowerShell runtime" with "Stop() called...from another thread." Eventually this scenario came to be referred to as the "automation" scenario in #9900.
The behavior I expected for this scenario is described by Rob Holt here (my emphasis added):
It appears to me that behavior was taken as given from there on.
Dongbo seems to first set out preferences about this behavior here:
There is no mention of stopping the
Cleanblock using.Stop()in the No guarantee that it will run to the end section. If the intention at that time was that.Stop()should interruptCleanI would expected that to be mentioned in that section.Dongbo's later comment seems to presume the "block the Stop() call" behavior for this scenario:
There are a few more replies to that here, here, and here. But all of those replies are consistent with the original suggestion that "it makes sense to block the Stop() call on the cleanup block completing."
I think there are two interpretations about what was decided about this scenario:
I the record does not seem to support
The neither-was-decided interpretation
For what it's worth, I think the record also supports the interpretation (perhaps better than any other interpretation) that behavior of neither of the above scenarios was decided.
This is story that fits best to me after having read through all of these deliberations: Discussion around
clean{}bogged down in the Ctrl+C debate in March 2021 with no resolution. Development on everything else related to the feature continued until a prototype by Dongbo was announced in April. That announcement included the following statement:So somehow the prototype was made in a way that "doesn't do anything about Ctrl+C handling." That's completely understandable given the lack of consensus. I expect I'd have done the same if I were in that situation. Development then continued into October 2021 with no further discussion about Ctrl+c and still no explicit decision and also no change to behavior. The feature was released in December 2021's
v7.3.0-preview.1with no explicit decision ever having been made about the Ctrl+C scenario.As for the
Stop-Jobscenario, that is, the scenario that corresponds to this issue: That scenario just incidentally ended up with the same behavior as the Ctrl+C scenario.SeeminglyScience commentedon Dec 24, 2024
They are the same mechanism, there just isn't a straight forward catch all term so ctrl + c was used as it's the primary use case. References you see to a
Stopmethod are talking about REPL implementations, as we will callPowerShell.Stop()when ctrl + c is pressed.iSazonov commentedon Dec 24, 2024
This comment was about the fact that even C# does not guarantee the release of resources and we should not expect that pwsh will be guaranteed to perform
finallyandclean.Otherwise, my expectations were as I described here. My only omission was that the Dongbo's example used Write-Host rather than Write-Output, and I didn't see the deviations that we were discussing now.
And I think we should take the next step and improve
finallyandclean.alx9r commentedon Dec 25, 2024
From my earlier comment:
This is no longer merely a quibble since using this overloaded tag leads to prejudicial closing of issues. I recognize that this might be an oversight, but it does leaves me wondering whether this policy remains such in good faith.
I no longer consider this an aside. I'm now looking for the appropriate forum in which to properly voice this grievance.
iSazonov commentedon Dec 25, 2024
I think we all want these blocks to work better than they do now.
alx9r commentedon Dec 26, 2024
Shims aren't reliable workarounds
The stop-safe shims did not survive testing. In particular, if
Stopoccurs afterclean{}is entered but before the shim invokes its workaround script block, invoking the workaround script block throws an exception. The test results are summarized in the following table:cleanfinallycleanup{try{}finally{}}shimdetails
The following is a summary of testing cleanup when interrupted by a
Stopsignal. Two techniques that attempt to ensure cleanup occurs,cleanup{try{}finally{}}andshim, are tested. The script blocks under test are analogous to the following script blocks . The script blocks are annotated with the locations at corresponding to the various point at which the stop signal was received during testing.cleanup{try{}finally{}}shimDetails
cleanup{try{}finally{}}finallycleanup{try{}finally{}}cleanshimfinallyshimcleancleanup{cleanup(1)} after cleanupcleanup{cleanup(1)} before cleanupcleanup{cleanup(1)} completecleanup{cleanup(2))} completecleanup{cleanup(2)} after cleanupcleanup{cleanup(2)} before cleanupcleanup{cleanup{}}} before shiminterrupted{} after ctorinterrupted{} before ctorinterrupted{} completeinterrupted{} inside ctor$PSVersionTableThis leaves the following
cleanup{try{}finally{}}pattern as the only known (to me, at least) way to structure a script block such that it achieves reliable cleanup afterStop-Job:<#...#>indicates user code.Note the following known (to me, at least) restrictions that apply to this pattern in order to accomplish reliable cleanup:
$cleanwill run to completion either once or twice after one of the other blocks has been entered. Care should be taken when authoring$cleanthat invoking it twice does not cause other problems.$cleanitself (but not both invocations) can be interrupted at any point byStop-Job. Care should be taken when authoring$cleanto ensure the following:Stop-Jobsuch that cleanup occurs is a separate issue and is covered in Provide a way to assign a returned object to a variable that is explicitly atomic when Stopping PowerShell #24658.Finallyblock isn't executed upon Ctrl+C #23786. In particular, an error that results in writing to the error stream from$cleanresults in curtailment of cleanup after Ctrl+C (but not afterStop-Job).daxian-dbw commentedon Jan 7, 2025
The
.Stopmethod was discussed in the original PR. I didn't have a thorough search, but here is one that I found #9900 (comment):As Patrick Meinecke (@SeeminglyScience) mentioned, the
Stop/StopAsyncAPIs are with the same mechanism asctrl+c. The APIs are mentioned in the RFC but was not discussed in detail as a separate topic.When the
cleanblock PR #15177 was merged, the main purpose was to have a finally-like thing that covers allBegin/Process/Endblocks, so that terminating errors thrown from any of those blocks can be handled without needingtry/finallyredundantly in those blocks with the same or similar cleanup code. The discussion about the pipeline-stopping behavior didn't reach a conclusion at the time of the PR. The PR followed thefinallybehavior for obvious reasons -- consistency, safety, and simplicity, but by no means that's the end of discussion or the final decision. I explicitly called out in #15177 (comment):I think the "Pipeline Stopping Behavior" section of the Clean block RFC captures the problems and concerns pretty well.
alx9r commentedon Jan 15, 2025
For what it's worth, the test fixture used to reproduce #24782 (fixture particulars) can be used to reproduce the behavior that is the subject of this issue using the following commands:
That code outputs the trace
which is missing
command clean{} completed.In contrast,
Trace-ControlFlow -NoCommand dot-source -Pipeline 'Normal Upstream'traces the the same pipeline without the stop signal and showscommmand clean{} completedin its trace log.The good news is that that the behavior described in this issue seems to be the only scenario where a script block invoked by the
.dot-source operator does not completeclean{}as a result of either an external stop signal or error.Using the same test fixture module,
outputs only
That tests more than 30 other distinct scenarios all of which clean up OK.