Skip to content

Commit f22d170

Browse files
authored
Merge pull request #30 from bartelink/cleanup
Cover blockWhenFull in README; update xUnit; minor tidying
2 parents 870c52e + fda2f4d commit f22d170

File tree

8 files changed

+90
-51
lines changed

8 files changed

+90
-51
lines changed

README.md

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ Install from [NuGet](https://nuget.org/packages/serilog.sinks.async):
1212
Install-Package Serilog.Sinks.Async
1313
```
1414

15-
Assuming you have already installed the target sink, such as the rolling file sink, move the wrapped sink's configuration within a `WriteTo.Async()` statement:
15+
Assuming you have already installed the target sink, such as the file sink, move the wrapped sink's configuration within a `WriteTo.Async()` statement:
1616

1717
```csharp
1818
Log.Logger = new LoggerConfiguration()
19-
.WriteTo.Async(a => a.RollingFile("logs/myapp-{Date}.txt"))
19+
.WriteTo.Async(a => a.File("logs/myapp.log"))
2020
// Other logger configuration
2121
.CreateLogger()
2222

@@ -26,7 +26,7 @@ Log.Information("This will be written to disk on the worker thread");
2626
Log.CloseAndFlush();
2727
```
2828

29-
The wrapped sink (`RollingFile` in this case) will be invoked on a worker thread while your application's thread gets on with more important stuff.
29+
The wrapped sink (`File` in this case) will be invoked on a worker thread while your application's thread gets on with more important stuff.
3030

3131
Because the memory buffer may contain events that have not yet been written to the target sink, it is important to call `Log.CloseAndFlush()` or `Logger.Dispose()` when the application exits.
3232

@@ -36,7 +36,19 @@ The default memory buffer feeding the worker thread is capped to 10,000 items, a
3636

3737
```csharp
3838
// Reduce the buffer to 500 events
39-
.WriteTo.Async(a => a.RollingFile("logs/myapp-{Date}.txt"), 500)
39+
.WriteTo.Async(a => a.File("logs/myapp.log"), bufferSize: 500)
40+
```
41+
42+
### Blocking
43+
44+
Warning: For the same reason one typically does not want exceptions from logging to leak into the execution path, one typically does not want a logger to be able to have the side-efect of actually interrupting application processing until the log propagation has been unblocked.
45+
46+
When the buffer size limit is reached, the default behavior is to drop any further attempted writes until the queue abates, reporting each such failure to the `Serilog.Debugging.SelfLog`. To replace this with a blocking behaviour, set `blockWhenFull` to `true`.
47+
48+
```csharp
49+
// Wait for any queued event to be accepted by the `File` log before allowing the calling thread
50+
// to resume its application work after a logging call when there are 10,000 LogEvents waiting
51+
.WriteTo.Async(a => a.File("logs/myapp.log"), blockWhenFull: true)
4052
```
4153

4254
### XML `<appSettings>` and JSON configuration
@@ -50,7 +62,7 @@ Using [Serilog.Settings.Configuration](https://github.com/serilog/serilog-settin
5062
"Name": "Async",
5163
"Args": {
5264
"configure": [{
53-
"Name": "LiterateConsole"
65+
"Name": "Console"
5466
}]
5567
}
5668
}]

src/Serilog.Sinks.Async/LoggerConfigurationAsyncExtensions.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public static class LoggerConfigurationAsyncExtensions
1616
/// <param name="loggerSinkConfiguration">The <see cref="LoggerSinkConfiguration"/> being configured.</param>
1717
/// <param name="configure">An action that configures the wrapped sink.</param>
1818
/// <param name="bufferSize">The size of the concurrent queue used to feed the background worker thread. If
19-
/// the thread is unable to process events quickly enough and the queue is filled, subsequent events will be
19+
/// the thread is unable to process events quickly enough and the queue is filled, subsequent events will be
2020
/// dropped until room is made in the queue.</param>
2121
/// <returns>A <see cref="LoggerConfiguration"/> allowing configuration to continue.</returns>
2222
[EditorBrowsable(EditorBrowsableState.Never)]
@@ -34,8 +34,8 @@ public static LoggerConfiguration Async(
3434
/// <param name="loggerSinkConfiguration">The <see cref="LoggerSinkConfiguration"/> being configured.</param>
3535
/// <param name="configure">An action that configures the wrapped sink.</param>
3636
/// <param name="bufferSize">The size of the concurrent queue used to feed the background worker thread. If
37-
/// the thread is unable to process events quickly enough and the queue is filled, depending on
38-
/// <paramref name="blockWhenFull"/> the queue will block or subsequent events will be dropped until
37+
/// the thread is unable to process events quickly enough and the queue is filled, depending on
38+
/// <paramref name="blockWhenFull"/> the queue will block or subsequent events will be dropped until
3939
/// room is made in the queue.</param>
4040
/// <param name="blockWhenFull">Block when the queue is full, instead of dropping events.</param>
4141
/// <returns>A <see cref="LoggerConfiguration"/> allowing configuration to continue.</returns>

src/Serilog.Sinks.Async/Sinks/Async/BackgroundWorkerSink.cs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
using System;
22
using System.Collections.Concurrent;
33
using System.Threading;
4+
using System.Threading.Tasks;
45
using Serilog.Core;
56
using Serilog.Debugging;
67
using Serilog.Events;
7-
using System.Threading.Tasks;
88

99
namespace Serilog.Sinks.Async
1010
{
1111
sealed class BackgroundWorkerSink : ILogEventSink, IDisposable
1212
{
1313
readonly ILogEventSink _pipeline;
14-
readonly int _bufferCapacity;
1514
readonly bool _blockWhenFull;
1615
readonly BlockingCollection<LogEvent> _queue;
1716
readonly Task _worker;
@@ -21,15 +20,14 @@ public BackgroundWorkerSink(ILogEventSink pipeline, int bufferCapacity, bool blo
2120
if (pipeline == null) throw new ArgumentNullException(nameof(pipeline));
2221
if (bufferCapacity <= 0) throw new ArgumentOutOfRangeException(nameof(bufferCapacity));
2322
_pipeline = pipeline;
24-
_bufferCapacity = bufferCapacity;
2523
_blockWhenFull = blockWhenFull;
26-
_queue = new BlockingCollection<LogEvent>(_bufferCapacity);
24+
_queue = new BlockingCollection<LogEvent>(bufferCapacity);
2725
_worker = Task.Factory.StartNew(Pump, CancellationToken.None, TaskCreationOptions.LongRunning | TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
2826
}
2927

3028
public void Emit(LogEvent logEvent)
3129
{
32-
if (this._queue.IsAddingCompleted)
30+
if (_queue.IsAddingCompleted)
3331
return;
3432

3533
try
@@ -41,12 +39,12 @@ public void Emit(LogEvent logEvent)
4139
else
4240
{
4341
if (!_queue.TryAdd(logEvent))
44-
SelfLog.WriteLine("{0} unable to enqueue, capacity {1}", typeof(BackgroundWorkerSink), _bufferCapacity);
42+
SelfLog.WriteLine("{0} unable to enqueue, capacity {1}", typeof(BackgroundWorkerSink), _queue.BoundedCapacity);
4543
}
4644
}
4745
catch (InvalidOperationException)
4846
{
49-
// Thrown in the event of a race condition when we try to add another event after
47+
// Thrown in the event of a race condition when we try to add another event after
5048
// CompleteAdding has been called
5149
}
5250
}

test/Serilog.Sinks.Async.PerformanceTests/Serilog.Sinks.Async.PerformanceTests.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818

1919
<ItemGroup>
2020
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.0.0" />
21-
<PackageReference Include="xunit" Version="2.2.0" />
22-
<PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" />
21+
<PackageReference Include="xunit" Version="2.3.1" />
22+
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
2323
<PackageReference Include="BenchmarkDotNet" Version="0.10.6" />
2424
<PackageReference Include="Serilog.Sinks.File" Version="3.1.0" />
2525
</ItemGroup>

test/Serilog.Sinks.Async.Tests/BackgroundWorkerSinkIntegrationSpec.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ public void WhenAuditSingle_ThenQueued()
130130

131131
var result = RetrieveEvents(_memorySink, 1);
132132

133-
Assert.Equal(1, result.Count);
133+
Assert.Single(result);
134134
}
135135

136136
[Fact]

test/Serilog.Sinks.Async.Tests/BackgroundWorkerSinkSpec.cs

Lines changed: 59 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public async Task WhenEmitSingle_ThenRelaysToInnerSink()
3939

4040
await Task.Delay(TimeSpan.FromSeconds(3));
4141

42-
Assert.Equal(1, _innerSink.Events.Count);
42+
Assert.Single(_innerSink.Events);
4343
}
4444
}
4545

@@ -84,46 +84,75 @@ public async Task WhenEmitMultipleTimes_ThenRelaysToInnerSink()
8484
}
8585

8686
[Fact]
87-
public async Task WhenQueueFull_ThenDropsEvents()
87+
public async Task GivenDefaultConfig_WhenQueueOverCapacity_DoesNotBlock()
8888
{
89-
using (var sink = new BackgroundWorkerSink(_logger, 1, false))
89+
var batchTiming = Stopwatch.StartNew();
90+
using (var sink = new BackgroundWorkerSink(_logger, 1, blockWhenFull: false /*default*/))
9091
{
91-
// Cause a delay when emmitting to the inner sink, allowing us to fill the queue to capacity
92-
// after the first event is popped
93-
_innerSink.DelayEmit = TimeSpan.FromMilliseconds(300);
94-
95-
var events = new List<LogEvent>
96-
{
97-
CreateEvent(),
98-
CreateEvent(),
99-
CreateEvent(),
100-
CreateEvent(),
101-
CreateEvent()
102-
};
103-
events.ForEach(e =>
92+
// Cause a delay when emmitting to the inner sink, allowing us to easily fill the queue to capacity
93+
// while the first event is being propagated
94+
var acceptInterval = TimeSpan.FromMilliseconds(500);
95+
_innerSink.DelayEmit = acceptInterval;
96+
var tenSecondsWorth = 10_000 / acceptInterval.TotalMilliseconds + 1;
97+
for (int i = 0; i < tenSecondsWorth; i++)
10498
{
105-
var sw = Stopwatch.StartNew();
106-
sink.Emit(e);
107-
sw.Stop();
99+
var emissionTiming = Stopwatch.StartNew();
100+
sink.Emit(CreateEvent());
101+
emissionTiming.Stop();
108102

109-
Assert.True(sw.ElapsedMilliseconds < 200, "Should not block the caller when the queue is full");
110-
});
103+
// Should not block the caller when the queue is full
104+
Assert.InRange(emissionTiming.ElapsedMilliseconds, 0, 200);
105+
}
111106

112-
// If we *weren't* dropped events, the delay in the inner sink would mean the 5 events would take
113-
// at least 15 seconds to process
114-
await Task.Delay(TimeSpan.FromSeconds(2));
107+
// Allow at least one to propagate
108+
await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
109+
}
110+
// Sanity check the overall timing
111+
batchTiming.Stop();
112+
// Need to add a significant fudge factor as AppVeyor build can result in `await` taking quite some time
113+
Assert.InRange(batchTiming.ElapsedMilliseconds, 950, 2050);
114+
}
115+
116+
[Fact]
117+
public async Task GivenDefaultConfig_WhenRequestsOverCapacity_ThenDropsEventsAndRecovers()
118+
{
119+
using (var sink = new BackgroundWorkerSink(_logger, 1, blockWhenFull: false /*default*/))
120+
{
121+
var acceptInterval = TimeSpan.FromMilliseconds(200);
122+
_innerSink.DelayEmit = acceptInterval;
115123

116-
// Events should be dropped
117-
Assert.Equal(2, _innerSink.Events.Count);
124+
for (int i = 0; i < 2; i++)
125+
{
126+
sink.Emit(CreateEvent());
127+
sink.Emit(CreateEvent());
128+
await Task.Delay(acceptInterval);
129+
sink.Emit(CreateEvent());
130+
}
131+
// Wait for the buffer and propagation to complete
132+
await Task.Delay(TimeSpan.FromSeconds(1));
133+
// Now verify things are back to normal; emit an event...
134+
var finalEvent = CreateEvent();
135+
sink.Emit(finalEvent);
136+
// ... give adequate time for it to be guaranteed to have percolated through
137+
await Task.Delay(TimeSpan.FromSeconds(1));
138+
139+
// At least one of the preceding events should not have made it through
140+
var propagatedExcludingFinal =
141+
from e in _innerSink.Events
142+
where !Object.ReferenceEquals(finalEvent, e)
143+
select e;
144+
Assert.InRange(2, 2 * 3 / 2 - 1, propagatedExcludingFinal.Count());
145+
// Final event should have made it through
146+
Assert.Contains(_innerSink.Events, x => Object.ReferenceEquals(finalEvent, x));
118147
}
119148
}
120149

121150
[Fact]
122-
public async Task WhenQueueFull_ThenBlocks()
151+
public async Task GivenConfiguredToBlock_WhenQueueFilled_ThenBlocks()
123152
{
124-
using (var sink = new BackgroundWorkerSink(_logger, 1, true))
153+
using (var sink = new BackgroundWorkerSink(_logger, 1, blockWhenFull: true))
125154
{
126-
// Cause a delay when emmitting to the inner sink, allowing us to fill the queue to capacity
155+
// Cause a delay when emmitting to the inner sink, allowing us to fill the queue to capacity
127156
// after the first event is popped
128157
_innerSink.DelayEmit = TimeSpan.FromMilliseconds(300);
129158

@@ -141,7 +170,7 @@ public async Task WhenQueueFull_ThenBlocks()
141170
sink.Emit(e);
142171
sw.Stop();
143172

144-
// Emit should return immediately the first time, since the queue is not yet full. On
173+
// Emit should return immediately the first time, since the queue is not yet full. On
145174
// subsequent calls, the queue should be full, so we should be blocked
146175
if (i > 0)
147176
{

test/Serilog.Sinks.Async.Tests/BackgroundWorkerSinkTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public void DisposeCompletesWithoutWorkPerformed()
3232
{
3333
}
3434

35-
Assert.Equal(0, collector.Events.Count);
35+
Assert.Empty(collector.Events);
3636
}
3737
}
3838
}

test/Serilog.Sinks.Async.Tests/Serilog.Sinks.Async.Tests.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515

1616
<ItemGroup>
1717
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.0.0" />
18-
<PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" />
19-
<PackageReference Include="xunit" Version="2.2.0" />
18+
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
19+
<PackageReference Include="xunit" Version="2.3.1" />
2020
</ItemGroup>
2121

2222
<ItemGroup>

0 commit comments

Comments
 (0)