Skip to content

Commit

Permalink
Query: Fix #5481 - RC2 Deadlock problem with CountAsync() that does n…
Browse files Browse the repository at this point in the history
…ot happen with Count()

- Replace IX-Async GroupJoin with async/await impl.
  • Loading branch information
anpete committed Jun 8, 2016
1 parent 9637721 commit 5f55720
Show file tree
Hide file tree
Showing 3 changed files with 243 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -402,12 +402,128 @@ private static readonly MethodInfo _groupJoin
[UsedImplicitly]
// ReSharper disable once InconsistentNaming
private static IAsyncEnumerable<TResult> _GroupJoin<TOuter, TInner, TKey, TResult>(
IAsyncEnumerable<TOuter> outer,
IAsyncEnumerable<TInner> inner,
Func<TOuter, TKey> outerKeySelector,
Func<TInner, TKey> innerKeySelector,
Func<TOuter, IAsyncEnumerable<TInner>, TResult> resultSelector)
=> outer.GroupJoin(inner, outerKeySelector, innerKeySelector, resultSelector);
IAsyncEnumerable<TOuter> outer,
IAsyncEnumerable<TInner> inner,
Func<TOuter, TKey> outerKeySelector,
Func<TInner, TKey> innerKeySelector,
Func<TOuter, IAsyncEnumerable<TInner>, TResult> resultSelector)
=> new GroupJoinAsyncEnumerable<TOuter, TInner, TKey, TResult>(
outer, inner, outerKeySelector, innerKeySelector, resultSelector);

private sealed class GroupJoinAsyncEnumerable<TOuter, TInner, TKey, TResult> : IAsyncEnumerable<TResult>
{
private readonly IAsyncEnumerable<TOuter> _outer;
private readonly IAsyncEnumerable<TInner> _inner;
private readonly Func<TOuter, TKey> _outerKeySelector;
private readonly Func<TInner, TKey> _innerKeySelector;
private readonly Func<TOuter, IAsyncEnumerable<TInner>, TResult> _resultSelector;

public GroupJoinAsyncEnumerable(
IAsyncEnumerable<TOuter> outer,
IAsyncEnumerable<TInner> inner,
Func<TOuter, TKey> outerKeySelector,
Func<TInner, TKey> innerKeySelector,
Func<TOuter, IAsyncEnumerable<TInner>, TResult> resultSelector)
{
_outer = outer;
_inner = inner;
_outerKeySelector = outerKeySelector;
_innerKeySelector = innerKeySelector;
_resultSelector = resultSelector;
}

public IAsyncEnumerator<TResult> GetEnumerator()
=> new GroupJoinAsyncEnumerator(
_outer.GetEnumerator(),
_inner.GetEnumerator(),
_outerKeySelector,
_innerKeySelector,
_resultSelector);

private sealed class GroupJoinAsyncEnumerator : IAsyncEnumerator<TResult>
{
private readonly IAsyncEnumerator<TOuter> _outer;
private readonly IAsyncEnumerator<TInner> _inner;
private readonly Func<TOuter, TKey> _outerKeySelector;
private readonly Func<TInner, TKey> _innerKeySelector;
private readonly Func<TOuter, IAsyncEnumerable<TInner>, TResult> _resultSelector;

private Dictionary<TKey, List<TInner>> _innerGroups;

public GroupJoinAsyncEnumerator(
IAsyncEnumerator<TOuter> outer,
IAsyncEnumerator<TInner> inner,
Func<TOuter, TKey> outerKeySelector,
Func<TInner, TKey> innerKeySelector,
Func<TOuter, IAsyncEnumerable<TInner>, TResult> resultSelector)
{
_outer = outer;
_inner = inner;
_outerKeySelector = outerKeySelector;
_innerKeySelector = innerKeySelector;
_resultSelector = resultSelector;
}

public async Task<bool> MoveNext(CancellationToken cancellationToken)
{
List<TInner> group;

if (!await _outer.MoveNext(cancellationToken))
{
return false;
}

if (_innerGroups == null)
{
_innerGroups = new Dictionary<TKey, List<TInner>>();

while (await _inner.MoveNext(cancellationToken))
{
var inner = _inner.Current;
var innerKey = _innerKeySelector(inner);

if (innerKey != null)
{
if (!_innerGroups.TryGetValue(innerKey, out group))
{
_innerGroups.Add(innerKey, group = new List<TInner>());
}

group.Add(inner);
}
}
}

var outer = _outer.Current;
var outerKey = _outerKeySelector(outer);

Current
= _resultSelector(
outer,
new AsyncEnumerableAdapter<TInner>(
outerKey != null
&& _innerGroups.TryGetValue(outerKey, out group)
? (IEnumerable<TInner>)group
: EmptyEnumerable<TInner>.Instance));

return true;
}

public TResult Current { get; private set; }

public void Dispose()
{
_inner.Dispose();
_outer.Dispose();
}

[UsedImplicitly]
private sealed class EmptyEnumerable<TElement>
{
public static readonly TElement[] Instance = new TElement[0];
}
}
}

/// <summary>
/// This API supports the Entity Framework Core infrastructure and is not intended to be used
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.EntityFrameworkCore.Specification.Tests;
using Microsoft.EntityFrameworkCore.Specification.Tests.TestModels.Northwind;
using Microsoft.EntityFrameworkCore.Specification.Tests.TestUtilities.Xunit;
using Xunit;
using Xunit.Abstractions;

#pragma warning disable 1998

namespace Microsoft.EntityFrameworkCore.SqlServer.FunctionalTests
{
[MonoVersionCondition(Min = "4.2.0", SkipReason = "Async queries will not work on Mono < 4.2.0 due to differences in the IQueryable interface")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore.Specification.Tests;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Specification.Tests;
using Microsoft.EntityFrameworkCore.SqlServer.FunctionalTests.Utilities;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
Expand All @@ -18,11 +19,110 @@
// ReSharper disable ReturnValueOfPureMethodIsNotUsed
// ReSharper disable UnusedAutoPropertyAccessor.Local
// ReSharper disable UnusedMember.Local

namespace Microsoft.EntityFrameworkCore.SqlServer.FunctionalTests
{
public class QueryBugsTest : IClassFixture<SqlServerFixture>
{
[Fact]
public async Task Multiple_optional_navs_should_not_deadlock_bug_5481()
{
using (var testStore = SqlServerTestStore.CreateScratch())
{
using (var context = new DeadlockContext(testStore.ConnectionString))
{
context.Database.EnsureCreated();
context.EnsureSeeded();

var count
= await context.Persons
.Where(p => (p.AddressOne != null && p.AddressOne.Street.Contains("Low Street"))
|| (p.AddressTwo != null && p.AddressTwo.Street.Contains("Low Street")))
.CountAsync();

Assert.Equal(0, count);
}
}
}

private class DeadlockContext : DbContext
{
private readonly string _connectionString;

public DeadlockContext(string connectionString)
{
_connectionString = connectionString;
}

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.UseSqlServer(_connectionString);

public DbSet<Person> Persons { get; set; }
public DbSet<Address> Addresses { get; set; }

public class Address
{
public int Id { get; set; }
public string Street { get; set; }
public int PersonId { get; set; }
public Person Person { get; set; }
}

public class Person
{
public int Id { get; set; }
public string Name { get; set; }
public int? AddressOneId { get; set; }
public Address AddressOne { get; set; }
public int? AddressTwoId { get; set; }
public Address AddressTwo { get; set; }
}

public void EnsureSeeded()
{
if (!Persons.Any())
{
AddRange(
new Person { Name = "John Doe" },
new Person { Name = "Joe Bloggs" });

SaveChanges();
}
}

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);

modelBuilder.Entity<Person>().HasKey(p => p.Id);

modelBuilder.Entity<Person>().Property(p => p.Name)
.IsRequired();

modelBuilder.Entity<Person>().HasOne(p => p.AddressOne)
.WithMany()
.HasForeignKey(p => p.AddressOneId)
.OnDelete(DeleteBehavior.Restrict);

modelBuilder.Entity<Person>().Property(p => p.AddressOneId);

modelBuilder.Entity<Person>().HasOne(p => p.AddressTwo)
.WithMany()
.HasForeignKey(p => p.AddressTwoId)
.OnDelete(DeleteBehavior.Restrict);

modelBuilder.Entity<Person>().Property(p => p.AddressTwoId);

modelBuilder.Entity<Address>().HasKey(a => a.Id);

modelBuilder.Entity<Address>().Property(a => a.Street).IsRequired(true);

modelBuilder.Entity<Address>().HasOne(a => a.Person)
.WithMany()
.HasForeignKey(a => a.PersonId)
.OnDelete(DeleteBehavior.Restrict);
}
}

[Fact]
public void Query_when_null_key_in_database_should_throw()
{
Expand Down Expand Up @@ -951,7 +1051,6 @@ from eRootJoined in RootEntities.DefaultIfEmpty()
}
}


[Fact]
public virtual void Repro3101_coalesce_tracking()
{
Expand Down Expand Up @@ -986,26 +1085,26 @@ private void CreateDatabase3101()
_fixture.ServiceProvider,
(sp, co) => new MyContext3101(sp),
context =>
{
var c11 = new Child3101 { Name = "c11" };
var c12 = new Child3101 { Name = "c12" };
var c13 = new Child3101 { Name = "c13" };
var c21 = new Child3101 { Name = "c21" };
var c22 = new Child3101 { Name = "c22" };
var c31 = new Child3101 { Name = "c31" };
var c32 = new Child3101 { Name = "c32" };
{
var c11 = new Child3101 { Name = "c11" };
var c12 = new Child3101 { Name = "c12" };
var c13 = new Child3101 { Name = "c13" };
var c21 = new Child3101 { Name = "c21" };
var c22 = new Child3101 { Name = "c22" };
var c31 = new Child3101 { Name = "c31" };
var c32 = new Child3101 { Name = "c32" };

context.Children.AddRange(c11, c12, c13, c21, c22, c31, c32);
context.Children.AddRange(c11, c12, c13, c21, c22, c31, c32);

var e1 = new Entity3101 { Id = 1, Children = new[] { c11, c12, c13 } };
var e2 = new Entity3101 { Id = 2, Children = new[] { c21, c22 } };
var e3 = new Entity3101 { Id = 3, Children = new[] { c31, c32 } };
var e1 = new Entity3101 { Id = 1, Children = new[] { c11, c12, c13 } };
var e2 = new Entity3101 { Id = 2, Children = new[] { c21, c22 } };
var e3 = new Entity3101 { Id = 3, Children = new[] { c31, c32 } };

e2.RootEntity = e1;
e2.RootEntity = e1;

context.Entities.AddRange(e1, e2, e3);
context.SaveChanges();
});
context.Entities.AddRange(e1, e2, e3);
context.SaveChanges();
});
}

public class MyContext3101 : DbContext
Expand All @@ -1021,8 +1120,6 @@ public MyContext3101(IServiceProvider serviceProvider)

public DbSet<Child3101> Children { get; set; }



protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.UseSqlServer(SqlServerTestStore.CreateConnectionString("Repro3101"));

Expand All @@ -1036,7 +1133,7 @@ public class Entity3101
{
public Entity3101()
{
this.Children = new Collection<Child3101>();
Children = new Collection<Child3101>();
}

public int Id { get; set; }
Expand Down

0 comments on commit 5f55720

Please sign in to comment.