Skip to content

Commit 59ed0cb

Browse files
committed
Merge branch 'develop'
2 parents fed1076 + e827d61 commit 59ed0cb

File tree

19 files changed

+328
-104
lines changed

19 files changed

+328
-104
lines changed

RELEASE_NOTES.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,23 @@
1-
### New in 0.47 (not released yet)
1+
### New in 0.48 (not released yet)
2+
3+
* Breaking: Moved non-async methods on `IReadModelPopulator` to extension
4+
methods
5+
* New: Added non-generic overloads for purge and populate methods on
6+
`IReadModelPopulator`
7+
* New: Provided `EventFlow.TestHelpers` which contains several test suites
8+
that is useful when developing event and read model stores for EventFlow.
9+
The package is an initial release and its interface is unstable and
10+
subject to change
11+
* New: Now possible to configure retry delay for MSSQL error `40501` (server
12+
too busy) using `IMsSqlConfiguration.SetServerBusyRetryDelay(RetryDelay)`
13+
* New: Now possible to configure the retry count of transient exceptions for
14+
MSSQL and SQLite using the `ISqlConfiguration.SetTransientRetryCount(int)`
15+
* Fixed: Added MSSQL error codes `10928`, `10929`, `18401` and `40540` as well
16+
as a few native `Win32Exception` exceptions to the list treated as transient
17+
errors, i.e., EventFlow will automatically retry if the server returns one
18+
of these
19+
20+
### New in 0.47.2894 (released 2017-06-28)
221

322
* New: To be more explicit, `IEventFlowOpions.AddSynchronousSubscriber<,,,>` and
423
`IEventFlowOpions.AddAsynchronousSubscriber<,,,>` generic methods

Source/EventFlow.MsSql/IMsSqlConfiguration.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,15 @@
2121
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
2222
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
2323

24+
using EventFlow.Core;
2425
using EventFlow.Sql.Connections;
2526

2627
namespace EventFlow.MsSql
2728
{
2829
public interface IMsSqlConfiguration : ISqlConfiguration<IMsSqlConfiguration>
2930
{
31+
RetryDelay ServerBusyRetryDelay { get; }
32+
33+
IMsSqlConfiguration SetServerBusyRetryDelay(RetryDelay retryDelay);
3034
}
3135
}

Source/EventFlow.MsSql/MsSqlConfiguration.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
2222
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
2323

24+
using System;
25+
using EventFlow.Core;
2426
using EventFlow.Sql.Connections;
2527

2628
namespace EventFlow.MsSql
@@ -32,5 +34,17 @@ public class MsSqlConfiguration : SqlConfiguration<IMsSqlConfiguration>, IMsSqlC
3234
private MsSqlConfiguration()
3335
{
3436
}
37+
38+
// From official documentation on MSDN: "The service is currently busy. Retry the request after 10 seconds"
39+
public RetryDelay ServerBusyRetryDelay { get; private set; } = RetryDelay.Between(
40+
TimeSpan.FromSeconds(10),
41+
TimeSpan.FromSeconds(15));
42+
43+
public IMsSqlConfiguration SetServerBusyRetryDelay(RetryDelay retryDelay)
44+
{
45+
ServerBusyRetryDelay = retryDelay;
46+
47+
return this;
48+
}
3549
}
3650
}

Source/EventFlow.MsSql/RetryStrategies/MsSqlErrorRetryStrategy.cs

Lines changed: 109 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@
2222
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
2323

2424
using System;
25+
using System.Collections.Generic;
26+
using System.ComponentModel;
2527
using System.Data.SqlClient;
28+
using System.Linq;
2629
using EventFlow.Core;
2730
using EventFlow.Logs;
2831

@@ -32,7 +35,6 @@ public class MsSqlErrorRetryStrategy : IMsSqlErrorRetryStrategy
3235
{
3336
private readonly ILog _log;
3437
private readonly IMsSqlConfiguration _msSqlConfiguration;
35-
private static readonly Random Random = new Random();
3638

3739
public MsSqlErrorRetryStrategy(
3840
ILog log,
@@ -42,73 +44,124 @@ public MsSqlErrorRetryStrategy(
4244
_msSqlConfiguration = msSqlConfiguration;
4345
}
4446

45-
public virtual Retry ShouldThisBeRetried(Exception exception, TimeSpan totalExecutionTime, int currentRetryCount)
47+
public virtual Retry ShouldThisBeRetried(
48+
Exception exception,
49+
TimeSpan totalExecutionTime,
50+
int currentRetryCount)
4651
{
52+
// List of possible errors inspired by Azure SqlDatabaseTransientErrorDetectionStrategy
53+
4754
var sqlException = exception as SqlException;
48-
if (sqlException == null || currentRetryCount > 2)
55+
if (sqlException == null || currentRetryCount > _msSqlConfiguration.TransientRetryCount)
4956
{
5057
return Retry.No;
5158
}
5259

53-
switch (sqlException.Number)
60+
var retry = Enumerable.Empty<Retry>()
61+
.Concat(CheckErrorCode(sqlException))
62+
.Concat(CheckInnerException(sqlException))
63+
.FirstOrDefault();
64+
65+
return retry ?? Retry.No;
66+
}
67+
68+
private IEnumerable<Retry> CheckErrorCode(SqlException sqlException)
69+
{
70+
foreach (SqlError sqlExceptionError in sqlException.Errors)
5471
{
55-
// SQL Error Code: 40501
56-
// The service is currently busy. Retry the request after 10 seconds.
57-
case 40501:
72+
// ReSharper disable once SwitchStatementMissingSomeCases
73+
switch (sqlExceptionError.Number)
74+
{
75+
// SQL Error Code: 40501
76+
// The service is currently busy. Retry the request after 10 seconds.
77+
case 40501:
5878
{
59-
var delay = TimeSpan.FromMilliseconds(5000 + (10000 * Random.NextDouble()));
79+
var delay = _msSqlConfiguration.ServerBusyRetryDelay.PickDelay();
6080
_log.Warning(
61-
"MSSQL server returned error 40501 which means it too busy! Trying to wait {0:0.###} (random between 5 and 15 seconds)",
81+
"MSSQL server returned error 40501 which means it too busy and asked us to wait 10 seconds! Trying to wait {0:0.###} seconds.",
6282
delay.TotalSeconds);
63-
return Retry.YesAfter(delay);
83+
yield return Retry.YesAfter(delay);
84+
yield break;
6485
}
6586

66-
// SQL Error Code: 40197
67-
// The service has encountered an error processing your request. Please try again.
68-
case 40197:
69-
70-
// SQL Error Code: 10053
71-
// A transport-level error has occurred when receiving results from the server.
72-
// An established connection was aborted by the software in your host machine.
73-
case 10053:
74-
75-
// SQL Error Code: 10054
76-
// A transport-level error has occurred when sending the request to the server.
77-
// (provider: TCP Provider, error: 0 - An existing connection was forcibly closed by the remote host.)
78-
case 10054:
79-
80-
// SQL Error Code: 10060
81-
// A network-related or instance-specific error occurred while establishing a connection to SQL Server.
82-
// The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server
83-
// is configured to allow remote connections. (provider: TCP Provider, error: 0 - A connection attempt failed
84-
// because the connected party did not properly respond after a period of time, or established connection failed
85-
// because connected host has failed to respond.)"}
86-
case 10060:
87-
88-
// SQL Error Code: 40613
89-
// Database XXXX on server YYYY is not currently available. Please retry the connection later. If the problem persists, contact customer
90-
// support, and provide them the session tracing ID of ZZZZZ.
91-
case 40613:
92-
93-
// SQL Error Code: 40143
94-
// The service has encountered an error processing your request. Please try again.
95-
case 40143:
96-
97-
// SQL Error Code: 233
98-
// The client was unable to establish a connection because of an error during connection initialization process before login.
99-
// Possible causes include the following: the client tried to connect to an unsupported version of SQL Server; the server was too busy
100-
// to accept new connections; or there was a resource limitation (insufficient memory or maximum allowed connections) on the server.
101-
// (provider: TCP Provider, error: 0 - An existing connection was forcibly closed by the remote host.)
102-
case 233:
103-
104-
// SQL Error Code: 64
105-
// A connection was successfully established with the server, but then an error occurred during the login process.
106-
// (provider: TCP Provider, error: 0 - The specified network name is no longer available.)
107-
case 64:
108-
return Retry.YesAfter(_msSqlConfiguration.TransientRetryDelay.PickDelay());
109-
110-
default:
111-
return Retry.No;
87+
// SQL Error Code: 40613
88+
// Database XXXX on server YYYY is not currently available. Please retry the connection later. If the problem persists, contact customer
89+
// support, and provide them the session tracing ID of ZZZZZ.
90+
case 40613:
91+
92+
// SQL Error Code: 40540
93+
// The service has encountered an error processing your request. Please try again.
94+
case 40540:
95+
96+
// SQL Error Code: 40197
97+
// The service has encountered an error processing your request. Please try again.
98+
case 40197:
99+
100+
// SQL Error Code: 40143
101+
// The service has encountered an error processing your request. Please try again.
102+
case 40143:
103+
104+
// SQL Error Code: 18401
105+
// Login failed for user '%s'. Reason: Server is in script upgrade mode. Only administrator can connect at this time.
106+
// Devnote: this can happen when SQL is going through recovery (e.g. after failover)
107+
case 18401:
108+
109+
// SQL Error Code: 10929
110+
// Resource ID: %d. The %s minimum guarantee is %d, maximum limit is %d and the current usage for the database is %d.
111+
// However, the server is currently too busy to support requests greater than %d for this database.
112+
case 10929:
113+
114+
// SQL Error Code: 10928
115+
// Resource ID: %d. The %s limit for the database is %d and has been reached.
116+
case 10928:
117+
118+
// SQL Error Code: 10060
119+
// A network-related or instance-specific error occurred while establishing a connection to SQL Server.
120+
// The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server
121+
// is configured to allow remote connections. (provider: TCP Provider, error: 0 - A connection attempt failed
122+
// because the connected party did not properly respond after a period of time, or established connection failed
123+
// because connected host has failed to respond.)"}
124+
case 10060:
125+
126+
// SQL Error Code: 10054
127+
// A transport-level error has occurred when sending the request to the server.
128+
// (provider: TCP Provider, error: 0 - An existing connection was forcibly closed by the remote host.)
129+
case 10054:
130+
131+
// SQL Error Code: 10053
132+
// A transport-level error has occurred when receiving results from the server.
133+
// An established connection was aborted by the software in your host machine.
134+
case 10053:
135+
136+
// SQL Error Code: 233
137+
// The client was unable to establish a connection because of an error during connection initialization process before login.
138+
// Possible causes include the following: the client tried to connect to an unsupported version of SQL Server; the server was too busy
139+
// to accept new connections; or there was a resource limitation (insufficient memory or maximum allowed connections) on the server.
140+
// (provider: TCP Provider, error: 0 - An existing connection was forcibly closed by the remote host.)
141+
case 233:
142+
143+
// SQL Error Code: 64
144+
// A connection was successfully established with the server, but then an error occurred during the login process.
145+
// (provider: TCP Provider, error: 0 - The specified network name is no longer available.)
146+
case 64:
147+
yield return Retry.YesAfter(_msSqlConfiguration.TransientRetryDelay.PickDelay());
148+
yield break;
149+
}
150+
}
151+
}
152+
153+
private IEnumerable<Retry> CheckInnerException(SqlException sqlException)
154+
{
155+
// Prelogin failure can happen due to waits expiring on windows handles. Or
156+
// due to bugs in the gateway code, a dropped database with a pooled connection
157+
// when reset results in a timeout error instead of immediate failure.
158+
159+
var win32Exception = sqlException.InnerException as Win32Exception;
160+
if (win32Exception == null) yield break;
161+
162+
if (win32Exception.NativeErrorCode == 0x102 || win32Exception.NativeErrorCode == 0x121)
163+
{
164+
yield return Retry.YesAfter(_msSqlConfiguration.TransientRetryDelay.PickDelay());
112165
}
113166
}
114167
}

Source/EventFlow.SQLite/RetryStrategies/SQLiteErrorRetryStrategy.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public SQLiteErrorRetryStrategy(
4141
public Retry ShouldThisBeRetried(Exception exception, TimeSpan totalExecutionTime, int currentRetryCount)
4242
{
4343
var sqLiteException = exception as SQLiteException;
44-
if (sqLiteException == null || currentRetryCount > 2)
44+
if (sqLiteException == null || currentRetryCount > _configuration.TransientRetryCount)
4545
{
4646
return Retry.No;
4747
}

Source/EventFlow.Sql/Connections/ISqlConfiguration.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,11 @@ namespace EventFlow.Sql.Connections
2828
public interface ISqlConfiguration<out T>
2929
where T : ISqlConfiguration<T>
3030
{
31-
RetryDelay TransientRetryDelay { get; }
3231
string ConnectionString { get; }
32+
RetryDelay TransientRetryDelay { get; }
33+
int TransientRetryCount { get; }
3334

3435
T SetTransientRetryDelay(RetryDelay retryDelay);
36+
T SetTransientRetryCount(int retryCount);
3537
}
3638
}

Source/EventFlow.Sql/Connections/SqlConfiguration.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ public abstract class SqlConfiguration<T> : ISqlConfiguration<T>
3535
TimeSpan.FromMilliseconds(50),
3636
TimeSpan.FromMilliseconds(100));
3737

38+
public int TransientRetryCount { get; private set; } = 2;
39+
3840
public T SetConnectionString(string connectionString)
3941
{
4042
ConnectionString = connectionString;
@@ -50,5 +52,13 @@ public T SetTransientRetryDelay(RetryDelay retryDelay)
5052
// Are there alternatives to this double cast?
5153
return (T)(object)this;
5254
}
55+
56+
public T SetTransientRetryCount(int retryCount)
57+
{
58+
TransientRetryCount = retryCount;
59+
60+
// Are there alternatives to this double cast?
61+
return (T)(object)this;
62+
}
5363
}
5464
}

Source/EventFlow.TestHelpers/EventFlow.TestHelpers.csproj

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,29 @@
44
<TargetFramework>net451</TargetFramework>
55
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
66
<GenerateAssemblyInfo>False</GenerateAssemblyInfo>
7+
<GeneratePackageOnBuild>False</GeneratePackageOnBuild>
8+
<PackageRequireLicenseAcceptance>True</PackageRequireLicenseAcceptance>
9+
<Title>EventFlow.TestHelpers</Title>
10+
<Authors>Rasmus Mikkelsen</Authors>
11+
<Company>Rasmus Mikkelsen</Company>
12+
<Copyright>Copyright (c) Rasmus Mikkelsen 2015 - 2017</Copyright>
13+
<Description>
14+
A collection of test helpers used to help develop event and read model stores for EventFlow. Please
15+
note that this is an alpha initial release of the test helpers package and content is subject
16+
to change.
17+
</Description>
18+
<PackageTags>CQRS ES event sourcing</PackageTags>
19+
<RepositoryType>git</RepositoryType>
20+
<RepositoryUrl>https://github.com/eventflow/EventFlow</RepositoryUrl>
21+
<PackageProjectUrl>http://docs.geteventflow.net/</PackageProjectUrl>
22+
<PackageIconUrl>https://raw.githubusercontent.com/eventflow/EventFlow/develop/icon-128.png</PackageIconUrl>
23+
<PackageLicenseUrl>https://raw.githubusercontent.com/eventflow/EventFlow/develop/LICENSE</PackageLicenseUrl>
24+
<NeutralLanguage>en-US</NeutralLanguage>
25+
<PackageReleaseNotes>UPDATED BY BUILD</PackageReleaseNotes>
726
</PropertyGroup>
827
<ItemGroup>
928
<PackageReference Include="AutoFixture.AutoMoq" Version="3.50.2" />
1029
<PackageReference Include="FluentAssertions" Version="4.19.2" />
11-
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.0.0" />
1230
<PackageReference Include="Moq" Version="4.7.9" />
1331
<PackageReference Include="NUnit" Version="3.6.1" />
1432
</ItemGroup>

Source/EventFlow.TestHelpers/TcpHelper.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,12 @@ public static int GetFreePort()
3636
{
3737
var ipGlobalProperties = IPGlobalProperties.GetIPGlobalProperties();
3838
var activeTcpListeners = ipGlobalProperties.GetActiveTcpListeners();
39-
var ports = new HashSet<int>(activeTcpListeners.Select(p => p.Port));
39+
var activeTcpConnections = ipGlobalProperties.GetActiveTcpConnections();
40+
41+
var ports = new HashSet<int>(Enumerable.Empty<int>()
42+
.Concat(activeTcpListeners.Select(l => l.Port)
43+
.Concat(activeTcpConnections.Select(c => c.LocalEndPoint.Port))
44+
));
4045

4146
while (true)
4247
{

Source/EventFlow.Tests/UnitTests/ReadStores/ReadModelPopulatorTests.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ public void SetUp()
7171
.Setup(r => r.Resolve<IEnumerable<IReadStoreManager>>())
7272
.Returns(new[] {_readStoreManagerMock.Object});
7373
_resolverMock
74-
.Setup(r => r.Resolve<IEnumerable<IReadModelStore<TestReadModel>>>())
74+
.Setup(r => r.ResolveAll(typeof(IReadModelStore<TestReadModel>)))
7575
.Returns(new[] {_readModelStoreMock.Object});
7676

7777
_eventFlowConfigurationMock
@@ -81,6 +81,9 @@ public void SetUp()
8181
_eventStoreMock
8282
.Setup(s => s.LoadAllEventsAsync(It.IsAny<GlobalPosition>(), It.IsAny<int>(), It.IsAny<CancellationToken>()))
8383
.Returns<GlobalPosition, int, CancellationToken>((s, p, c) => Task.FromResult(GetEvents(s, p)));
84+
_readStoreManagerMock
85+
.Setup(m => m.ReadModelType)
86+
.Returns(typeof(TestReadModel));
8487
}
8588

8689
[Test]

0 commit comments

Comments
 (0)