Skip to content
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

Closed
wants to merge 4 commits into from
Closed

Conversation

sharwell
Copy link
Member

@sharwell sharwell commented Apr 22, 2018

  • Implement transferable disposables with OwnedDisposable<T>
  • Apply OwnedDisposable<T> to RemoteHostClient.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:

  • The IDisposable analyzers are enabled with severity Error
  • Warnings for the analyzers must not be suppressed
Ask 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.

@sharwell sharwell requested a review from a team as a code owner April 22, 2018 03:34
Copy link
Member Author

@sharwell sharwell left a 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();
Copy link
Member Author

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));
Copy link
Member Author

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.

Copy link
Member

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);
Copy link
Member Author

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.

Copy link
Contributor

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?

Copy link
Member Author

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)
Copy link
Member Author

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.

Copy link
Contributor

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>>
Copy link
Member Author

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.

Copy link
Contributor

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?

Copy link
Member Author

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;
Copy link
Member Author

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
Copy link
Member Author

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);
Copy link
Member Author

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).

Copy link
Contributor

@heejaechang heejaechang Apr 22, 2018

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.

Copy link
Contributor

@heejaechang heejaechang Apr 22, 2018

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.

@sharwell sharwell added Area-Analyzers PR For Personal Review Only The PR doesn’t require anyone other than the developer to review it. labels Apr 22, 2018
}

public OwnedDisposable(Func<T> resource)
{
Copy link
Contributor

@heejaechang heejaechang Apr 22, 2018

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.

Copy link
Member Author

@sharwell sharwell Apr 23, 2018

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())
Copy link
Contributor

@heejaechang heejaechang Apr 22, 2018

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?

Copy link
Member Author

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;
Copy link
Contributor

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?

Copy link
Member Author

@sharwell sharwell Apr 23, 2018

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.

@heejaechang
Copy link
Contributor

heejaechang commented Apr 22, 2018

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.

...

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.

@sharwell
Copy link
Member Author

I like the idea of having special type for Disposable. not sure whether we always want to use wrapper for it

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.

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.

ReferenceCountedDisposable<T> is a robust wrapper, but using it requires allocation overhead which would be nice to avoid.

@@ -146,7 +160,7 @@ private void Free(string serviceName, JsonRpcConnection connection)
}

// pool the connection
queue.Enqueue(connection);
queue.Enqueue(connection.Move());
Copy link
Member

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?"

Copy link
Member Author

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;
Copy link
Member

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>>
Copy link
Member

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.

Copy link
Member Author

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);
}
Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member

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?

Copy link
Member Author

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).

@CyrusNajmabadi
Copy link
Member

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!

@mavasani
Copy link
Contributor

@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.

@sharwell
Copy link
Member Author

Am i correct in that this appears to be trying to provide a .net version of unique_ptr?

This is almost exactly what it is.

@sharwell
Copy link
Member Author

@mavasani It seems that new DisposableStruct(value) does not trigger the code analysis warnings when the type is a struct. Is this a known limitation/bug?

@CyrusNajmabadi
Copy link
Member

CyrusNajmabadi commented Apr 23, 2018

This is almost exactly what it is.

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.

@mavasani
Copy link
Contributor

It seems that new DisposableStruct(value) does not trigger the code analysis warnings when the type is a struct. Is this a known limitation/bug?

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.

@sharwell
Copy link
Member Author

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.

For this, the suggestions by @heejaechang come into play - ReferenceCountedDisposable<T> may be a suitable type for general use. In addition to already being hardened against the cases needed for OwnedDisposable<T>, it can be used in robust ways with asynchronous operations and other cases where the "safe point to dispose" is not easily defined or verified in code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-Analyzers PR For Personal Review Only The PR doesn’t require anyone other than the developer to review it.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants