Add concurrent access detection tests to Dictionary<TKey, TValue>#30515
Conversation
| Assert.False(dic.GetType().GetGenericArguments()[0].IsValueType); | ||
| Assert.Equal("ThrowInvalidOperationException_ConcurrentOperationsNotSupported", Assert.Throws<InvalidOperationException>(() => dic.Add(new DummyRefType() { Value = 1 }, new DummyRefType() { Value = 1 })).TargetSite.Name); | ||
| Assert.Equal("ThrowInvalidOperationException_ConcurrentOperationsNotSupported", Assert.Throws<InvalidOperationException>(() => dic[new DummyRefType() { Value = 1 }]).TargetSite.Name); | ||
| //Remove is not resilient yet |
There was a problem hiding this comment.
Remove are not yet resilient to infinite loops...we can merge this(if are good) and after enable tests or wait for coreclr perft tests/merge.
| dic.Add(1, 1); | ||
|
|
||
| //break internal state | ||
| var entriesType = dic.GetType().GetField("_entries", BindingFlags.NonPublic | BindingFlags.Instance); |
There was a problem hiding this comment.
Could you please extract common code (eg the reflection code)
| } | ||
| } | ||
|
|
||
| public class DummyRefType |
There was a problem hiding this comment.
Why do you need special types and comparers, it seems strings and ints should suffice?
There was a problem hiding this comment.
because string is optimized...empty constructor use internal comparer https://github.com/dotnet/coreclr/blob/master/src/System.Private.CoreLib/shared/System/Collections/Generic/Dictionary.cs#L79
| customThread.Start(); | ||
|
|
||
| //Wait max 5 seconds, could loop forever | ||
| Assert.True(customThread.Join(TimeSpan.FromSeconds(5))); |
There was a problem hiding this comment.
I would make these timeouts large (eg 30 or 60 sec). Because if the machine is overloaded, they may fail in the passing case - which is bad. I have seen test machines occasionally be very slow. It is OK if they are large, because unless we regress, they will never hit the timeout. All we care about is the entire test run isn't brought down in that case. Each test library gets 20 minutes total.
| [Fact] | ||
| public static void Add_DictionaryConcurrentAccessDetection_NullComparer_ValueTypeKey() | ||
| { | ||
| Thread customThread = new Thread(() => |
There was a problem hiding this comment.
I'm probably spacing here -- but where is the concurrent access? There is only one thread accessing it?
There was a problem hiding this comment.
i'm simulating "concurrent access" breaking internal structure as if concurrent access happened. How i can be "sure" that threads will break internals?Depends on some random factors...i cannot guarantee break in n seconds(at least in my career trying to reproduce concurrent issue is not so easy...samples work well on my machine and doesn't work on other, PerformanceCounter for instance...the behaviour are different on different machine with different number of processor, different parameters to fail test), i use thread only because if for some reason in future access detection will be removed threads doesn't loop forever blocking CI. I hope I've been clear.
There was a problem hiding this comment.
Doh - of course - that was the plan and it's what you did. BTW, I wonder if you could await a Task instead of using a Thread - @ViktorHofer the Task/Thread is only to add a timeout - if we wait on a Task perhaps it won't be marshaled to another thread, so there effectively won't be a timeout?
There was a problem hiding this comment.
Task perhaps it won't be marshaled to another thread, so there effectively won't be a timeout?
I could do that with Task.Run+Task.Delay+Task.WaitAll maybe...to simulate timeout...but maybe code will be less clear.
There was a problem hiding this comment.
You could use a Task with the LongRunning option which forces the TaskScheduler to spin up a new thread.
There was a problem hiding this comment.
yes i forget Wait(TimeSpan) overload.
There was a problem hiding this comment.
@ViktorHofer @danmosemsft another idea could be use Thread with LowPriority https://source.dot.net/#System.Private.CoreLib/shared/System/Threading/ThreadPriority.cs,e968ef87c207562d, if test fail loop will be less heavy...long running task is a normal Thread https://github.com/dotnet/coreclr/blob/master/src/System.Private.CoreLib/src/System/Threading/Tasks/ThreadPoolTaskScheduler.cs#L47
There was a problem hiding this comment.
please prefer using Task over Thread if possible.
There was a problem hiding this comment.
At that point, would @MarcoRossignoli await the task, and make the test async? I suppose that doesn't give us much - the tests could overlap, but they should be very quick anyway.
| public static void Add_DictionaryConcurrentAccessDetection_Comparer_ValueTypeKey() | ||
| { | ||
| Thread customThread = new Thread(() => | ||
| { |
| customThread.Start(); | ||
|
|
||
| //Wait max 5 seconds, could loop forever | ||
| Assert.True(customThread.Join(TimeSpan.FromSeconds(5))); |
There was a problem hiding this comment.
@danmosemsft In case of timeout and assertion fail thread will run "forever" however...but as background thread at the end of test will be killed. I avoided to call Kill/Abort by myself because there are no guarantee at least on win. We cannot pass some CancellationToken to internals.
There was a problem hiding this comment.
A timeout causing an assertion failure sounds good to me.
|
@danmosemsft @ViktorHofer i've simplified test and added Task instead of Thread. |
| </Compile> | ||
| <!-- Generic tests --> | ||
| <Compile Include="Generic\Dictionary\Dictionary.Generic.Tests.netcoreapp.cs" Condition="'$(TargetGroup)' == 'netcoreapp'" /> | ||
| <Compile Include="Generic\Dictionary\Dictionary.Generic.Tests.ConcurrentAccessDetection.netcoreapp.cs" Condition="'$(TargetGroup)' == 'netcoreapp'" /> |
There was a problem hiding this comment.
It would be great if you could put all netcoreapp condition compile directives into their own ItemGroup.
...| [Theory] | ||
| [InlineData(null)] | ||
| [InlineData(typeof(CustomEqualityComparerInt32ValueType))] | ||
| async public static Task Add_DictionaryConcurrentAccessDetection_ValueTypeKey(Type comparerType) |
There was a problem hiding this comment.
public async Task Add_DictionaryConcurrentAccessDetection_ValueTypeKey. Async should be after the access modifier and there's no need for the method to be static.
There was a problem hiding this comment.
ok i used static because i'm working also on Span tests refactor...and in that project are all static..is there a guideline?
There was a problem hiding this comment.
I and most of us prefer non-static test methods.
There was a problem hiding this comment.
@karelz should we have a guideline for static vs non-static test methods?
There was a problem hiding this comment.
I and most of us prefer non-static test methods.
Also me and my team.
There was a problem hiding this comment.
I think Xunit runs the ctor for each test. So non static gives you a fresh instance. Otherwise I'm not sure we care, save consistency. @stephentoub?
There was a problem hiding this comment.
There was a problem hiding this comment.
So static class for tests are more performant.
| } | ||
| } | ||
|
|
||
| public class DummyRefType |
There was a problem hiding this comment.
Why are those types not inside a namespace? Also please use the most restrictive access modifier possible, in you case either private or internal depending on where you place the types. This also applies to the custom equalitycomparers.
|
|
||
| public class DummyRefType | ||
| { | ||
| public int Value { get; set; } |
There was a problem hiding this comment.
nit: separate methods and properties by a an empty line.
| [Theory] | ||
| [InlineData(null)] | ||
| [InlineData(typeof(CustomEqualityComparerDummyRefType))] | ||
| async public static Task Add_DictionaryConcurrentAccessDetection_ReferenceTypeKey(Type comparerType) |
| { | ||
| IEqualityComparer<int> customComparer = null; | ||
|
|
||
| Dictionary<int, int> dic = |
There was a problem hiding this comment.
nit: we usually put the comparison into the same line and only the then and else clause into a new line.
| Task task = Task.Factory.StartNew(() => | ||
| { | ||
| //break internal state | ||
| var entriesType = dictionary.GetType().GetField("_entries", BindingFlags.NonPublic | BindingFlags.Instance); |
There was a problem hiding this comment.
nit: concrete type instead of var where it's not obvious on the right hand side of the declaration.
There was a problem hiding this comment.
is this not obvious? 😄 you'are right, reflection is worste code for var!
| { | ||
| //break internal state | ||
| FieldInfo entriesType = dictionary.GetType().GetField("_entries", BindingFlags.NonPublic | BindingFlags.Instance); | ||
| object entriesInstance = (Array)entriesType.GetValue(dictionary); |
There was a problem hiding this comment.
Nit, this can be declared Array
| { | ||
| Task task = Task.Factory.StartNew(() => | ||
| { | ||
| //break internal state |
There was a problem hiding this comment.
No need to fix unless you reset the PR again...we use single space after //
Also "break internal state" could be more explicit eg " set an empty entries array to simulate a dictionary corrupted by concurrent access".
Am I reading that right? I was assuming you would set an entries array with a loop, how does an empty one work?
There was a problem hiding this comment.
Am I reading that right? I was assuming you would set an entries array with a loop, how does an empty one work?
is not empty
Array entryArray = (Array)Activator.CreateInstance(entriesInstance.GetType(), new object[] { ((IDictionary)dictionary).Count });
the idea is break alignement between states of _buckets and _entries "old" _buckets state avoid exit from loops and new _entries.next generate loop.
There was a problem hiding this comment.
Ah, I see (now I'm not reading on the phone). The correct Bucket points to an Entry with .next of 0, so it loops.
| } | ||
| } | ||
|
|
||
| class DummyRefType |
There was a problem hiding this comment.
Consider a comment explaining why you use custom types in your tests
There was a problem hiding this comment.
Done...sorry for my very basic english...help me with mistakes please 😅
danmoseley
left a comment
There was a problem hiding this comment.
I tweaked the comment. Thanks @MarcoRossignoli
Thank's and sorry for wasted time. |
|
@MarcoRossignoli you didn't waste any of my time, you saved me time by doing this work. |
|
blocked on dotnet/coreclr#18524 |
I've enabled remove tests...i think is ready to go after green CI. |
|
Thanks @MarcoRossignoli! |
mmm...1.28 PM in Italy...deep night in Redmond...i'm suspecting you're a bot @danmosemsft-bot |
…tnet/corefx#30515) * add concurrent access detection tests * add remove overloads * simplify tests * address PR feedback * remove var * nit extraline * move comparison into same line * cleanup unuseful instance * cleanup typo * improve readability * address PR feedback * Comment * Access modifier * enable removes tests Commit migrated from dotnet/corefx@24b9efc
contributes to https://github.com/dotnet/corefx/issues/30023
/cc @danmosemsft