Skip to content

NH-4077 - do not auto-flush while already flushing #682

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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
185 changes: 185 additions & 0 deletions src/NHibernate.Test/Async/NHSpecificTest/NH4077/PostInsertFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by AsyncGenerator.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------


using System;
using System.Linq;
using NHibernate.Cfg;
using NHibernate.Cfg.MappingSchema;
using NHibernate.Event;
using NHibernate.Mapping.ByCode;
using NUnit.Framework;

namespace NHibernate.Test.NHSpecificTest.NH4077
{
using System.Threading.Tasks;
using System.Threading;
[TestFixture]
public partial class PostInsertFixtureAsync : TestCaseMappingByCode
{
[Test]
public async Task AutoflushInPostInsertListener_CausesDuplicateInserts_WithPrimaryKeyViolationsAsync()
{
using (var session = OpenSession())
using (var transaction = session.BeginTransaction())
{
// using FlushMode.Commit prevents the issue; using the default FlushMode.Auto breaks.
//session.FlushMode = FlushMode.Commit;
await (session.SaveAsync(new Entity { Code = "one" }));
await (session.SaveAsync(new Entity { Code = "two" }));

// committing the transaction causes a primary key violation by saving the entities multiple times
await (transaction.CommitAsync());
await (session.FlushAsync());
}
}

[Test]
public async Task Autoflush_MayTriggerAdditionalAutoFlushFromEventsAsync()
{
using (var session = OpenSession())
using (var transaction = session.BeginTransaction())
{
// using FlushMode.Commit prevents the issue; using the default FlushMode.Auto breaks.
//session.FlushMode = FlushMode.Commit;
await (session.SaveAsync(new Entity { Code = "one" }));
await (session.SaveAsync(new Entity { Code = "two" }));

// Querying the entity triggers an auto-flush
var count = await (session.CreateQuery("select count(o) from Entity o").UniqueResultAsync<long>());
Assert.That(count, Is.GreaterThan(0));
await (transaction.CommitAsync());
await (session.FlushAsync());
}
}

protected override HbmMapping GetMappings()
{
var mapper = new ModelMapper();
mapper.Class<Entity>(rc =>
{
rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
rc.Property(x => x.Code);
});

return mapper.CompileMappingForAllExplicitlyAddedEntities();
}

protected override void Configure(Configuration configuration)
{
base.Configure(configuration);
var existingListeners = (configuration.EventListeners.PostInsertEventListeners ?? new IPostInsertEventListener[0]).ToList();
// this evil listener uses the session to perform a few queries and causes an auto-flush to happen
existingListeners.Add(new CausesAutoflushListener());
configuration.EventListeners.PostInsertEventListeners = existingListeners.ToArray();
}

protected override void OnTearDown()
{
using (var session = OpenSession())
using (var transaction = session.BeginTransaction())
{
session.Delete("from Entity");

session.Flush();
transaction.Commit();
}
}

private sealed partial class CausesAutoflushListener : IPostInsertEventListener
{
private bool _currentlyLogging;

public async Task OnPostInsertAsync(PostInsertEvent @event, CancellationToken cancellationToken)
{
if (!(@event.Entity is Entity))
return;
// This guard is necessary to avoid multiple inserts of the original objects.
// Commenting this out is likely to cause one PK violation per run, which seems to be capped to at most 10 attempts.
// With the guard, only one PK violation is reported.
if (_currentlyLogging)
return;

try
{
_currentlyLogging = true;
var session = @event.Session;
// this causes an Autoflush
long count = await (session.CreateQuery("select count(o) from Entity o").UniqueResultAsync<long>(cancellationToken));
Console.WriteLine("Total entity count: {0}", count);
}
finally
{
_currentlyLogging = false;
}
}

public void OnPostInsert(PostInsertEvent @event)
{
if (!(@event.Entity is Entity))
return;
// This guard is necessary to avoid multiple inserts of the original objects.
// Commenting this out is likely to cause one PK violation per run, which seems to be capped to at most 10 attempts.
// With the guard, only one PK violation is reported.
if (_currentlyLogging)
return;

try
{
_currentlyLogging = true;
var session = @event.Session;
// this causes an Autoflush
long count = session.CreateQuery("select count(o) from Entity o").UniqueResult<long>();
Console.WriteLine("Total entity count: {0}", count);
}
finally
{
_currentlyLogging = false;
}
}
}
}
/// <content>
/// Contains generated async methods
/// </content>
public partial class PostInsertFixture : TestCaseMappingByCode
{

/// <content>
/// Contains generated async methods
/// </content>
private sealed partial class CausesAutoflushListener : IPostInsertEventListener
{

public async Task OnPostInsertAsync(PostInsertEvent @event, CancellationToken cancellationToken)
{
if (!(@event.Entity is Entity))
return;
// This guard is necessary to avoid multiple inserts of the original objects.
// Commenting this out is likely to cause one PK violation per run, which seems to be capped to at most 10 attempts.
// With the guard, only one PK violation is reported.
if (_currentlyLogging)
return;

try
{
_currentlyLogging = true;
var session = @event.Session;
// this causes an Autoflush
long count = await (session.CreateQuery("select count(o) from Entity o").UniqueResultAsync<long>(cancellationToken));
Console.WriteLine("Total entity count: {0}", count);
}
finally
{
_currentlyLogging = false;
}
}
}
}
}
187 changes: 187 additions & 0 deletions src/NHibernate.Test/Async/NHSpecificTest/NH4077/PostUpdateFixture.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by AsyncGenerator.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------


using System;
using System.Linq;
using NHibernate.Cfg;
using NHibernate.Cfg.MappingSchema;
using NHibernate.Criterion;
using NHibernate.Event;
using NHibernate.Mapping.ByCode;
using NUnit.Framework;

namespace NHibernate.Test.NHSpecificTest.NH4077
{
using System.Threading.Tasks;
using System.Threading;
[TestFixture]
public partial class PostUpdateFixtureAsync : TestCaseMappingByCode
{
[Test]
public async Task AutoflushInPostUpdateListener_CausesArgumentOutOfRangeException_in_ActionQueueExecuteActionsAsync()
{
// load a few (more than one) entities and process them. we let NHibernate figure out if they need saving or not.
Entity entityOne;
Entity entityTwo;
using (var session = OpenSession())
{
entityOne = (await (session.CreateCriteria<Entity>().Add(Restrictions.Eq(nameof(Entity.Code), "one")).ListAsync<Entity>())).First();
entityTwo = (await (session.CreateCriteria<Entity>().Add(Restrictions.Eq(nameof(Entity.Code), "two")).ListAsync<Entity>())).First();
}

// processing omitted (not necessary to illustrate the problem)

// resave them, but all-or-nothing inside a transaction
using (var session = OpenSession())
using (var transaction = session.BeginTransaction())
{
// using FlushMode.Commit prevents the issue; using the default FlushMode.Auto breaks.
//session.FlushMode = FlushMode.Commit;
await (session.SaveOrUpdateAsync(entityOne));
await (session.SaveOrUpdateAsync(entityTwo));

// committing the transaction causes an ArgumentOutOfRange exception inside ActionQueue.ExecuteActions
await (transaction.CommitAsync());
await (session.FlushAsync());
}
}

protected override HbmMapping GetMappings()
{
var mapper = new ModelMapper();
mapper.Class<Entity>(rc =>
{
rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
rc.Property(x => x.Code);
});

return mapper.CompileMappingForAllExplicitlyAddedEntities();
}

protected override void Configure(Configuration configuration)
{
base.Configure(configuration);
var existingListeners = (configuration.EventListeners.PostUpdateEventListeners ?? new IPostUpdateEventListener[0]).ToList();
// this evil listener uses the session to perform a few queries and causes an auto-flush to happen
existingListeners.Add(new CausesAutoflushListener());
configuration.EventListeners.PostUpdateEventListeners = existingListeners.ToArray();
}

protected override void OnTearDown()
{
using (var session = OpenSession())
using (var transaction = session.BeginTransaction())
{
session.Delete("from Entity");

session.Flush();
transaction.Commit();
}
}

protected override void OnSetUp()
{
using (var session = OpenSession())
using (var transaction = session.BeginTransaction())
{
// objects must exist before doing the processing; the issue does not occur during
session.Save(new Entity { Code = "one" });
session.Save(new Entity { Code = "two" });

session.Flush();
transaction.Commit();
}
}

private sealed partial class CausesAutoflushListener : IPostUpdateEventListener
{
private bool _currentlyLogging;

public async Task OnPostUpdateAsync(PostUpdateEvent @event, CancellationToken cancellationToken)
{
if (!(@event.Entity is Entity))
return;
// this guard is necessary to avoid a StackOverflowException due to the Query below triggering this event again.
if (_currentlyLogging)
return;

try
{
_currentlyLogging = true;
var session = @event.Session;
// this causes an Autoflush
long count = await (session.CreateQuery("select count(o) from Entity o").UniqueResultAsync<long>(cancellationToken));
Console.WriteLine("Total entity count: {0}", count);
}
finally
{
_currentlyLogging = false;
}
}

public void OnPostUpdate(PostUpdateEvent @event)
{
if (!(@event.Entity is Entity))
return;
// this guard is necessary to avoid a StackOverflowException due to the Query below triggering this event again.
if (_currentlyLogging)
return;

try
{
_currentlyLogging = true;
var session = @event.Session;
// this causes an Autoflush
long count = session.CreateQuery("select count(o) from Entity o").UniqueResult<long>();
Console.WriteLine("Total entity count: {0}", count);
}
finally
{
_currentlyLogging = false;
}
}
}
}
/// <content>
/// Contains generated async methods
/// </content>
public partial class PostUpdateFixture : TestCaseMappingByCode
{

/// <content>
/// Contains generated async methods
/// </content>
private sealed partial class CausesAutoflushListener : IPostUpdateEventListener
{

public async Task OnPostUpdateAsync(PostUpdateEvent @event, CancellationToken cancellationToken)
{
if (!(@event.Entity is Entity))
return;
// this guard is necessary to avoid a StackOverflowException due to the Query below triggering this event again.
if (_currentlyLogging)
return;

try
{
_currentlyLogging = true;
var session = @event.Session;
// this causes an Autoflush
long count = await (session.CreateQuery("select count(o) from Entity o").UniqueResultAsync<long>(cancellationToken));
Console.WriteLine("Total entity count: {0}", count);
}
finally
{
_currentlyLogging = false;
}
}
}
}
}
10 changes: 10 additions & 0 deletions src/NHibernate.Test/NHSpecificTest/NH4077/Model.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System;

namespace NHibernate.Test.NHSpecificTest.NH4077
{
public class Entity
{
public virtual Guid Id { get; set; }
public virtual string Code { get; set; }
}
}
Loading