Skip to content

Commit 2bbb6d8

Browse files
BhaaLseNfredericDelaporte
authored andcommitted
NH-4077 - create test cases
both fixtures use an evil PostXxxListener to cause/force an auto-flush of the session that is currently being committed inside a transaction. for PostInsertFixture it leads to to duplicate inserts. for PostUpdateFixture it causes an ArgumentOutOfRange exception by clearing a list that is currently being iterated.
1 parent 5d8219b commit 2bbb6d8

File tree

5 files changed

+574
-0
lines changed

5 files changed

+574
-0
lines changed
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
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+
protected override HbmMapping GetMappings()
44+
{
45+
var mapper = new ModelMapper();
46+
mapper.Class<Entity>(rc =>
47+
{
48+
rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb));
49+
rc.Property(x => x.Code);
50+
});
51+
52+
return mapper.CompileMappingForAllExplicitlyAddedEntities();
53+
}
54+
55+
protected override void Configure(Configuration configuration)
56+
{
57+
base.Configure(configuration);
58+
var existingListeners = (configuration.EventListeners.PostInsertEventListeners ?? new IPostInsertEventListener[0]).ToList();
59+
// this evil listener uses the session to perform a few queries and causes an auto-flush to happen
60+
existingListeners.Add(new CausesAutoflushListener());
61+
configuration.EventListeners.PostInsertEventListeners = existingListeners.ToArray();
62+
}
63+
64+
protected override void OnTearDown()
65+
{
66+
using (var session = OpenSession())
67+
using (var transaction = session.BeginTransaction())
68+
{
69+
session.Delete("from Entity");
70+
71+
session.Flush();
72+
transaction.Commit();
73+
}
74+
}
75+
76+
private sealed partial class CausesAutoflushListener : IPostInsertEventListener
77+
{
78+
private bool _currentlyLogging;
79+
80+
public async Task OnPostInsertAsync(PostInsertEvent @event, CancellationToken cancellationToken)
81+
{
82+
if (!(@event.Entity is Entity))
83+
return;
84+
// This guard is necessary to avoid multiple inserts of the original objects.
85+
// Commenting this out is likely to cause one PK violation per run, which seems to be capped to at most 10 attempts.
86+
// With the guard, only one PK violation is reported.
87+
if (_currentlyLogging)
88+
return;
89+
90+
try
91+
{
92+
_currentlyLogging = true;
93+
var session = @event.Session;
94+
// this causes an Autoflush
95+
long count = await (session.CreateQuery("select count(o) from Entity o").UniqueResultAsync<long>(cancellationToken));
96+
Console.WriteLine("Total entity count: {0}", count);
97+
}
98+
finally
99+
{
100+
_currentlyLogging = false;
101+
}
102+
}
103+
104+
public void OnPostInsert(PostInsertEvent @event)
105+
{
106+
if (!(@event.Entity is Entity))
107+
return;
108+
// This guard is necessary to avoid multiple inserts of the original objects.
109+
// Commenting this out is likely to cause one PK violation per run, which seems to be capped to at most 10 attempts.
110+
// With the guard, only one PK violation is reported.
111+
if (_currentlyLogging)
112+
return;
113+
114+
try
115+
{
116+
_currentlyLogging = true;
117+
var session = @event.Session;
118+
// this causes an Autoflush
119+
long count = session.CreateQuery("select count(o) from Entity o").UniqueResult<long>();
120+
Console.WriteLine("Total entity count: {0}", count);
121+
}
122+
finally
123+
{
124+
_currentlyLogging = false;
125+
}
126+
}
127+
}
128+
}
129+
/// <content>
130+
/// Contains generated async methods
131+
/// </content>
132+
public partial class PostInsertFixture : TestCaseMappingByCode
133+
{
134+
135+
/// <content>
136+
/// Contains generated async methods
137+
/// </content>
138+
private sealed partial class CausesAutoflushListener : IPostInsertEventListener
139+
{
140+
141+
public async Task OnPostInsertAsync(PostInsertEvent @event, CancellationToken cancellationToken)
142+
{
143+
if (!(@event.Entity is Entity))
144+
return;
145+
// This guard is necessary to avoid multiple inserts of the original objects.
146+
// Commenting this out is likely to cause one PK violation per run, which seems to be capped to at most 10 attempts.
147+
// With the guard, only one PK violation is reported.
148+
if (_currentlyLogging)
149+
return;
150+
151+
try
152+
{
153+
_currentlyLogging = true;
154+
var session = @event.Session;
155+
// this causes an Autoflush
156+
long count = await (session.CreateQuery("select count(o) from Entity o").UniqueResultAsync<long>(cancellationToken));
157+
Console.WriteLine("Total entity count: {0}", count);
158+
}
159+
finally
160+
{
161+
_currentlyLogging = false;
162+
}
163+
}
164+
}
165+
}
166+
}
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)