Skip to content

Commit 3fea388

Browse files
condronjoshkempner
authored andcommitted
remove spin wait from is or becomes true
test execution speed improvements
1 parent f0fc805 commit 3fea388

File tree

4 files changed

+76
-38
lines changed

4 files changed

+76
-38
lines changed

src/ReactiveDomain.Foundation.Tests/when_using_read_model_base.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ public when_using_read_model_base(StreamStoreConnectionFixture fixture)
3737

3838
AppendEvents(10, _conn, _stream1, 2);
3939
AppendEvents(10, _conn, _stream2, 3);
40-
_conn.TryConfirmStream(_stream1, 10);
40+
41+
_conn.TryConfirmStream(_stream1, 10);
4142
_conn.TryConfirmStream(_stream2, 10);
4243
_conn.TryConfirmStream(Namer.GenerateForCategory(typeof(TestAggregate)), 20);
4344
}
@@ -116,7 +117,7 @@ public void can_wait_for_two_streams_to_go_live()
116117

117118
Start(_stream2, null, true);
118119
AssertEx.IsModelVersion(this, 21, 100, msg: $"Expected 21 got {Version}");
119-
AssertEx.IsOrBecomesTrue(() => Sum == 50, 100);
120+
AssertEx.IsOrBecomesTrue(() => Sum == 50, 150);
120121
}
121122
[Fact]
122123
public void can_listen_to_one_stream()

src/ReactiveDomain.Testing/AssertEx.cs

Lines changed: 53 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
using System;
2+
using System.Data.SqlTypes;
23
using System.Threading;
4+
using System.Threading.Tasks;
35
using ReactiveDomain.Foundation;
46
using ReactiveDomain.Messaging.Bus;
7+
using ReactiveDomain.Util;
58
using Xunit;
69

710

@@ -40,47 +43,64 @@ public static void ArraySegmentEqual<T>(
4043
}
4144

4245
/// <summary>
43-
/// Asserts the given function will return false before the timeout expires.
44-
/// Repeatedly evaluates the function until false is returned or the timeout expires.
45-
/// Will return immediately when the condition is false.
46-
/// Evaluates the timeout every 10 msec until expired.
47-
/// Will not yield the thread by default, if yielding is required to resolve deadlocks set yieldThread to true.
46+
/// Asserts the given function will return false before the timeout expires.
47+
/// Repeatedly evaluates the function until false is returned or the timeout expires.
48+
/// Will return immediately when the condition is false.
49+
/// Evaluates the condition on an exponential back off up to 250 ms until timeout.
50+
/// Will yield the thread after each evaluation, use the hint yieldThread if it is known the function should yield before evaluation.
4851
/// </summary>
4952
/// <param name="func">The function to evaluate.</param>
5053
/// <param name="timeout">A timeout in milliseconds. If not specified, defaults to 1000.</param>
5154
/// <param name="msg">A message to display if the condition is not satisfied.</param>
52-
/// <param name="yieldThread">If true, the thread relinquishes the remainder of its time
53-
/// slice to any thread of equal priority that is ready to run.</param>
55+
/// <param name="yieldThread">Execution hint to yield thread before evaluation</param>
5456
public static void IsOrBecomesFalse(Func<bool> func, int? timeout = null, string msg = null, bool yieldThread = false)
5557
{
5658
IsOrBecomesTrue(() => !func(), timeout, msg, yieldThread);
5759
}
5860

5961
/// <summary>
60-
/// Asserts the given function will return true before the timeout expires.
61-
/// Repeatedly evaluates the function until true is returned or the timeout expires.
62-
/// Will return immediately when the condition is true.
63-
/// Evaluates the timeout every 10 msec until expired.
64-
/// Will not yield the thread by default, if yielding is required to resolve deadlocks set yieldThread to true.
62+
/// Asserts the given function will return true before the timeout expires.
63+
/// Repeatedly evaluates the function until true is returned or the timeout expires.
64+
/// Will return immediately when the condition is true.
65+
/// Evaluates the condition on an exponential back off up to 250 ms until timeout.
66+
/// Will yield the thread after each evaluation, use the hint yieldThread if it is known the function should yield before evaluation.
6567
/// </summary>
6668
/// <param name="func">The function to evaluate.</param>
6769
/// <param name="timeout">A timeout in milliseconds. If not specified, defaults to 1000.</param>
6870
/// <param name="msg">A message to display if the condition is not satisfied.</param>
69-
/// <param name="yieldThread">If true, the thread relinquishes the remainder of its time
70-
/// slice to any thread of equal priority that is ready to run.</param>
71+
/// <param name="yieldThread">Execution hint to yield thread before evaluation</param>
7172
public static void IsOrBecomesTrue(Func<bool> func, int? timeout = null, string msg = null, bool yieldThread = false)
7273
{
73-
if (yieldThread) Thread.Sleep(0);
74-
if (!timeout.HasValue) timeout = 1000;
75-
var waitLoops = timeout / 10;
74+
if (!yieldThread && func() == true)
75+
{
76+
Assert.True(true, msg ?? "");
77+
return;
78+
}
79+
7680
var result = false;
77-
for (int i = 0; i < waitLoops; i++)
81+
var startTime = Environment.TickCount; //returns MS since machine start
82+
var endTime = startTime + (timeout ?? 1000);
83+
84+
85+
var delay = 1;
86+
while (true)
7887
{
79-
if (SpinWait.SpinUntil(func, 10))
88+
using (var task = EvaluateAfterDelay(func, TimeSpan.FromMilliseconds(delay)))
8089
{
81-
result = true;
82-
break;
90+
task.Wait();
91+
if (task.Result == true)
92+
{
93+
result = true;
94+
break;
95+
}
8396
}
97+
var now = Environment.TickCount;
98+
if ((endTime - now) <= 0) { break; }
99+
if (delay < 250)
100+
{
101+
delay = delay << 1;
102+
}
103+
delay = Math.Min(delay, endTime - now);
84104
}
85105
Assert.True(result, msg ?? "");
86106
}
@@ -116,5 +136,17 @@ public static void AtLeastModelVersion(ReadModelBase readModel, int expectedVers
116136
{
117137
IsOrBecomesTrue(() => readModel.Version >= expectedVersion, timeout, msg, yieldThread);
118138
}
139+
/// <summary>
140+
/// Evaluates the given function after the supplied delay.
141+
/// The current thread will yield at the start of the delay.
142+
/// </summary>
143+
/// <param name="func">The function to evaluate.</param>
144+
/// <param name="delay">A delay timeSpan.</param>
145+
/// <param name="cancellationToken">A token to cancel evaluation, TaskCanceledException will be thrown.</param>
146+
public static async Task<bool> EvaluateAfterDelay(Func<bool> func, TimeSpan delay, CancellationToken cancellationToken = default)
147+
{
148+
await Task.Delay(delay, cancellationToken);
149+
return func();
150+
}
119151
}
120152
}

src/ReactiveDomain.Testing/ConnectionUtil.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ public static bool TryConfirmStream(this IStreamStoreConnection conn, string str
77
while (true) {
88
try {
99
var slice = conn.ReadStreamForward(streamTypeName, StreamPosition.Start, 500);
10-
if (slice.IsEndOfStream && slice.Events.Length == expectedEventCount) {
10+
if (slice.IsEndOfStream && slice.Events.Length >= expectedEventCount) {
1111
return true;
1212
}
1313
Thread.Sleep(10);

src/ReactiveDomain.Testing/Specifications/TestQueue.cs

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public TestQueue(
4949
{
5050
_isFiltered = messageTypeFilter != null;
5151
_msgTypeFilter = messageTypeFilter ?? new Type[] { };
52-
52+
5353
_trackTypes = trackTypes;
5454

5555
Messages = new ConcurrentMessageQueue<IMessage>("Messages");
@@ -108,11 +108,11 @@ public void Clear()
108108
/// <param name="timeout">How long to wait before timing out.</param>
109109
public void WaitFor<T>(TimeSpan timeout) where T : IMessage
110110
{
111-
WaitForMultiple<T>(1, timeout);
111+
WaitForMultiple<T>(1, timeout);
112112
}
113113

114114
/// <summary>
115-
/// Wait for some number of messages of type T to appear in the queue.
115+
/// Wait for some number of messages compatible with T (via the 'is' operator) to appear in the queue.
116116
/// </summary>
117117
/// <typeparam name="T">The type to wait for.</typeparam>
118118
/// <param name="num">The number of messages of the given type to wait for.</param>
@@ -121,22 +121,27 @@ public void WaitForMultiple<T>(uint num, TimeSpan timeout) where T : IMessage
121121
{
122122
if (_disposed) { throw new ObjectDisposedException(nameof(TestQueue)); }
123123
if (!_trackTypes) { throw new InvalidOperationException("Type tracking is disabled for this instance."); }
124-
var deadline = DateTime.Now + timeout;
125-
do
124+
125+
var startTime = Environment.TickCount; //returns MS since machine start
126+
var endTime = startTime + (int)timeout.TotalMilliseconds;
127+
128+
var delay = 1;
129+
while (true)
126130
{
127-
if (SpinWait.SpinUntil(() => Messages.Count(x => x is T) >= num, 50))
128-
{
129-
return;
130-
}
131-
if (DateTime.Now > deadline)
131+
if (_disposed) { throw new ObjectDisposedException(nameof(TestQueue)); }
132+
//Evaluating the entire queue is a bit heavy, but is required to support waiting on base types, interfaces, etc.
133+
using (var task = AssertEx.EvaluateAfterDelay(() => Messages.Count(x => x is T) >= num, TimeSpan.FromMilliseconds(delay)))
132134
{
133-
throw new TimeoutException();
135+
task.Wait();
136+
if (task.Result == true) { break; }
134137
}
135-
if (_disposed) { throw new ObjectDisposedException(nameof(TestQueue)); }
138+
var now = Environment.TickCount;
139+
if ((endTime - now) <= 0) { throw new TimeoutException(); }
136140

137-
} while (true);
141+
if (delay < 250) { delay = delay << 1; }
142+
delay = Math.Min(delay, endTime - now);
143+
}
138144
}
139-
140145
/// <summary>
141146
/// Wait for a message with a specific MsgId to appear in the queue.
142147
/// </summary>

0 commit comments

Comments
 (0)