Open AGBrown opened 6 years ago
I initially created this at zzzprojects/EntityFramework-Effort#113, but then realised it more likely applies here.
Oh man, parallel issues are so much fun to track down!
I eventually managed to recreate the issue and I seem to have proved my hunch that it was due to the ObjectDataCollection
not being a thread safe collection. I've added simple locking in the AddOrUpdate
and TryGetValue
methods. Fortunately it's an internal class and those are the only methods I use, so I don't think I'll need to worry about the inherited methods.
Are you able to test from the latest commit before I build and push to NuGet, or will I need to create a release before you can test it?
Are you able to test from the latest commit before I build and push to NuGet
Sure thing. I'll give it a go.
TL;WR: I repro'd the error using a test; I packed a nuget at at both cb18ba471f544ef409870f969ec9043f9f3ceeb3 and 26c5c034f2632e9cfe4dc1b2a34a0f337529de18; the repro test passes on both those commits so from the test's point of view they are good to go.
I've put together an MVCE test using NUnit parallel tests that repeatedly fails on the current 1.4.0 nuget. Anecdotally it fails every 1 in 5 (ish) runs with just two parallel tests. The failures are various (not just the KeyNotFoundException, but also exceptions including "System.InvalidOperationException : Collection was modified; enumeration operation may not execute").
I've then packed a nuget at both cb18ba471f544ef409870f969ec9043f9f3ceeb3 and 26c5c034f2632e9cfe4dc1b2a34a0f337529de18.
The MVCE test does not fail on either of the two new commits with either 16 or 32 parallel threads (equal to the number of cores and number of hyper threads on the dev machine); I've then repeated that test run of n parallel tests 100x using nunit console runner and I wasn't able to get a test failure.
I've not been able to add the tests to your project (as we discussed) as I don't have an accessible VS2017 instance running to properly add the code ... sorry. I think you mentioned you've got tests that repro the issue anyway so I'm not sure you need mine. Here they are just in case:
ChildModel.cs (expand for source code).
```c# using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace EffortTests { [Table("ChildModel")] public class ChildModel { [Required] [Key] public int Id { get; set; } [Required] [StringLength(50)] public string Nk { get; set; } [Required] public int ParentModelId { get; set; } public ParentModel ParentModel { get; set; } } } ```
ParentModel.cs
```c#
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace EffortTests
{
[Table("ParentModel")]
public class ParentModel
{
public ParentModel()
{
ChildModels = new List
```c#
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Common;
using System.Data.Entity;
namespace EffortTests
{
public class TestDbContext : DbContext
{
static TestDbContext()
{
Database.SetInitializer
```c#
using System.Data.Common;
using System.Data.Entity;
using System.Linq;
using Effort;
using Effort.Extra;
using NUnit.Framework;
using Ploeh.AutoFixture;
namespace EffortTests
{
[TestFixture]
public class EffortDemoFixture
{
#region Fixture setup and DbContext factory methods
///
/// Creates a
private static TestDbContext CreateTestDbContext()
{
var connection = DbConnectionFactory.CreateTransient();
try
{
return CreateTestDbContext(connection);
}
catch (System.Exception)
{
connection.Dispose();
throw;
}
}
///
/// Creates a
private static TestDbContext CreateTestDbContext(DbConnection connection, bool contextOwnsConnection = true)
{
return new TestDbContext(connection, contextOwnsConnection);
}
#endregion Fixture setup and DbContext factory methods
[Test]
public void CanUseEffort()
{
// ARRANGE -------------------------------------------------------
using (var context = CreateTestDbContext())
{
context.Database.CreateIfNotExists();
// ACT -----------------------------------------------------------
var dbSet = context.Set
Environment:
Nugets and versions
(expand for packages.config).
```xml
```
Test are run in parallel using `[assembly: Parallelizable(ParallelScope.All)]`. ##### Sample test This exception can be reproduced with a single test, duplicated several times within the fixture and run in parallel on a multi-core workstation either within visual studio or using the nunit console runner. The sample test code is:(expand for full code).
```c# [Test] public void CanUseEffort() { var fixture = new Fixture(); // autofixture // --- Use Extra.Effort to pre-seed the database var data = new ObjectData(); var newModel = new Apple { Id = 1, Name = fixture.Create() };
data.Table().Add(newModel);
var dataLoader = new ObjectDataLoader(data);
using (var connection = DbConnectionFactory.CreateTransient(dataLoader))
using (var context = new TestDbContext(connection, false))
{
// Initialise the transient database - this fails intermittently
context.Database.CreateIfNotExists();
var dbSet = context.Apples;
var actual = dbSet.ToList();
Assert.That(actual.Count, Is.EqualTo(1));
}
}
```