Skip to content

Commit 567ffe8

Browse files
Merge pull request #682 from fredericDelaporte/NH-4077-Master
NH-4077 - do not auto-flush while already flushing
2 parents 5d8219b + 0cba4e0 commit 567ffe8

File tree

15 files changed

+868
-225
lines changed

15 files changed

+868
-225
lines changed
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
//------------------------------------------------------------------------------
2+
// <auto-generated>
3+
// This code was generated by AsyncGenerator.
4+
//
5+
// Changes to this file may cause incorrect behavior and will be lost if
6+
// the code is regenerated.
7+
// </auto-generated>
8+
//------------------------------------------------------------------------------
9+
10+
11+
using System;
12+
using System.Linq;
13+
using NHibernate.Cfg;
14+
using NHibernate.Cfg.MappingSchema;
15+
using NHibernate.Event;
16+
using NHibernate.Mapping.ByCode;
17+
using NUnit.Framework;
18+
19+
namespace NHibernate.Test.NHSpecificTest.NH4077
20+
{
21+
using System.Threading.Tasks;
22+
using System.Threading;
23+
[TestFixture]
24+
public partial class PostInsertFixtureAsync : TestCaseMappingByCode
25+
{
26+
[Test]
27+
public async Task AutoflushInPostInsertListener_CausesDuplicateInserts_WithPrimaryKeyViolationsAsync()
28+
{
29+
using (var session = OpenSession())
30+
using (var transaction = session.BeginTransaction())
31+
{
32+
// using FlushMode.Commit prevents the issue; using the default FlushMode.Auto breaks.
33+
//session.FlushMode = FlushMode.Commit;
34+
await (session.SaveAsync(new Entity { Code = "one" }));
35+
await (session.SaveAsync(new Entity { Code = "two" }));
36+
37+
// committing the transaction causes a primary key violation by saving the entities multiple times
38+
await (transaction.CommitAsync());
39+
await (session.FlushAsync());
40+
}
41+
}
42+
43+
[Test]
44+
public async Task Autoflush_MayTriggerAdditionalAutoFlushFromEventsAsync()
45+
{
46+
using (var session = OpenSession())
47+
using (var transaction = session.BeginTransaction())
48+
{
49+
// using FlushMode.Commit prevents the issue; using the default FlushMode.Auto breaks.
50+
//session.FlushMode = FlushMode.Commit;
51+
await (session.SaveAsync(new Entity { Code = "one" }));
52+
await (session.SaveAsync(new Entity { Code = "two" }));
53+
54+
// Querying the entity triggers an auto-flush
55+
var count = await (session.CreateQuery("select count(o) from Entity o").UniqueResultAsync<long>());
56+
Assert.That(count, Is.GreaterThan(0));
57+
await (transaction.CommitAsync());
58+
await (session.FlushAsync());
59+
}
60+
}
61+
62+
protected override HbmMapping GetMappings()
63+
{
64+
var mapper = new ModelMapper();
65+
mapper.Class<Entity>(rc =>
66+
{
67+
rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
68+
rc.Property(x => x.Code);
69+
});
70+
71+
return mapper.CompileMappingForAllExplicitlyAddedEntities();
72+
}
73+
74+
protected override void Configure(Configuration configuration)
75+
{
76+
base.Configure(configuration);
77+
var existingListeners = (configuration.EventListeners.PostInsertEventListeners ?? new IPostInsertEventListener[0]).ToList();
78+
// this evil listener uses the session to perform a few queries and causes an auto-flush to happen
79+
existingListeners.Add(new CausesAutoflushListener());
80+
configuration.EventListeners.PostInsertEventListeners = existingListeners.ToArray();
81+
}
82+
83+
protected override void OnTearDown()
84+
{
85+
using (var session = OpenSession())
86+
using (var transaction = session.BeginTransaction())
87+
{
88+
session.Delete("from Entity");
89+
90+
session.Flush();
91+
transaction.Commit();
92+
}
93+
}
94+
95+
private sealed partial class CausesAutoflushListener : IPostInsertEventListener
96+
{
97+
private bool _currentlyLogging;
98+
99+
public async Task OnPostInsertAsync(PostInsertEvent @event, CancellationToken cancellationToken)
100+
{
101+
if (!(@event.Entity is Entity))
102+
return;
103+
// This guard is necessary to avoid multiple inserts of the original objects.
104+
// Commenting this out is likely to cause one PK violation per run, which seems to be capped to at most 10 attempts.
105+
// With the guard, only one PK violation is reported.
106+
if (_currentlyLogging)
107+
return;
108+
109+
try
110+
{
111+
_currentlyLogging = true;
112+
var session = @event.Session;
113+
// this causes an Autoflush
114+
long count = await (session.CreateQuery("select count(o) from Entity o").UniqueResultAsync<long>(cancellationToken));
115+
Console.WriteLine("Total entity count: {0}", count);
116+
}
117+
finally
118+
{
119+
_currentlyLogging = false;
120+
}
121+
}
122+
123+
public void OnPostInsert(PostInsertEvent @event)
124+
{
125+
if (!(@event.Entity is Entity))
126+
return;
127+
// This guard is necessary to avoid multiple inserts of the original objects.
128+
// Commenting this out is likely to cause one PK violation per run, which seems to be capped to at most 10 attempts.
129+
// With the guard, only one PK violation is reported.
130+
if (_currentlyLogging)
131+
return;
132+
133+
try
134+
{
135+
_currentlyLogging = true;
136+
var session = @event.Session;
137+
// this causes an Autoflush
138+
long count = session.CreateQuery("select count(o) from Entity o").UniqueResult<long>();
139+
Console.WriteLine("Total entity count: {0}", count);
140+
}
141+
finally
142+
{
143+
_currentlyLogging = false;
144+
}
145+
}
146+
}
147+
}
148+
/// <content>
149+
/// Contains generated async methods
150+
/// </content>
151+
public partial class PostInsertFixture : TestCaseMappingByCode
152+
{
153+
154+
/// <content>
155+
/// Contains generated async methods
156+
/// </content>
157+
private sealed partial class CausesAutoflushListener : IPostInsertEventListener
158+
{
159+
160+
public async Task OnPostInsertAsync(PostInsertEvent @event, CancellationToken cancellationToken)
161+
{
162+
if (!(@event.Entity is Entity))
163+
return;
164+
// This guard is necessary to avoid multiple inserts of the original objects.
165+
// Commenting this out is likely to cause one PK violation per run, which seems to be capped to at most 10 attempts.
166+
// With the guard, only one PK violation is reported.
167+
if (_currentlyLogging)
168+
return;
169+
170+
try
171+
{
172+
_currentlyLogging = true;
173+
var session = @event.Session;
174+
// this causes an Autoflush
175+
long count = await (session.CreateQuery("select count(o) from Entity o").UniqueResultAsync<long>(cancellationToken));
176+
Console.WriteLine("Total entity count: {0}", count);
177+
}
178+
finally
179+
{
180+
_currentlyLogging = false;
181+
}
182+
}
183+
}
184+
}
185+
}
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
//------------------------------------------------------------------------------
2+
// <auto-generated>
3+
// This code was generated by AsyncGenerator.
4+
//
5+
// Changes to this file may cause incorrect behavior and will be lost if
6+
// the code is regenerated.
7+
// </auto-generated>
8+
//------------------------------------------------------------------------------
9+
10+
11+
using System;
12+
using System.Linq;
13+
using NHibernate.Cfg;
14+
using NHibernate.Cfg.MappingSchema;
15+
using NHibernate.Criterion;
16+
using NHibernate.Event;
17+
using NHibernate.Mapping.ByCode;
18+
using NUnit.Framework;
19+
20+
namespace NHibernate.Test.NHSpecificTest.NH4077
21+
{
22+
using System.Threading.Tasks;
23+
using System.Threading;
24+
[TestFixture]
25+
public partial class PostUpdateFixtureAsync : TestCaseMappingByCode
26+
{
27+
[Test]
28+
public async Task AutoflushInPostUpdateListener_CausesArgumentOutOfRangeException_in_ActionQueueExecuteActionsAsync()
29+
{
30+
// load a few (more than one) entities and process them. we let NHibernate figure out if they need saving or not.
31+
Entity entityOne;
32+
Entity entityTwo;
33+
using (var session = OpenSession())
34+
{
35+
entityOne = (await (session.CreateCriteria<Entity>().Add(Restrictions.Eq(nameof(Entity.Code), "one")).ListAsync<Entity>())).First();
36+
entityTwo = (await (session.CreateCriteria<Entity>().Add(Restrictions.Eq(nameof(Entity.Code), "two")).ListAsync<Entity>())).First();
37+
}
38+
39+
// processing omitted (not necessary to illustrate the problem)
40+
41+
// resave them, but all-or-nothing inside a transaction
42+
using (var session = OpenSession())
43+
using (var transaction = session.BeginTransaction())
44+
{
45+
// using FlushMode.Commit prevents the issue; using the default FlushMode.Auto breaks.
46+
//session.FlushMode = FlushMode.Commit;
47+
await (session.SaveOrUpdateAsync(entityOne));
48+
await (session.SaveOrUpdateAsync(entityTwo));
49+
50+
// committing the transaction causes an ArgumentOutOfRange exception inside ActionQueue.ExecuteActions
51+
await (transaction.CommitAsync());
52+
await (session.FlushAsync());
53+
}
54+
}
55+
56+
protected override HbmMapping GetMappings()
57+
{
58+
var mapper = new ModelMapper();
59+
mapper.Class<Entity>(rc =>
60+
{
61+
rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
62+
rc.Property(x => x.Code);
63+
});
64+
65+
return mapper.CompileMappingForAllExplicitlyAddedEntities();
66+
}
67+
68+
protected override void Configure(Configuration configuration)
69+
{
70+
base.Configure(configuration);
71+
var existingListeners = (configuration.EventListeners.PostUpdateEventListeners ?? new IPostUpdateEventListener[0]).ToList();
72+
// this evil listener uses the session to perform a few queries and causes an auto-flush to happen
73+
existingListeners.Add(new CausesAutoflushListener());
74+
configuration.EventListeners.PostUpdateEventListeners = existingListeners.ToArray();
75+
}
76+
77+
protected override void OnTearDown()
78+
{
79+
using (var session = OpenSession())
80+
using (var transaction = session.BeginTransaction())
81+
{
82+
session.Delete("from Entity");
83+
84+
session.Flush();
85+
transaction.Commit();
86+
}
87+
}
88+
89+
protected override void OnSetUp()
90+
{
91+
using (var session = OpenSession())
92+
using (var transaction = session.BeginTransaction())
93+
{
94+
// objects must exist before doing the processing; the issue does not occur during
95+
session.Save(new Entity { Code = "one" });
96+
session.Save(new Entity { Code = "two" });
97+
98+
session.Flush();
99+
transaction.Commit();
100+
}
101+
}
102+
103+
private sealed partial class CausesAutoflushListener : IPostUpdateEventListener
104+
{
105+
private bool _currentlyLogging;
106+
107+
public async Task OnPostUpdateAsync(PostUpdateEvent @event, CancellationToken cancellationToken)
108+
{
109+
if (!(@event.Entity is Entity))
110+
return;
111+
// this guard is necessary to avoid a StackOverflowException due to the Query below triggering this event again.
112+
if (_currentlyLogging)
113+
return;
114+
115+
try
116+
{
117+
_currentlyLogging = true;
118+
var session = @event.Session;
119+
// this causes an Autoflush
120+
long count = await (session.CreateQuery("select count(o) from Entity o").UniqueResultAsync<long>(cancellationToken));
121+
Console.WriteLine("Total entity count: {0}", count);
122+
}
123+
finally
124+
{
125+
_currentlyLogging = false;
126+
}
127+
}
128+
129+
public void OnPostUpdate(PostUpdateEvent @event)
130+
{
131+
if (!(@event.Entity is Entity))
132+
return;
133+
// this guard is necessary to avoid a StackOverflowException due to the Query below triggering this event again.
134+
if (_currentlyLogging)
135+
return;
136+
137+
try
138+
{
139+
_currentlyLogging = true;
140+
var session = @event.Session;
141+
// this causes an Autoflush
142+
long count = session.CreateQuery("select count(o) from Entity o").UniqueResult<long>();
143+
Console.WriteLine("Total entity count: {0}", count);
144+
}
145+
finally
146+
{
147+
_currentlyLogging = false;
148+
}
149+
}
150+
}
151+
}
152+
/// <content>
153+
/// Contains generated async methods
154+
/// </content>
155+
public partial class PostUpdateFixture : TestCaseMappingByCode
156+
{
157+
158+
/// <content>
159+
/// Contains generated async methods
160+
/// </content>
161+
private sealed partial class CausesAutoflushListener : IPostUpdateEventListener
162+
{
163+
164+
public async Task OnPostUpdateAsync(PostUpdateEvent @event, CancellationToken cancellationToken)
165+
{
166+
if (!(@event.Entity is Entity))
167+
return;
168+
// this guard is necessary to avoid a StackOverflowException due to the Query below triggering this event again.
169+
if (_currentlyLogging)
170+
return;
171+
172+
try
173+
{
174+
_currentlyLogging = true;
175+
var session = @event.Session;
176+
// this causes an Autoflush
177+
long count = await (session.CreateQuery("select count(o) from Entity o").UniqueResultAsync<long>(cancellationToken));
178+
Console.WriteLine("Total entity count: {0}", count);
179+
}
180+
finally
181+
{
182+
_currentlyLogging = false;
183+
}
184+
}
185+
}
186+
}
187+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using System;
2+
3+
namespace NHibernate.Test.NHSpecificTest.NH4077
4+
{
5+
public class Entity
6+
{
7+
public virtual Guid Id { get; set; }
8+
public virtual string Code { get; set; }
9+
}
10+
}

0 commit comments

Comments
 (0)