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

Avoid race condition in map for TupleElementIndex #58500

Merged
merged 7 commits into from
Feb 11, 2022
Merged

Conversation

jcouv
Copy link
Member

@jcouv jcouv commented Dec 26, 2021

The race condition is that PENamedTypeSymbol.LoadMembers calls MakeSynthesizedTupleMembers and then saves members in _lazyMembersInDeclarationOrder. But MakeSynthesizedTupleMembers is having a side-effect which saves some members in a map.
If two threads call LoadMembers concurrently, the members in _lazyMembersInDeclarationOrder and in _lazyFieldDefinitionsToIndexMap may not match (ie. they are coming from two different threads that are loading symbols).

Here are some of the existing tests that are relevant: TupleWithElementNamedWithDefaultName, TestValueTuple8Definition, IndexOfUnderlyingFieldsInTuple9, UnderlyingTypeMemberWithWrongSignature_1.

Fixes #52658 (flaky value of TupleElementIndex in TupleWithElementNamedWithDefaultName test)
Fixes #57560 (crash in TupleWithElementNamedWithDefaultName test now that AssertOrFailFast was added)

This PR maintains existing behavior, but I think it's not quite right. Filed #58499


return found is not null
? tupleElementPosition - 1
: -1;
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Dec 26, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is any of this expensive? would it be worth caching the reuslt of this? #Resolved

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Considered this question and I lean towards not caching.
When using tuple syntax, we create TupleElementFieldSymbol for default elements (and TupleVirtualElementFieldSymbol for named elements) which stores location and name. So this path is only used when accessing a field directly from ValueTuple type (from source or PE) and that'd be less common.
I've filed an issue for discussion which proposes to remove this wrapping with TupleElementFieldSymbol. If we go down that route then caching would make more sense.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cool, that makes sense to me :)

{
if (name.StartsWith("Item", StringComparison.Ordinal))
string tail = name.Substring(4);
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Dec 26, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
string tail = name.Substring(4);
string tail = name.Substring("Item".Length);

(will be the exact same jitted code, but this helps make it clearer what's going on (imo)). #Resolved

Copy link
Member

@cston cston Feb 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will be the exact same jitted code

It looks like get_Length() is called (see sharplab.io).

@jcouv jcouv marked this pull request as ready for review January 3, 2022 17:01
@jcouv jcouv requested a review from a team as a code owner January 3, 2022 17:01
WellKnownMember wellKnownMember = NamedTypeSymbol.GetTupleTypeMember(arity, tupleElementPosition);
MemberDescriptor descriptor = WellKnownMembers.GetDescriptor(wellKnownMember);
Symbol found = CSharpCompilation.GetRuntimeMember(ImmutableArray.Create<Symbol>(this), descriptor, CSharpCompilation.SpecialMembersSignatureComparer.Instance,
accessWithinOpt: null); // force lookup of public members only
Copy link
Member

@cston cston Jan 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we call comparer.MatchFieldSignature(this, descriptor.Signature) directly instead? #ByDesign

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetRuntimeMember checks a few other things (static-ness, accessibility). I'd rather stay close to logic we used in MakeSynthesizedTupleMembers.

/// This supports <see cref="FieldSymbol.TupleElementIndex"/>.
/// For those fields, we map from their definition to an index.
/// </summary>
private SmallDictionary<FieldSymbol, int>? _lazyFieldDefinitionsToIndexMap;
Copy link
Contributor

@AlekseyTs AlekseyTs Jan 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_lazyFieldDefinitionsToIndexMap

Are we certain that nothing else in this class is prone to a similar race? Would it be simpler and more robust to ensure that members and tuple data are in sync by using an alternative approach? For example, by ensuring that they are stored together at once. #Resolved

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've reviewed the other parts of TupleExtraData and didn't spot a similar race condition.
That said, there is a race condition left somewhere (issue #53943) which seems different to the one fixed here. I haven't identified the root cause yet.

For the suggestion to have MakeSynthesizedTupleMembers return the map and have the caller store it, we'd discussed this with Chuck among a few options before the holiday break (but we leaned towards removing the map). I'm open to the approach if Chuck is okay with it.
If we do go that route, do you have an initialization pattern for the caller? Feels like we'd need to spin-wait the threads that are waiting for the winning thread to save the map.

@AlekseyTs
Copy link
Contributor

I think that the more robust way to avoid this and similar races would be to change MakeSynthesizedTupleMembers to not mutate an instance of symbol, but instead return an instance of TupleExtraData that corresponds to members it looked at (perhaps it can take an instance of TupleExtraData to populate as an input as well), that should be stored together with the members by the consumer. Not only that would guarantee that we would address the current race, but that would address other races we overlooked and would prevent us from re-introducing a race in the future. That would also reduce the volume of required changes in a quite complicated logic.

@jcouv jcouv marked this pull request as draft January 10, 2022 19:48
@RikkiGibson RikkiGibson self-assigned this Feb 4, 2022
Comment on lines +491 to +492
Symbol found = CSharpCompilation.GetRuntimeMember(ImmutableArray.Create<Symbol>(this), descriptor, CSharpCompilation.SpecialMembersSignatureComparer.Instance,
accessWithinOpt: null); // force lookup of public members only
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
Symbol found = CSharpCompilation.GetRuntimeMember(ImmutableArray.Create<Symbol>(this), descriptor, CSharpCompilation.SpecialMembersSignatureComparer.Instance,
accessWithinOpt: null); // force lookup of public members only
Symbol found = CSharpCompilation.GetRuntimeMember(
ImmutableArray.Create<Symbol>(this),
descriptor,
CSharpCompilation.SpecialMembersSignatureComparer.Instance,
accessWithinOpt: null); // force lookup of public members only

// ex: no "Item2" in 'ValueTuple<T1>'
return -1;
}
Debug.Assert(tupleElementPosition < NamedTypeSymbol.ValueTupleRestPosition);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please clarify why we can assume this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this assert be moved to immediately following the if (!ContainingType.IsDefinition) { ... } block?

@RikkiGibson
Copy link
Contributor

Is this PR ready to merge?

@jcouv jcouv enabled auto-merge (squash) February 11, 2022 00:59
@jcouv jcouv merged commit 4d1d4b9 into dotnet:main Feb 11, 2022
@ghost ghost added this to the Next milestone Feb 11, 2022
@jcouv jcouv deleted the tuple-map branch February 11, 2022 02:47
@RikkiGibson RikkiGibson modified the milestones: Next, 17.2.P2 Mar 1, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
5 participants