-
Notifications
You must be signed in to change notification settings - Fork 4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[WIP] OwnedDisposable<T> #26321
[WIP] OwnedDisposable<T> #26321
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mavasani This change resolves all CA2000/CA2013 warnings related to Connection
instances without requiring any suppressions. Additional notes provided in the diff.
} | ||
finally | ||
{ | ||
newConnection.Dispose(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💭 @mavasani Commenting out this line doesn't seem to trigger CA2000/CA2013, but I'm not sure why.
return new PooledConnection(this, serviceName, connection); | ||
try | ||
{ | ||
return new OwnedDisposable<Connection>(() => new PooledConnection(this, serviceName, ref connection)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💭 @mavasani The factory function avoids the need to transfer an object into this constructor, but it comes with an allocation cost.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
having a 'ref' inside a lambda always gives me the heebie jeebies FWIW.
WellKnownServiceHubServices.ServiceHubServiceBase_Initialize, | ||
new object[] { scope.SolutionInfo }, | ||
cancellationToken).ConfigureAwait(false); | ||
|
||
return new SessionWithSolution(connection, scope); | ||
return new SessionWithSolution(ref connection.Resource, scope); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📝 The use of a wrapper allows this function to avoid "taking ownership" of connection
in error cases. The ownership doesn't transfer prior to this line, which means this method is no longer responsible for releasing the resource on error paths.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldn't there be Move somewhere to move ownership?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldn't there be Move somewhere to move ownership?
Yes, see #26321 (comment)
private readonly PinnedRemotableDataScope _scope; | ||
|
||
public static async Task<SessionWithSolution> CreateAsync(RemoteHostClient.Connection connection, Solution solution, CancellationToken cancellationToken) | ||
public static async Task<SessionWithSolution> CreateAsync(OwnedDisposable<RemoteHostClient.Connection>.Boxed connection, Solution solution, CancellationToken cancellationToken) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📝 OwnedDisposable<Connection>.Boxed
is functionally equivalent to ref OwnedDisposable<Connection>
, but the latter cannot be passed as an argument to an async
method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. so it is to be used when ref can't be used.
|
||
namespace Roslyn.Utilities | ||
{ | ||
internal struct OwnedDisposable<T> : IDisposable, IEquatable<OwnedDisposable<T>> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📝 This type would need analyzer support to ensure it's treated as non-copyable. It can either be passed by reference or moved to a different instance. For cases where it's used but will not transfer ownership, in
can be used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
some questions. why it is struct instead of class? is that to force having ref?
if it require special analyzer support to detect, how about creating well known interface such as IOwnerTrackingDisposable or something, that analyzer knows about, and has well known operation such as Claims, Move and etc below so that analyzer can track ownership more clearly and detect mis-usage such as assigning disposable object to another without transferring ownership?
is the point having wrapper type that can always disposed within same method?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why it is struct instead of class?
Allocation-free fast path. The type should not incur a performance penalty.
if it require special analyzer support to detect, how about creating well known interface such as IOwnerTrackingDisposable or something, that analyzer knows about, and has well known operation such as Claims, Move and etc below so that analyzer can track ownership more clearly and detect mis-usage such as assigning disposable object to another without transferring ownership?
This is intended to be that interface. The use of struct
vs interface
for it goes back to the previous item.
is the point having wrapper type that can always disposed within same method?
Yes, this is a requirement of the current IDisposable
analyzer design.
_resource = resource.Move(); | ||
} | ||
|
||
public ref OwnedDisposable<T> Resource => ref _resource; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💭 My understanding is you could unbox the value by using boxed.Resource.Move()
.
@@ -273,12 +270,19 @@ public void Shutdown() | |||
} | |||
|
|||
var connection = await client.TryCreateConnectionAsync(_serviceName, _callbackTarget, cancellationToken).ConfigureAwait(false); | |||
if (connection == null) | |||
try |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📝 Ideally you would just place the previous line in a using
statement, but this will not work due to CS1657. See #26313.
/cc @jcouv @OmarTawfik
var exception = await Assert.ThrowsAnyAsync<OperationCanceledException>(() => SessionWithSolution.CreateAsync(connection, solution, source.Token)); | ||
Assert.Equal(exception.CancellationToken, source.Token); | ||
|
||
// make sure things that should have been cleaned up are cleaned up | ||
var service = (RemotableDataServiceFactory.Service)solution.Workspace.Services.GetService<IRemotableDataService>(); | ||
Assert.Null(service.GetRemotableData_TestOnly(solutionChecksum, CancellationToken.None)); | ||
Assert.True(connection.Disposed); | ||
Assert.False(((InvokeThrowsCancellationConnection)connection.Target).Disposed); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@heejaechang This type of situation is what led me to make the comment here: #26178 (comment).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
still not sure what you mean. original code was as soon as connection is given to SessionWithSolution.CreateAsync ownership is moved according to your term. and CreateAsync got the responsibility to dispose it.
now it looks like you changed that to not move ownership until the constructor which is after test is designed to throw cancellation. so sure the test will fail. since this PR changed responsibility of disposing the connection to the extension method. previous PR explicitly tried to move the responsibility to the CreateAsync. and this basically reverted that.
(still not sure why there is Box, and then get the resource from it, rather than pass down ref Owner... thing to CreateAsync. if we are going to use this type to mark ownership, then it would be nice if this becomes a bit simpler)
anyway, you can make this test to do SessionWithSolution.CreateAsync(connection.Move(), ..) to have same semantic if you want to.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like your new pattern better though, connection can always dispose his connection without worrying about ownership. making ownership tracking simpler. but can be done by your refcountingdisposable type as well.
} | ||
|
||
public OwnedDisposable(Func<T> resource) | ||
{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure when this ctor should be used compared to the one above.
The factory function avoids the need to transfer an object into this constructor
is there reason you want to avoid transfer ownership? and it is using ref, so does it require to move ownership? a bit hard to know when I should and shouldn't move ownership. it would be nicer if one can more easily know when to call Claim or Move or use ref.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure when this ctor should be used compared to the one above.
The other constructor avoids a closure allocation, but I haven't found a way to use it without triggering one of the IDisposable
analyzer warnings. Per the primary design goal, so far this restricts the constructor usage to the form taking a delegate.
is there reason you want to avoid transfer ownership? and it is using ref, so does it require to move ownership? a bit hard to know when I should and shouldn't move ownership. it would be nicer if one can more easily know when to call Claim or Move or use ref.
These rules are not fully created yet. The answers will depend on how the previous item drives the design.
|
||
return await SessionWithSolution.CreateAsync(connection, solution, cancellationToken).ConfigureAwait(false); | ||
using (var boxed = connection.Box()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you explain when people use box? is it like if something inside of me take ownership of resource from me then no-op otherwise I will dispose it kind of thing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
➡️ Ownership of an IDisposable
resource will exist at one location in memory with this pattern. Normally this location is stored on the evaluation stack for efficiency (within the struct). However, it is not always possible to reference a memory location on the evaluation stack (e.g. when calling an async
method), so a boxed location is provided as a fallback to allow explicit ownership transfer in these cases.
@@ -51,7 +48,7 @@ internal sealed class SessionWithSolution : IDisposable | |||
} | |||
} | |||
|
|||
private SessionWithSolution(RemoteHostClient.Connection connection, PinnedRemotableDataScope scope) | |||
private SessionWithSolution(ref OwnedDisposable<RemoteHostClient.Connection> connection, PinnedRemotableDataScope scope) | |||
{ | |||
_connection = connection; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldn't this has Move? otherwise, won't using (.. Box()) will dispose the connection?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
➡️ Yes, the instance was incorrectly copied by value. Corrected in 3075c31.
I like the idea of having special type for Disposable. not sure whether we always want to use wrapper for it but I can see some benefit of having those as well. since one can always dispose it without worrying about whether it has ownership or not. ...
I think we can also use the referencecounteddispoable type thing for this purpose as well without introducing new wrapper if we want to by the way. |
I have not found a way to meet the primary design goal without a wrapper type, but we should evaluate any other solution which adheres to the constraints.
|
@@ -146,7 +160,7 @@ private void Free(string serviceName, JsonRpcConnection connection) | |||
} | |||
|
|||
// pool the connection | |||
queue.Enqueue(connection); | |||
queue.Enqueue(connection.Move()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does move mean "extra out value, and release ownership?"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Transfer ownership from the current instance to the returned value, which may be stored in the location of the new owner"
_resource = resource(); | ||
} | ||
|
||
public T Target => _resource; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this need to be an Interlocked.Read?
|
||
namespace Roslyn.Utilities | ||
{ | ||
internal struct OwnedDisposable<T> : IDisposable, IEquatable<OwnedDisposable<T>> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It feels like making this a struct makes some things more painful (like needing to ensure it's not accidentally copied, which can happen so easily). It doesn't look like this i used in hotspots that woudl warrant the need for a struct over a class. Having this be a class seems like it would simplify and ensure invariants better, while having negligible perf overhead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The issues mentioned in the original design goal make it virtually impossible to use IDisposable
objects in any non-trivial setting (e.g. a simple using
statement) without a wrapper. I'm concerned the aggregate impact would become non-trivial.
public override int GetHashCode() | ||
{ | ||
return EqualityComparer<T>.Default.GetHashCode(_resource); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should this type even be hashable? do we need that. given that it can mutate in very significant ways, it seems like possibly no. perhaps better to throw here until there is a need for this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The type passes through the hash properties of the underlying resource.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
except that this tyep is mutable and _resource can become null. That seems very weird to me. Do we have an existing use case where we need to use OwnedDisposables as keys?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
except that this tyep is mutable and _resource can become null.
It's not valid to use a resource after it is transferred away. The only defined operation at this point is Dispose()
(which becomes a NOP).
Am i correct in that this appears to be trying to provide a .net version of unique_ptr? Or is there another type out there (in the C++/rust/midori space) that this more closely emulates? Thanks! |
@sharwell - This is an interesting approach to tackling the dispose ownership problem, and probably can be recommended as a possible solution for projects that value the disposable rules but are bothered by the high false positive rate because the rule itself does not track dispose ownership transfer. |
This is almost exactly what it is. |
@mavasani It seems that |
Ok, that's what i though, and that seems very useful. My only critique is what i listed above, that i think makign this a struct actually does us little favor (though i totally can commiserate with the desire to have a zero cost abstraction). I think being a struct simply makes the usage too unweildy and confusing. In this case, this is one of those: having a mutable struct that you have to handle with utmost care just seems too onerous. I would far prefer a small-cost class-based version of this that was much simpelr to deal with, had far less niggles about how it was used, but which only cost us an allocation in a non-hot path it seems. |
Yes, FXCop rule implementation did not flag value types, and I retained the semantics. I agree we should fix it, even if this is a breaking change, it is a good one. |
For this, the suggestions by @heejaechang come into play - |
OwnedDisposable<T>
OwnedDisposable<T>
toRemoteHostClient.Connection
The types and patterns provided in this change work towards a design goal of addressing #25880 (comment) in a world where the following hold:
IDisposable
analyzers are enabled with severity ErrorAsk Mode template not completed
Customer scenario
What does the customer do to get into this situation, and why do we think this
is common enough to address for this release. (Granted, sometimes this will be
obvious "Open project, VS crashes" but in general, I need to understand how
common a scenario is)
Bugs this fixes
(either VSO or GitHub links)
Workarounds, if any
Also, why we think they are insufficient for RC vs. RC2, RC3, or RTW
Risk
This is generally a measure our how central the affected code is to adjacent
scenarios and thus how likely your fix is to destabilize a broader area of code
Performance impact
(with a brief justification for that assessment (e.g. "Low perf impact because no extra allocations/no complexity changes" vs. "Low")
Is this a regression from a previous update?
Root cause analysis
How did we miss it? What tests are we adding to guard against it in the future?
How was the bug found?
(E.g. customer reported it vs. ad hoc testing)
Test documentation updated?
If this is a new non-compiler feature or a significant improvement to an existing feature, update https://github.com/dotnet/roslyn/wiki/Manual-Testing once you know which release it is targeting.