Skip to content

Commit 5cbc40f

Browse files
committed
Fixed concurrency issue #607
Do not reuse SqlCommand because apparently it is not threadsafe. Set connection on command leads to a exception in audit sink under heavy load.
1 parent 2b3e2fc commit 5cbc40f

File tree

14 files changed

+60
-292
lines changed

14 files changed

+60
-292
lines changed

src/Serilog.Sinks.MSSqlServer/Sinks/MSSqlServer/MSSqlServerAuditSink.cs

Lines changed: 1 addition & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,10 @@ namespace Serilog.Sinks.MSSqlServer
2626
/// Writes log events as rows in a table of MSSqlServer database using Audit logic, meaning that each row is synchronously committed
2727
/// and any errors that occur are propagated to the caller.
2828
/// </summary>
29-
public class MSSqlServerAuditSink : ILogEventSink, IDisposable
29+
public class MSSqlServerAuditSink : ILogEventSink
3030
{
3131
private readonly ISqlLogEventWriter _sqlLogEventWriter;
3232

33-
private bool _disposedValue;
34-
3533
/// <summary>
3634
/// Construct a sink posting to the specified database.
3735
///
@@ -99,33 +97,6 @@ internal MSSqlServerAuditSink(
9997
public void Emit(LogEvent logEvent) =>
10098
_sqlLogEventWriter.WriteEvent(logEvent);
10199

102-
/// <summary>
103-
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
104-
/// </summary>
105-
public void Dispose()
106-
{
107-
Dispose(true);
108-
GC.SuppressFinalize(this);
109-
}
110-
111-
/// <summary>
112-
/// Releases the unmanaged resources used by the Serilog.Sinks.MSSqlServer.MSSqlServerAuditSink and optionally
113-
/// releases the managed resources.
114-
/// </summary>
115-
/// <param name="disposing">True to release both managed and unmanaged resources; false to release only unmanaged resources.</param>
116-
protected virtual void Dispose(bool disposing)
117-
{
118-
if (!_disposedValue)
119-
{
120-
if (disposing)
121-
{
122-
_sqlLogEventWriter.Dispose();
123-
}
124-
125-
_disposedValue = true;
126-
}
127-
}
128-
129100
private static void ValidateParameters(MSSqlServerSinkOptions sinkOptions, ColumnOptions columnOptions)
130101
{
131102
if (sinkOptions?.TableName == null)

src/Serilog.Sinks.MSSqlServer/Sinks/MSSqlServer/MSSqlServerSink.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,6 @@ protected virtual void Dispose(bool disposing)
152152
if (disposing)
153153
{
154154
_sqlBulkBatchWriter.Dispose();
155-
_sqlLogEventWriter.Dispose();
156155
}
157156

158157
_disposedValue = true;

src/Serilog.Sinks.MSSqlServer/Sinks/MSSqlServer/Platform/ISqlCommandFactory.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ namespace Serilog.Sinks.MSSqlServer.Platform
44
{
55
internal interface ISqlCommandFactory
66
{
7-
ISqlCommandWrapper CreateCommand(ISqlConnectionWrapper sqlConnection);
87
ISqlCommandWrapper CreateCommand(string cmdText, ISqlConnectionWrapper sqlConnection);
98
}
109
}

src/Serilog.Sinks.MSSqlServer/Sinks/MSSqlServer/Platform/ISqlLogEventWriter.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
using System;
2-
using System.Collections.Generic;
1+
using System.Collections.Generic;
32
using System.Threading.Tasks;
43
using Serilog.Events;
54

65
namespace Serilog.Sinks.MSSqlServer.Platform
76
{
8-
internal interface ISqlLogEventWriter : IDisposable
7+
internal interface ISqlLogEventWriter
98
{
109
void WriteEvent(LogEvent logEvent);
1110

src/Serilog.Sinks.MSSqlServer/Sinks/MSSqlServer/Platform/SqlBulkBatchWriter.cs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Data;
4-
using System.Linq;
54
using System.Threading.Tasks;
6-
using Serilog.Debugging;
75
using Serilog.Events;
86
using Serilog.Sinks.MSSqlServer.Output;
97

@@ -45,10 +43,10 @@ public async Task WriteBatch(IEnumerable<LogEvent> events)
4543
{
4644
FillDataTable(events);
4745

48-
using (var cn = _sqlConnectionFactory.Create())
46+
using (var sqlConnection = _sqlConnectionFactory.Create())
4947
{
50-
await cn.OpenAsync().ConfigureAwait(false);
51-
using (var copy = cn.CreateSqlBulkCopy(_disableTriggers, _schemaAndTableName))
48+
await sqlConnection.OpenAsync().ConfigureAwait(false);
49+
using (var copy = sqlConnection.CreateSqlBulkCopy(_disableTriggers, _schemaAndTableName))
5250
{
5351
for (var i = 0; i < _dataTable.Columns.Count; i++)
5452
{

src/Serilog.Sinks.MSSqlServer/Sinks/MSSqlServer/Platform/SqlClient/ISqlCommandWrapper.cs

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,10 @@
11
using System;
2-
using System.Data;
32
using System.Threading.Tasks;
4-
using Microsoft.Data.SqlClient;
53

64
namespace Serilog.Sinks.MSSqlServer.Platform.SqlClient
75
{
86
internal interface ISqlCommandWrapper : IDisposable
97
{
10-
CommandType CommandType { get; set; }
11-
string CommandText { get; set; }
12-
13-
void SetConnection(ISqlConnectionWrapper sqlConnectionWrapper);
14-
void ClearParameters();
158
void AddParameter(string parameterName, object value);
169
int ExecuteNonQuery();
1710
Task<int> ExecuteNonQueryAsync();

src/Serilog.Sinks.MSSqlServer/Sinks/MSSqlServer/Platform/SqlClient/SqlCommandWrapper.cs

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,32 +10,9 @@ internal class SqlCommandWrapper : ISqlCommandWrapper
1010
private readonly SqlCommand _sqlCommand;
1111
private bool _disposedValue;
1212

13-
public SqlCommandWrapper(SqlCommand sqlCommand, SqlConnection sqlConnection)
13+
public SqlCommandWrapper(SqlCommand sqlCommand)
1414
{
1515
_sqlCommand = sqlCommand ?? throw new ArgumentNullException(nameof(sqlCommand));
16-
_sqlCommand.Connection = sqlConnection ?? throw new ArgumentNullException(nameof(sqlConnection));
17-
}
18-
19-
public CommandType CommandType
20-
{
21-
get => _sqlCommand.CommandType;
22-
set => _sqlCommand.CommandType = value;
23-
}
24-
25-
public string CommandText
26-
{
27-
get => _sqlCommand.CommandText;
28-
set => _sqlCommand.CommandText = value;
29-
}
30-
31-
public void SetConnection(ISqlConnectionWrapper sqlConnectionWrapper)
32-
{
33-
_sqlCommand.Connection = sqlConnectionWrapper.SqlConnection;
34-
}
35-
36-
public void ClearParameters()
37-
{
38-
_sqlCommand.Parameters.Clear();
3916
}
4017

4118
public void AddParameter(string parameterName, object value)

src/Serilog.Sinks.MSSqlServer/Sinks/MSSqlServer/Platform/SqlCommandFactory.cs

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,10 @@ namespace Serilog.Sinks.MSSqlServer.Platform
55
{
66
internal class SqlCommandFactory : ISqlCommandFactory
77
{
8-
public ISqlCommandWrapper CreateCommand(ISqlConnectionWrapper sqlConnection)
9-
{
10-
var sqlCommand = new SqlCommand();
11-
return new SqlCommandWrapper(sqlCommand, sqlConnection.SqlConnection);
12-
}
13-
148
public ISqlCommandWrapper CreateCommand(string cmdText, ISqlConnectionWrapper sqlConnection)
159
{
16-
var sqlCommand = new SqlCommand(cmdText);
17-
return new SqlCommandWrapper(sqlCommand, sqlConnection.SqlConnection);
10+
var sqlCommand = new SqlCommand(cmdText, sqlConnection.SqlConnection);
11+
return new SqlCommandWrapper(sqlCommand);
1812
}
1913
}
2014
}
Lines changed: 36 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
using System;
22
using System.Collections.Generic;
3-
using System.Data;
43
using System.Linq;
54
using System.Text;
65
using System.Threading.Tasks;
7-
using Serilog.Debugging;
86
using Serilog.Events;
97
using Serilog.Sinks.MSSqlServer.Output;
108
using Serilog.Sinks.MSSqlServer.Platform.SqlClient;
@@ -20,8 +18,7 @@ internal class SqlInsertStatementWriter : ISqlLogEventWriter
2018
private readonly ISqlCommandFactory _sqlCommandFactory;
2119
private readonly ILogEventDataGenerator _logEventDataGenerator;
2220

23-
private ISqlCommandWrapper _sqlCommand;
24-
private bool _disposedValue;
21+
private string _sqlCommandText;
2522

2623
public SqlInsertStatementWriter(
2724
string tableName,
@@ -43,92 +40,68 @@ public SqlInsertStatementWriter(
4340

4441
public async Task WriteEvents(IEnumerable<LogEvent> events)
4542
{
46-
using (var cn = _sqlConnectionFactory.Create())
43+
using (var sqlConnection = _sqlConnectionFactory.Create())
4744
{
48-
await cn.OpenAsync().ConfigureAwait(false);
45+
await sqlConnection.OpenAsync().ConfigureAwait(false);
4946

5047
foreach (var logEvent in events)
5148
{
5249
var fields = _logEventDataGenerator.GetColumnsAndValues(logEvent).ToList();
53-
InitializeSqlCommand(cn, fields);
54-
55-
var index = 0;
56-
_sqlCommand.ClearParameters();
57-
foreach (var field in fields)
50+
using (var sqlCommand = InitializeSqlCommand(sqlConnection, fields))
5851
{
59-
_sqlCommand.AddParameter(Invariant($"@P{index}"), field.Value);
60-
index++;
52+
await sqlCommand.ExecuteNonQueryAsync().ConfigureAwait(false);
6153
}
62-
63-
await _sqlCommand.ExecuteNonQueryAsync().ConfigureAwait(false);
6454
}
6555
}
6656
}
6757

68-
/// <summary>
69-
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
70-
/// </summary>
71-
public void Dispose()
58+
private ISqlCommandWrapper InitializeSqlCommand(
59+
ISqlConnectionWrapper sqlConnection,
60+
IEnumerable<KeyValuePair<string, object>> logEventFields)
7261
{
73-
Dispose(disposing: true);
74-
GC.SuppressFinalize(this);
75-
}
62+
InitializeSqlCommandText(logEventFields);
7663

77-
/// <summary>
78-
/// Releases the unmanaged resources used by the Serilog.Sinks.MSSqlServer.Platform.SqlInsertStatementWriter and optionally
79-
/// releases the managed resources.
80-
/// </summary>
81-
/// <param name="disposing">True to release both managed and unmanaged resources; false to release only unmanaged resources.</param>
82-
protected virtual void Dispose(bool disposing)
83-
{
84-
if (!_disposedValue)
64+
var sqlCommand = _sqlCommandFactory.CreateCommand(_sqlCommandText, sqlConnection);
65+
var index = 0;
66+
foreach (var field in logEventFields)
8567
{
86-
if (disposing)
87-
{
88-
_sqlCommand?.Dispose();
89-
}
90-
91-
_disposedValue = true;
68+
sqlCommand.AddParameter(Invariant($"@P{index}"), field.Value);
69+
index++;
9270
}
71+
72+
return sqlCommand;
9373
}
9474

95-
private void InitializeSqlCommand(ISqlConnectionWrapper sqlConnection,
96-
IEnumerable<KeyValuePair<string, object>> logEventFields)
75+
private void InitializeSqlCommandText(IEnumerable<KeyValuePair<string, object>> logEventFields)
9776
{
98-
// Optimization: generate INSERT statement and SqlCommand only once
99-
// and reuse it with different values and SqlConnections because
100-
// the structure does not change.
101-
if (_sqlCommand == null)
77+
if (_sqlCommandText != null)
10278
{
103-
_sqlCommand = _sqlCommandFactory.CreateCommand(sqlConnection);
104-
_sqlCommand.CommandType = CommandType.Text;
79+
return;
80+
}
10581

106-
var fieldList = new StringBuilder(Invariant($"INSERT INTO [{_schemaName}].[{_tableName}] ("));
107-
var parameterList = new StringBuilder(") VALUES (");
82+
var fieldList = new StringBuilder(Invariant($"INSERT INTO [{_schemaName}].[{_tableName}] ("));
83+
var parameterList = new StringBuilder(") VALUES (");
10884

109-
var index = 0;
110-
foreach (var field in logEventFields)
85+
var index = 0;
86+
foreach (var field in logEventFields)
87+
{
88+
if (index != 0)
11189
{
112-
if (index != 0)
113-
{
114-
fieldList.Append(',');
115-
parameterList.Append(',');
116-
}
117-
118-
fieldList.Append(Invariant($"[{field.Key}]"));
119-
parameterList.Append("@P");
120-
parameterList.Append(index);
121-
122-
index++;
90+
fieldList.Append(',');
91+
parameterList.Append(',');
12392
}
12493

125-
parameterList.Append(')');
126-
fieldList.Append(parameterList);
94+
fieldList.Append(Invariant($"[{field.Key}]"));
95+
parameterList.Append("@P");
96+
parameterList.Append(index);
12797

128-
_sqlCommand.CommandText = fieldList.ToString();
98+
index++;
12999
}
130100

131-
_sqlCommand.SetConnection(sqlConnection);
101+
parameterList.Append(')');
102+
fieldList.Append(parameterList);
103+
104+
_sqlCommandText = fieldList.ToString();
132105
}
133106
}
134107
}

test/Serilog.Sinks.MSSqlServer.PerformanceTests/Sinks/MSSqlServer/Platform/SqlInsertStatementWriterBenchmarks.cs

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ namespace Serilog.Sinks.MSSqlServer.PerformanceTests.Platform;
1313

1414
[MemoryDiagnoser]
1515
[MaxIterationCount(16)]
16-
public class SqlInsertStatementWriterBenchmarks : IDisposable
16+
public class SqlInsertStatementWriterBenchmarks
1717
{
1818
private const string _tableName = "TestTableName";
1919
private const string _schemaName = "TestSchemaName";
@@ -35,7 +35,7 @@ public void Setup()
3535
_sqlCommandWrapperMock = new Mock<ISqlCommandWrapper>();
3636

3737
_sqlConnectionFactoryMock.Setup(f => f.Create()).Returns(_sqlConnectionWrapperMock.Object);
38-
_sqlCommandFactoryMock.Setup(f => f.CreateCommand(It.IsAny<ISqlConnectionWrapper>()))
38+
_sqlCommandFactoryMock.Setup(f => f.CreateCommand(It.IsAny<string>(), It.IsAny<ISqlConnectionWrapper>()))
3939
.Returns(_sqlCommandWrapperMock.Object);
4040

4141
CreateLogEvents();
@@ -67,10 +67,4 @@ private void CreateLogEvents()
6767
_logEvents.Add(CreateLogEvent());
6868
}
6969
}
70-
71-
public void Dispose()
72-
{
73-
GC.SuppressFinalize(this);
74-
_sut.Dispose();
75-
}
7670
}

0 commit comments

Comments
 (0)