-
Notifications
You must be signed in to change notification settings - Fork 8k
Description
As an author of PowerShell jobs that are routinely canceled, I would like to have confidence that cleanup occurs as expected. I very much appreciate how the addition of the clean{}
block helps to accomplish that.
Some cleanup scenarios hinge on atomic assignment of a new object to a PowerShell variable. Consider, for example
param($filePath)
end {
$fs = [System.IO.FileStream]::new($filePath,'Append','Write')
$fs
}
clean {
${fs}?.Dispose()
}
which relies on assignment to $fs
to be atomic with the construction of FileStream
in order for Dispose()
be called reliably. Indeed, that assignment statement seems to be atomic. That is, if the job where this code is running is stopped while the constructor is running, the assignment to $fs
succeeds. On the other hand, the assignment
$fs = New-Object System.IO.FileStream $filePath,'Append','Write'
is not atomic with the construction of FileStream
. That is, if the job is stopped while the new FileStream
object is being constructed, the construction completes but the assignment does not complete. In that unfortunate condition, this leaves the file at $filePath
locked for writing at least until garbage collection occurs. That leads to intermittent errors like
Exception calling ".ctor" with "3" argument(s): "The process cannot access the file 'C:\log.binlog' because it is being used by another process."
when opening the same file is attempted again, for example.
Whether such assignments are atomic to stopping jobs is the subject of this question. There is sample code that tests the atomicity of assignment in this and this answer. There is also a working definition of "atomic" in that question that applies to this proposal (as well as that Q&A).
The reliable cleanup of the FileStream
objects in the above examples relies on atomic assignment. Many similar such cleanup scenarios involving the clean{}
block also rely on atomic atomic assignment. It appears from this comment, however, that the atomicity of assignment may well be a side-effect of performance considerations rather than intended behavior. The comment includes the following statement:
Stop()
is implemented by having the runtime check theStopping
flag in a bunch of places. Any long-running operation should check but we don't check for every operation as that would be too much overhead. I don't believe we check it for assignment statements (but I'd have to check the code).
I like that at least some assignment statements are currently atomic. But I don't like that other idiomatic assignment statements like those involving Set-Variable
or New-Object
are not. Currently to ensure reliable cleanup I have to carefully consider how each variable used in the clean{}
block was assigned and try to reorganize the code so that the assignment happens in a way that is atomic. What's worse is I'm relying on empirical tests rather than intentional design to formulate what are, hopefully, atomic assignment statements.
Proposal
I propose that PowerShell be extended to provide at least one way to construct an object and assign it to a PowerShell variable in a way that is
- atomic when PowerShell is stopping,
- is unambiguously and easily recognizable as atomic to readers acquainted with its use, and
- is intended by the maintainers of PowerShell to be atomic in future releases.
Related Considerations
While not the main concern of this proposal, I note that a similar question of atomicity arises wherever someone is inspired to use the using
pattern in PowerShell. Consider for example, an implementation of Use-Object
used thus:
Use-Object ($d = [SomeDisposable]::new()) {
$d # do some useful thing with d
}
I like this usage. I would this to work reliably. Unfortunately for this scenario, PowerShell's parameter binder seems to check the Stopping
flag. That means that if a Stop-Job
call results in Stopping
while [SomeDisposable]::new()
is running, $d
is constructed, but Use-Object
never has its first argument bound, and never calls Dispose()
on $d
. So $d
sometimes remains undisposed until the garbage collector gets to it.
I think clean{}
has mostly obviated the need for the using
pattern, but it still arises on occasion. However, the same problem of parameter binding atomicity is at play in this formulation of the aforemention FileStream
assignment:
Set-Variable fs ([System.IO.FileStream]::new($filePath,'Append','Write'))
That assignment statement is not atomic for at least the problem of the parameter binding not being atomic. It seems then that achieving the goals of the proposal by way of atomic parameter binding would also allow for reliable Use-Object
implementations. But I don't know which would be the most prudent approach.